# encoding: utf-8 # This file is distributed under New Relic's license terms. # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details. require 'rubygems' require 'rack' require 'uri' require 'socket' require 'timeout' require 'ostruct' require 'fake_server' require 'json' if RUBY_VERSION >= '1.9' module NewRelic class FakeCollector < FakeServer class Response attr_reader :status, :body def initialize(status, body) @default_status = status @default_body = body @remaining = nil revert end def override(status, body) @status = status @body = body self end def revert @status = @default_status @body = @default_body @remaining = nil end def once @remaining = 1 self end def evaluate if @remaining == 0 revert elsif @remaining @remaining -= 1 end resolved_body = @body.respond_to?(:call) ? @body.call : @body [@status, resolved_body] end end attr_accessor :agent_data, :mock def initialize super @id_counter = 0 @mock = { 'get_redirect_host' => Response.new(200, {'return_value' => 'localhost'}), 'connect' => Response.new(200, Proc.new { {'return_value' => {"agent_run_id" => agent_run_id}} }), 'get_agent_commands' => Response.new(200, {'return_value' => []}), 'agent_command_results' => Response.new(200, {'return_value' => []}), 'metric_data' => Response.new(200, {'return_value' => [[{'name' => 'Some/Metric/Spec'}, 1]]}), 'sql_trace_data' => Response.new(200, {'return_value' => nil}), 'transaction_sample_data' => Response.new(200, {'return_value' => nil}), 'error_data' => Response.new(200, {'return_value' => nil}), 'profile_data' => Response.new(200, {'return_value' => nil}), 'shutdown' => Response.new(200, {'return_value' => nil}), 'analytic_event_data' => Response.new(200, {'return_value' => nil}), } reset end def agent_run_id @id_counter += 1 end def reset @mock.each_value(&:revert) @id_counter = 0 @agent_data = [] end def default_response Response.new(200, {'return_value' => nil}) end def stub(method, return_value, status=200) self.mock[method] ||= default_response self.mock[method].override(status, {'return_value' => return_value}) end def stub_exception(method, exception, status=200) self.mock[method] ||= default_response self.mock[method].override(status, {'exception' => exception}) end def call(env) req = ::Rack::Request.new(env) res = ::Rack::Response.new uri = URI.parse(req.url) if uri.path =~ /agent_listener\/\d+\/.+\/(\w+)/ method = $1 format = json_format?(uri) && RUBY_VERSION >= '1.9' ? :json : :pruby if @mock.keys.include? method status, body = @mock[method].evaluate res.status = status if format == :json res.write JSON.dump(body) else res.write Marshal.dump(body) end else res.status = 500 res.write "Method not found" end run_id = uri.query =~ /run_id=(\d+)/ ? $1 : nil req.body.rewind begin raw_body = req.body.read raw_body = Zlib::Inflate.inflate(raw_body) if req.env["HTTP_CONTENT_ENCODING"] == "deflate" body = if format == :json body = JSON.load(raw_body) else body = Marshal.load(raw_body) end rescue => err body = "UNABLE TO DECODE BODY: #{raw_body}" end @agent_data << AgentPost.create(:action => method, :body => body, :run_id => run_id, :format => format) end res.finish end def json_format?(uri) uri.query && uri.query.include?('marshal_format=json') end def app self end def calls_for(method) @agent_data.select {|d| d.action == method } end def reported_stats_for_metric(name, scope=nil) calls_for('metric_data').map do |post| post.body[3].find do |metric_record| metric_record[0]['name'] == name && (!scope || metric_record[0]['scope'] == scope) end end.compact.map{|m| m[1]} end class AgentPost attr_accessor :action, :body, :run_id, :format def initialize(opts={}) @action = opts[:action] @body = opts[:body] @run_id = opts[:run_id] @format = opts[:format] end def self.create(opts={}) case opts[:action] when 'connect' ConnectPost.new(opts) when 'metric_data' MetricDataPost.new(opts) when 'profile_data' ProfileDataPost.new(opts) when 'sql_trace_data' SqlTraceDataPost.new(opts) when 'transaction_sample_data' TransactionSampleDataPost.new(opts) when 'analytic_event_data' AnalyticEventDataPost.new(opts) else new(opts) end end def [](key) @body[key] end def unblob(blob) self.class.unblob(blob) end def self.unblob(blob) return unless blob JSON.load(Zlib::Inflate.inflate(Base64.decode64(blob))) end end class MetricDataPost < AgentPost def initialize(opts={}) super end def metrics @body[3] end def metric_names metrics.map {|m| m[0]["name"] } end end class ConnectPost < AgentPost def initialize(opts={}) super @body = @body[0] end end class ProfileDataPost < AgentPost attr_accessor :sample_count, :traces def initialize(opts={}) super @sample_count = @body[1][0][3] @body[1][0][4] = unblob(@body[1][0][4]) if @format == :json @traces = @body[1][0][4] end end class SqlTraceDataPost < AgentPost def initialize(opts={}) super @body[0][0][9] = unblob(@body[0][0][9]) if @format == :json end end class TransactionSampleDataPost < AgentPost class SubmittedTransactionTrace def initialize(body, format) @body = body @format = format end def metric_name @body[2] end def uri @body[3] end def tree SubmittedTransactionTraceTree.new(@body[4], @format) end def xray_id @body[8] end end class SubmittedTransactionTraceTree def initialize(body, format) @body = body @body = AgentPost.unblob(@body) if format == :json end def request_params normalize_params(@body[1]) end def custom_params normalize_params(@body[2]) end # Pruby marshalled stuff ends up with symbol keys. JSON ends up with # strings. This makes writing tests on the params annoying. Normalize # it all to string keys. def normalize_params(params) result = {} @body[2].each do |k,v| result[k.to_s] = v end result end end def initialize(opts={}) super @body[4] = unblob(@body[4]) if @format == :json end def samples @samples ||= @body[1].map { |s| SubmittedTransactionTrace.new(s, @format) } end def metric_name samples.first.metric_name end end class AnalyticEventDataPost < AgentPost def initialize(opts={}) opts[:run_id] = opts[:body].shift opts[:body] = opts[:body].shift super end end end end if $0 == __FILE__ require 'test/unit' require 'net/http' class FakeCollectorTest < Test::Unit::TestCase def setup @collector = NewRelic::FakeCollector.new @collector.run end def teardown @collector.stop end def test_get_redirect @collector.mock['get_redirect_host'].evaluate = [200, 'test.example.com'] response = invoke('get_redirect_host') assert_equal 'test.example.com', response assert_equal 'get_redirect_host', @collector.agent_data[0].action end def test_connect response = invoke('connect') assert_equal 1, response['agent_run_id'] assert_equal 'connect', @collector.agent_data[0].action end def test_metric_data response = invoke('metric_data?run_id=1', {'Foo/Bar' => [1,2,3], 'Baz/Cux' => [4,5,6]}) assert_equal 1, response['Some/Metric/Spec'] post = @collector.agent_data[0] assert_equal 'metric_data', post.action assert_equal({'Foo/Bar' => [1,2,3], 'Baz/Cux' => [4,5,6]}, post.body) assert_equal 1, post.run_id.to_i end def test_sql_trace_data response = invoke('sql_trace_data?run_id=2', ['trace', 'trace', 'trace']) assert_nil response post = @collector.agent_data[0] assert_equal 'sql_trace_data', post.action assert_equal ['trace', 'trace', 'trace'], post.body assert_equal 2, post.run_id.to_i end def test_transaction_sample_data response = invoke('transaction_sample_data?run_id=3', ['node', ['node', 'node'], 'node']) assert_nil response post = @collector.agent_data[0] assert_equal 'transaction_sample_data', post.action assert_equal ['node', ['node', 'node'], 'node'], post.body assert_equal 3, post.run_id.to_i end def test_transaction_sample_data_json response = invoke('transaction_sample_data?run_id=3&marshal_format=json', '["node",["node","node"],"node"]') assert_equal '', response post = @collector.agent_data[0] assert_equal 'transaction_sample_data', post.action assert_equal '["node",["node","node"],"node"]', post.body assert_equal 3, post.run_id.to_i end def test_error_data response = invoke('error_data?run_id=4', ['error']) assert_nil response post = @collector.agent_data[0] assert_equal 'error_data', post.action assert_equal ['error'], post.body end def test_analytic_event_data events = [ { 'type' => 'Transaction', 'name' => 'Controller/foo/bar', 'duration' => '718', 'timestamp' => 1368489547.888435 } ] response = invoke('analytic_event_data', [33, events]) assert_nil response['return_value'] post = @collector.agent_data[0] assert_equal 'analytic_event_data', post.action assert_equal events, post.body assert_equal 33, post.run_id.to_i end def test_shutdown response = invoke('shutdown?run_id=1') assert_nil response assert_equal 'shutdown', @collector.agent_data[0].action end def test_multiple_invokations pid = Process.fork do invoke('get_redirect_host') invoke('connect') invoke('metric_data?run_id=1') invoke('transaction_sample_data?run_id=1') invoke('shutdown?run_id=1') end invoke('get_redirect_host') invoke('connect') invoke('metric_data?run_id=2') invoke('transaction_sample_data?run_id=2') invoke('shutdown?run_id=2') Process.wait(pid) expected = ['get_redirect_host', 'connect', 'metric_data', 'transaction_sample_data', 'shutdown', 'get_redirect_host', 'connect', 'metric_data', 'transaction_sample_data', 'shutdown'] assert_equal expected.sort, @collector.agent_data.map(&:action).sort end def test_reset @collector.mock['get_redirect_host'].evaluate = [200, 'never!'] @collector.reset assert_equal [200, 'localhost'], @collector.mock['get_redirect_host'].evaluate end def invoke(method, post={}, code=200) uri = URI.parse("http://127.0.0.1:#{@collector.determine_port}/agent_listener/8/12345/#{method}") request = Net::HTTP::Post.new("#{uri.path}?#{uri.query}") if uri.query && uri.query.include?('marshal_format=json') request.body = JSON.dump(post) else request.body = Marshal.dump(post) end response = Net::HTTP.start(uri.host, uri.port) do |http| http.request(request) end if uri.query && uri.query.include?('marshal_format=json') JSON.load(response.body) else Marshal.load(response.body) end end end end