#!/usr/bin/env ruby
# encoding: utf-8
require 'net/http'
require File.expand_path(File.join(File.dirname(__FILE__),'..','..','..','test_helper'))
require 'new_relic/agent/cross_app_tracing'
# Add some stuff to Net::HTTP::HTTPResponse to facilitate building response data
class Net::HTTPResponse
def to_s
buf = ''
buf << "HTTP/%s %d %s\r\n" % [ self.http_version, self.code, self.message ]
self.each_capitalized {|k,v| buf << k << ': ' << v << "\r\n" }
buf << "\r\n"
buf << @body if @body
return buf
end
def initialize_copy( original )
@http_version = @http_version.dup
@code = @code.dup
@message = @message.dup
@body = @body.dup
@read = false
@header = @header.dup
end
unless instance_methods.map {|name| name.to_sym }.include?( :body= )
def body=( newbody )
@body = newbody
end
end
end
class NewRelic::Agent::Instrumentation::NetInstrumentationTest < Test::Unit::TestCase
include NewRelic::Agent::Instrumentation::ControllerInstrumentation,
NewRelic::Agent::CrossAppMonitor::EncodingFunctions,
NewRelic::Agent::CrossAppTracing
CANNED_RESPONSE = Net::HTTPOK.new( '1.1', '200', 'OK' )
CANNED_RESPONSE.body =
'
Canned ResponseCanned response.'
CANNED_RESPONSE['content-type'] = 'text/html; charset=UTF-8'
CANNED_RESPONSE['date'] = 'Tue, 29 Jan 2013 21:52:04 GMT'
CANNED_RESPONSE['expires'] = '-1'
CANNED_RESPONSE['server'] = 'gws'
CANNED_RESPONSE.freeze
TRANSACTION_GUID = 'BEC1BC64675138B9'
def setup
NewRelic::Agent.manual_start(
:"cross_application_tracer.enabled" => false,
:"transaction_tracer.enabled" => true,
:cross_process_id => '269975#22824',
:encoding_key => 'gringletoes'
)
# $stderr.puts '', '---', ''
# NewRelic::Agent.logger = NewRelic::Agent::AgentLogger.new( {:log_level => 'debug'}, '', Logger.new($stderr) )
@engine = NewRelic::Agent.instance.stats_engine
@engine.clear_stats
@engine.start_transaction( 'test' )
NewRelic::Agent::TransactionInfo.get.guid = TRANSACTION_GUID
@sampler = NewRelic::Agent.instance.transaction_sampler
@sampler.notice_first_scope_push( Time.now.to_f )
@sampler.notice_transaction( '/path', '/path', {} )
@sampler.notice_push_scope "Controller/sandwiches/index"
@sampler.notice_sql("SELECT * FROM sandwiches WHERE bread = 'wheat'", nil, 0)
@sampler.notice_push_scope "ab"
@sampler.notice_sql("SELECT * FROM sandwiches WHERE bread = 'white'", nil, 0)
@sampler.notice_push_scope "fetch_external_service"
@response = CANNED_RESPONSE.clone
@socket = fixture_tcp_socket( @response )
end
def teardown
NewRelic::Agent.instance.transaction_sampler.reset!
Thread::current[:newrelic_scope_stack] = nil
NewRelic::Agent.instance.stats_engine.end_transaction
end
#
# Helpers
#
def fixture_tcp_socket( response )
# Don't actually talk to Google.
socket = stub("socket") do
stubs(:closed?).returns(false)
stubs(:close)
# Simulate a bunch of socket-ey stuff since Mocha doesn't really
# provide any other way to do it
class << self
attr_accessor :response, :write_checker
end
def self.check_write
self.write_checker = Proc.new
end
def self.write( buf )
self.write_checker.call( buf ) if self.write_checker
buf.length
end
def self.sysread( size, buf='' )
@data ||= response.to_s
raise EOFError if @data.empty?
buf.replace @data.slice!( 0, size )
buf
end
class << self
alias_method :read_nonblock, :sysread
end
end
socket.response = response
TCPSocket.stubs( :open ).returns( socket )
return socket
end
def make_app_data_payload( *args )
return obfuscate_with_key( 'gringletoes', args.to_json ).gsub( /\n/, '' ) + "\n"
end
def find_last_segment
builder = NewRelic::Agent.agent.transaction_sampler.builder
last_segment = nil
builder.current_segment.each_segment {|s| last_segment = s }
return last_segment
end
#
# Tests
#
def test_get
url = URI.parse('http://www.google.com/index.html')
res = Net::HTTP.start(url.host, url.port) {|http|
http.get('/index.html')
}
assert_match %r//i, res.body
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_background
res = nil
perform_action_with_newrelic_trace("task", :category => :task) do
url = URI.parse('http://www.google.com/index.html')
res = Net::HTTP.start(url.host, url.port) {|http|
http.get('/index.html')
}
end
assert_match %r//i, res.body
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics,
'OtherTransaction/Background/NewRelic::Agent::Instrumentation::NetInstrumentationTest/task'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_transactional
res = nil
perform_action_with_newrelic_trace("task") do
url = URI.parse('http://www.google.com/index.html')
res = Net::HTTP.start(url.host, url.port) {|http|
http.get('/index.html')
}
end
assert_match %r//i, res.body
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/allWeb'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics,
'Controller/NewRelic::Agent::Instrumentation::NetInstrumentationTest/task'
assert_not_includes @engine.metrics, 'External/allOther'
end
def test_get__simple
Net::HTTP.get URI.parse('http://www.google.com/index.html')
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_ignore
NewRelic::Agent.disable_all_tracing do
url = URI.parse('http://www.google.com/index.html')
res = Net::HTTP.start(url.host, url.port) {|http|
http.post('/index.html','data')
}
end
assert_not_includes @engine.metrics, 'External/all'
assert_not_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_not_includes @engine.metrics, 'External/allOther'
assert_not_includes @engine.metrics, 'External/www.google.com/all'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_head
url = URI.parse('http://www.google.com/index.html')
res = Net::HTTP.start(url.host, url.port) {|http|
http.head('/index.html')
}
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/HEAD'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_post
url = URI.parse('http://www.google.com/index.html')
res = Net::HTTP.start(url.host, url.port) {|http|
http.post('/index.html','data')
}
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/POST'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_not_includes @engine.metrics, 'External/allWeb'
end
# When an http call is made, the agent should add a request header named
# X-NewRelic-ID with a value equal to the encoded cross_app_id.
def test_adds_a_request_header_to_outgoing_requests_if_xp_enabled
@socket.check_write do |data|
# assert_match /(?i:x-newrelic-id): VURQV1BZRkZdXUFT/, data
# The above assertion won't work in Ruby 2.0.0-p0 because of a bug in the
# regexp engine. Until that's fixed we'll check the header name case
# sensitively.
assert_match /X-Newrelic-Id: VURQV1BZRkZdXUFT/, data
end
with_config(:"cross_application_tracer.enabled" => true) do
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
end
def test_adds_a_request_header_to_outgoing_requests_if_old_xp_config_is_present
@socket.check_write do |data|
# assert_match /(?i:x-newrelic-id): VURQV1BZRkZdXUFT/, data
# The above assertion won't work in Ruby 2.0.0-p0 because of a bug in the
# regexp engine. Until that's fixed we'll check the header name case
# sensitively.
assert_match /X-Newrelic-Id: VURQV1BZRkZdXUFT/, data
end
with_config(:cross_application_tracing => true) do
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
end
def test_agent_doesnt_add_a_request_header_to_outgoing_requests_if_xp_disabled
@socket.check_write do |data|
# assert_no_match /(?i:x-newrelic-id): VURQV1BZRkZdXUFT/, data
# The above assertion won't work in Ruby 2.0.0-p0 because of a bug in the
# regexp engine. Until that's fixed we'll check the header name case
# sensitively.
assert_no_match /X-Newrelic-Id: VURQV1BZRkZdXUFT/, data
end
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
def test_instrumentation_with_crossapp_enabled_records_normal_metrics_if_no_header_present
with_config(:"cross_application_tracer.enabled" => true) do
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
assert_equal 5, @engine.metrics.length
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET:test'
assert_not_includes @engine.metrics, 'ExternalApp/www.google.com/18#1884/all'
assert_not_includes @engine.metrics, 'ExternalTransaction/www.google.com/18#1884/txn-name'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_instrumentation_with_crossapp_disabled_records_normal_metrics_even_if_header_is_present
@response[ NR_APPDATA_HEADER ] =
make_app_data_payload( '18#1884', 'txn-name', 2, 8, 0, TRANSACTION_GUID )
Net::HTTP.get URI.parse('http://www.google.com/index.html')
assert_equal 5, @engine.metrics.length
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET:test'
assert_not_includes @engine.metrics, 'ExternalApp/www.google.com/18#1884/all'
assert_not_includes @engine.metrics, 'ExternalTransaction/www.google.com/18#1884/txn-name'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_instrumentation_with_crossapp_enabled_records_crossapp_metrics_if_header_present
@response[ NR_APPDATA_HEADER ] =
make_app_data_payload( '18#1884', 'txn-name', 2, 8, 0, TRANSACTION_GUID )
with_config(:"cross_application_tracer.enabled" => true) do
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
assert_equal 6, @engine.metrics.length
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'ExternalApp/www.google.com/18#1884/all'
assert_includes @engine.metrics, 'ExternalTransaction/www.google.com/18#1884/txn-name'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics, 'ExternalTransaction/www.google.com/18#1884/txn-name:test'
assert_not_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_not_includes @engine.metrics, 'External/allWeb'
last_segment = find_last_segment()
assert_includes last_segment.params.keys, :transaction_guid
assert_equal TRANSACTION_GUID, last_segment.params[:transaction_guid]
end
def test_crossapp_metrics_allow_valid_utf8_characters
@response[ NR_APPDATA_HEADER ] =
make_app_data_payload( '12#1114', '世界線航跡蔵', 18.0, 88.1, 4096, TRANSACTION_GUID )
with_config(:"cross_application_tracer.enabled" => true) do
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
assert_equal 6, @engine.metrics.length
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'ExternalApp/www.google.com/12#1114/all'
assert_includes @engine.metrics, 'ExternalTransaction/www.google.com/12#1114/世界線航跡蔵'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics, 'ExternalTransaction/www.google.com/12#1114/世界線航跡蔵:test'
assert_not_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_not_includes @engine.metrics, 'External/allWeb'
last_segment = find_last_segment()
assert_includes last_segment.params.keys, :transaction_guid
assert_equal TRANSACTION_GUID, last_segment.params[:transaction_guid]
end
def test_crossapp_metrics_ignores_crossapp_header_with_malformed_crossprocess_id
@response[ NR_APPDATA_HEADER ] =
make_app_data_payload( '88#88#88', 'invalid', 1, 2, 4096, TRANSACTION_GUID )
with_config(:"cross_application_tracer.enabled" => true) do
Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
assert_equal 5, @engine.metrics.length
assert_includes @engine.metrics, 'External/all'
assert_includes @engine.metrics, 'External/allOther'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET'
assert_includes @engine.metrics, 'External/www.google.com/all'
assert_includes @engine.metrics, 'External/www.google.com/Net::HTTP/GET:test'
assert_not_includes @engine.metrics, 'ExternalApp/www.google.com/88#88#88/all'
assert_not_includes @engine.metrics, 'ExternalTransaction/www.google.com/88#88#88/invalid'
assert_not_includes @engine.metrics, 'External/allWeb'
end
def test_doesnt_affect_the_request_if_an_exception_is_raised_while_setting_up_tracing
res = nil
NewRelic::Agent.instance.stats_engine.stubs( :push_scope ).
raises( NoMethodError, "undefined method `push_scope'" )
with_config(:"cross_application_tracer.enabled" => true) do
assert_nothing_raised do
res = Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
end
assert_equal res, CANNED_RESPONSE.instance_variable_get( :@body )
end
def test_doesnt_affect_the_request_if_an_exception_is_raised_while_finishing_tracing
res = nil
NewRelic::Agent.instance.stats_engine.stubs( :pop_scope ).
raises( NoMethodError, "undefined method `pop_scope'" )
with_config(:"cross_application_tracer.enabled" => true) do
assert_nothing_raised do
res = Net::HTTP.get URI.parse('http://www.google.com/index.html')
end
end
assert_equal res, CANNED_RESPONSE.instance_variable_get( :@body )
end
def test_scope_stack_integrity_maintained_on_request_failure
@socket.stubs(:write).raises('fake network error')
with_config(:"cross_application_tracer.enabled" => true) do
assert_nothing_raised do
expected = @engine.push_scope('dummy')
Net::HTTP.get(URI.parse('http://www.google.com/index.html')) rescue nil
@engine.pop_scope(expected, 42)
end
end
end
end