Skip to content
Browse files

first version of the rpm agent gem

  • Loading branch information...
0 parents commit ab5dedc169cc47aafd7c6e6448707ea91c65fdde @bkayser bkayser committed Oct 30, 2008
Showing with 8,091 additions and 0 deletions.
  1. +6 −0 .gitignore
  2. +17 −0 .project
  3. +37 −0 LICENSE
  4. +57 −0 README
  5. +38 −0 Rakefile
  6. +16 −0 init.rb
  7. +24 −0 install.rb
  8. +20 −0 lib/new_relic/agent.rb
  9. +767 −0 lib/new_relic/agent/agent.rb
  10. +76 −0 lib/new_relic/agent/collection_helper.rb
  11. +98 −0 lib/new_relic/agent/error_collector.rb
  12. +104 −0 lib/new_relic/agent/instrumentation/action_controller.rb
  13. +23 −0 lib/new_relic/agent/instrumentation/action_web_service.rb
  14. +97 −0 lib/new_relic/agent/instrumentation/active_record.rb
  15. +217 −0 lib/new_relic/agent/instrumentation/dispatcher.rb
  16. +32 −0 lib/new_relic/agent/instrumentation/errors.rb
  17. +18 −0 lib/new_relic/agent/instrumentation/memcache.rb
  18. +14 −0 lib/new_relic/agent/instrumentation/memcached.rb
  19. +10 −0 lib/new_relic/agent/instrumentation/rails.rb
  20. +165 −0 lib/new_relic/agent/method_tracer.rb
  21. +28 −0 lib/new_relic/agent/samplers/cpu.rb
  22. +47 −0 lib/new_relic/agent/samplers/memory.rb
  23. +240 −0 lib/new_relic/agent/stats_engine.rb
  24. +43 −0 lib/new_relic/agent/synchronize.rb
  25. +280 −0 lib/new_relic/agent/transaction_sampler.rb
  26. +106 −0 lib/new_relic/agent/worker_loop.rb
  27. +176 −0 lib/new_relic/config.rb
  28. +39 −0 lib/new_relic/config/merb.rb
  29. +110 −0 lib/new_relic/config/rails.rb
  30. +9 −0 lib/new_relic/config/ruby.rb
  31. +108 −0 lib/new_relic/local_environment.rb
  32. +6 −0 lib/new_relic/merbtasks.rb
  33. +29 −0 lib/new_relic/metric_data.rb
  34. +39 −0 lib/new_relic/metric_spec.rb
  35. +7 −0 lib/new_relic/metrics.rb
  36. +21 −0 lib/new_relic/noticed_error.rb
  37. +91 −0 lib/new_relic/shim_agent.rb
  38. +335 −0 lib/new_relic/stats.rb
  39. +122 −0 lib/new_relic/transaction_analysis.rb
  40. +500 −0 lib/new_relic/transaction_sample.rb
  41. +93 −0 lib/new_relic/version.rb
  42. +26 −0 lib/newrelic.rb
  43. +135 −0 newrelic.yml
  44. +9 −0 newrelic_spec.rb
  45. +1 −0 spec_helper.rb
  46. +14 −0 tasks/agent_tests.rake
  47. +9 −0 tasks/install.rake
  48. +23 −0 test/config/newrelic.yml
  49. +10 −0 test/config/test_config.rb
  50. +40 −0 test/new_relic/agent/mock_ar_connection.rb
  51. +23 −0 test/new_relic/agent/mock_scope_listener.rb
  52. +107 −0 test/new_relic/agent/tc_active_record.rb
  53. +67 −0 test/new_relic/agent/tc_agent.rb
  54. +32 −0 test/new_relic/agent/tc_collection_helper.rb
  55. +59 −0 test/new_relic/agent/tc_controller.rb
  56. +117 −0 test/new_relic/agent/tc_error_collector.rb
  57. +297 −0 test/new_relic/agent/tc_method_tracer.rb
  58. +218 −0 test/new_relic/agent/tc_stats_engine.rb
  59. +37 −0 test/new_relic/agent/tc_synchronize.rb
  60. +175 −0 test/new_relic/agent/tc_transaction_sample.rb
  61. +200 −0 test/new_relic/agent/tc_transaction_sample_builder.rb
  62. +294 −0 test/new_relic/agent/tc_transaction_sampler.rb
  63. +69 −0 test/new_relic/agent/tc_worker_loop.rb
  64. +13 −0 test/new_relic/agent/testable_agent.rb
  65. +29 −0 test/new_relic/tc_config.rb
  66. +102 −0 test/new_relic/tc_environment.rb
  67. +150 −0 test/new_relic/tc_metric_spec.rb
  68. +141 −0 test/new_relic/tc_stats.rb
  69. +38 −0 test/test_helper.rb
  70. +184 −0 ui/controllers/newrelic_controller.rb
  71. +55 −0 ui/helpers/google_pie_chart.rb
  72. +274 −0 ui/helpers/newrelic_helper.rb
  73. +49 −0 ui/views/layouts/newrelic_default.rhtml
  74. +27 −0 ui/views/newrelic/_explain_plans.rhtml
  75. +12 −0 ui/views/newrelic/_sample.rhtml
  76. +28 −0 ui/views/newrelic/_segment.rhtml
  77. +14 −0 ui/views/newrelic/_segment_row.rhtml
  78. +22 −0 ui/views/newrelic/_show_sample_detail.rhtml
  79. +19 −0 ui/views/newrelic/_show_sample_sql.rhtml
  80. +3 −0 ui/views/newrelic/_show_sample_summary.rhtml
  81. +11 −0 ui/views/newrelic/_sql_row.rhtml
  82. +30 −0 ui/views/newrelic/_stack_trace.rhtml
  83. +12 −0 ui/views/newrelic/_table.rhtml
  84. +45 −0 ui/views/newrelic/explain_sql.rhtml
  85. BIN ui/views/newrelic/images/16-arrow-down.png
  86. BIN ui/views/newrelic/images/16-arrow-right.png
  87. BIN ui/views/newrelic/images/blue_bar.gif
  88. BIN ui/views/newrelic/images/gray_bar.gif
  89. +37 −0 ui/views/newrelic/index.rhtml
  90. +107 −0 ui/views/newrelic/javascript/transaction_sample.js
  91. +2 −0 ui/views/newrelic/sample_not_found.rhtml
  92. +62 −0 ui/views/newrelic/show_sample.rhtml
  93. +3 −0 ui/views/newrelic/show_source.rhtml
  94. +394 −0 ui/views/newrelic/stylesheets/style.css
  95. +1 −0 version.txt
6 .gitignore
@@ -0,0 +1,6 @@
+.DS\_Store
+.svn/
+*~
+pkg/
+*.gem
+
17 .project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>New Relic Agent</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.rubypeople.rdt.core.rubybuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.rubypeople.rdt.core.rubynature</nature>
+ </natures>
+</projectDescription>
37 LICENSE
@@ -0,0 +1,37 @@
+Copyright (c) 2008 New Relic, Inc. All rights reserved.
+
+Certain inventions disclosed in this file may be claimed within
+patents owned or patent applications filed by New Relic, Inc. or third
+parties.
+
+Subject to the terms of this notice, New Relic grants you a
+nonexclusive, nontransferable license, without the right to
+sublicense, to (a) install and execute one copy of these files on any
+number of workstations owned or controlled by you and (b) distribute
+verbatim copies of these files to third parties. As a condition to the
+foregoing grant, you must provide this notice along with each copy you
+distribute and you must not remove, alter, or obscure this notice. All
+other use, reproduction, modification, distribution, or other
+exploitation of these files is strictly prohibited, except as may be set
+forth in a separate written license agreement between you and New
+Relic. The terms of any such license agreement will control over this
+notice. The license stated above will be automatically terminated and
+revoked if you exceed its scope or violate any of the terms of this
+notice.
+
+This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of New Relic, except as
+required for reasonable and customary use in describing the origin of
+this file and reproducing the content of this notice. You may not
+mark or brand this file with any trade name, trademarks, service
+marks, or product names other than the original brand (if any)
+provided by New Relic.
+
+Unless otherwise expressly agreed by New Relic in a separate written
+license agreement, these files are provided AS IS, WITHOUT WARRANTY OF
+ANY KIND, including without any implied warranties of MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE, or NON-INFRINGEMENT. As a
+condition to your use of these files, you are solely responsible for
+such use. New Relic will have no liability to you for direct,
+indirect, consequential, incidental, special, or punitive damages or
+for lost profits or data.
57 README
@@ -0,0 +1,57 @@
+=============
+NEW RELIC RPM
+=============
+
+NewRelic RPM is a Ruby performance management system, developed by New Relic, Inc.
+RPM provides you with deep information about the performance of your Ruby on Rails
+or Merb application as it runs in production. The New Relic Agent is distributed as
+a either a Rails plugin or a Gem.
+
+Our testing shows that this agent introduces minimal overhead: in production, a
+typical controller action will be impacted by less than 5 milliseconds in
+response time. (That's significantly less time than is typically spent in mapping
+a request to the appropriate controller action.) So, you can run RPM all the
+time, and always have access to the information you need to resolve performance
+problems and ensure your application can scale to meet the demands of your
+business.
+
+The New Relic Agent runs in one of two modes:
+
+DEVELOPER MODE
+------------
+Developer mode is on by default when you run your application in the development
+environment (but not when it runs in other environments.) When running in
+developer mode, the RPM will track the performance of every http request serviced
+by your application, and store in memory this information for the last 100 http
+transactions.
+
+When running in Developer Mode, the RPM will also add a few pages to
+your application that allow you to analyze this performance information. (Don't
+worry - those pages are not added to your application's routes when you run in
+production mode.)
+
+To view this performance information, including detailed SQL statement analysis,
+open '/newrelic' in your web application. For instance if you are running
+mongrel or thin on port 3000, enter the following into your browser:
+
+http://localhost:3000/newrelic
+
+PRODUCTION MODE
+---------------
+When your application runs in the production environment, the NewRelic agent
+runs in production mode. It connects to the New Relic RPM service and sends deep
+performance data to the RPM service for your analysis. To view this data, login
+to http://rpm.newrelic.com.
+
+NOTE: you must have a valid account and license key to view this data online.
+When you sign up for an account at newrelic.com, you will be provided with a
+license key, as well as a default configuration file for NewRelic RPM. You can
+either paste your license key into your existing configuration file,
+config/newrelic.yml, or you can replace that config file with the one included in
+your welcome email.
+
+Thank you, and may your application scale to infinity plus one.
+
+Lew Cirne, Founder and CEO
+New Relic, Inc.
+
38 Rakefile
@@ -0,0 +1,38 @@
+require 'rubygems'
+require 'rake/gempackagetask'
+require 'lib/new_relic/version.rb'
+
+GEM_NAME = "newrelic"
+GEM_VERSION = NewRelic::VERSION::STRING
+AUTHOR = "Bill Kayser"
+EMAIL = "bkayser@newrelic.com"
+HOMEPAGE = "http://www.newrelic.com"
+SUMMARY = "Performance Monitoring Agent for New Relic Ruby Performance Monitoring Service"
+
+spec = Gem::Specification.new do |s|
+ s.rubyforge_project = 'newrelic'
+ s.name = GEM_NAME
+ s.version = GEM_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.has_rdoc = true
+ s.extra_rdoc_files = ["README", "LICENSE"]
+ s.summary = SUMMARY
+ s.description = s.summary
+ s.author = AUTHOR
+ s.email = EMAIL
+ s.homepage = HOMEPAGE
+ s.require_path = 'lib'
+ s.files = %w(install.rb LICENSE README newrelic.yml Rakefile) + Dir.glob("{lib,tasks,test,ui}/**/*")
+
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.gem_spec = spec
+end
+
+desc "Create a gemspec file"
+task :gemspec do
+ File.open("#{GEM_NAME}.gemspec", "w") do |file|
+ file.puts spec.to_ruby
+ end
+end
16 init.rb
@@ -0,0 +1,16 @@
+# This is the initialization for the RPM Rails plugin
+##require 'new_relic/config'
+
+# Initializer for the NewRelic Agent
+begin
+
+ # START THE AGENT
+ # We install the shim agent unless the tracers are enabled, the plugin
+ # env setting is not false, and the agent started okay.
+ NewRelic::Config.instance.start_plugin (defined?(config) ? config : nil)
+
+rescue => e
+ NewRelic::Config.instance.log! "Error initializing New Relic plugin (#{e})", :error
+ NewRelic::Config.instance.log! e.backtrace.join("\n"), :error
+ NewRelic::Config.instance.log! "Agent is disabled."
+end
24 install.rb
@@ -0,0 +1,24 @@
+require 'ftools'
+
+puts IO.read(File.join(File.dirname(__FILE__), 'README'))
+
+dest_config_file = File.expand_path("#{File.dirname(__FILE__)}/../../../config/newrelic.yml")
+src_config_file = "#{File.dirname(__FILE__)}/newrelic.yml"
+
+if File::exists? dest_config_file
+ puts "\nA config file already exists at #{dest_config_file}.\n"
+else
+ generated_for_user = ""
+ license_key = "PASTE_YOUR_KEY_HERE"
+
+ yaml = eval "%Q[#{File.read(src_config_file)}]"
+
+ File.open( dest_config_file, 'w' ) do |out|
+ out.puts yaml
+ end
+
+ puts "\nInstalling a default configuration file."
+ puts "To monitor your application in production mode, you must enter a license key."
+ puts "See #{dest_config_file}"
+ puts "For a license key, sign up at http://rpm.newrelic.com/signup."
+end
20 lib/new_relic/agent.rb
@@ -0,0 +1,20 @@
+require 'new_relic/version'
+require 'new_relic/local_environment'
+require 'new_relic/stats'
+require 'new_relic/metric_spec'
+require 'new_relic/metric_data'
+require 'new_relic/transaction_analysis'
+require 'new_relic/transaction_sample'
+
+require 'new_relic/agent/agent'
+require 'new_relic/agent/method_tracer'
+require 'new_relic/agent/synchronize'
+require 'new_relic/agent/worker_loop'
+require 'new_relic/agent/stats_engine'
+require 'new_relic/agent/collection_helper'
+require 'new_relic/agent/transaction_sampler'
+require 'new_relic/agent/error_collector'
+
+module NewRelic::Agent
+
+end
767 lib/new_relic/agent/agent.rb
@@ -0,0 +1,767 @@
+require 'net/https'
+require 'net/http'
+require 'logger'
+require 'singleton'
+require 'zlib'
+require 'stringio'
+
+
+# The NewRelic Agent collects performance data from ruby applications in realtime as the
+# application runs, and periodically sends that data to the NewRelic server.
+module NewRelic::Agent
+ # an exception that is thrown by the server if the agent license is invalid
+ class LicenseException < StandardError; end
+
+ # an exception that forces an agent to stop reporting until its mongrel is restarted
+ class ForceDisconnectException < StandardError; end
+
+ class IgnoreSilentlyException < StandardError; end
+
+ class ServerError < StandardError; end
+
+ # add some convenience methods for easy access to the Agent singleton.
+ # the following static methods all point to the same Agent instance:
+ #
+ # NewRelic::Agent.agent
+ # NewRelic::Agent.instance
+ # NewRelic::Agent::Agent.instance
+ class << self
+ def agent
+ NewRelic::Agent::Agent.instance
+ end
+
+ alias instance agent
+
+ # Get or create a statistics gatherer that will aggregate numerical data
+ # under a metric name.
+ #
+ # metric_name should follow a slash separated path convention. Application
+ # specific metrics should begin with "Custom/".
+ #
+ # the statistical gatherer returned by get_stats accepts data
+ # via calls to add_data_point(value)
+ def get_stats(metric_name)
+ agent.stats_engine.get_stats(metric_name, false)
+ end
+
+
+ # Call this to manually start the Agent in situations where the Agent does
+ # not auto-start.
+ # When the app environment loads, so does the Agent. However, the Agent will
+ # only connect to RPM if a web plugin is found. If you want to selectively monitor
+ # ruby processes that don't use web plugins, then call this method in your
+ # code and the Agent will fire up and start reporting to RPM.
+ #
+ # environment - the name of the environment. used for logging only
+ # port - the name of this instance. shows up in the RPM UI screens. can be any String
+ #
+ def manual_start(environment, identifier)
+ agent.manual_start(environment, identifier)
+ end
+
+ # This method sets the block sent to this method as a sql obfuscator.
+ # The block will be called with a single String SQL statement to obfuscate.
+ # The method must return the obfuscated String SQL.
+ # If chaining of obfuscators is required, use type = :before or :after
+ #
+ # type = :before, :replace, :after
+ #
+ # example:
+ # NewRelic::Agent.set_sql_obfuscator(:replace) do |sql|
+ # my_obfuscator(sql)
+ # end
+ #
+ def set_sql_obfuscator(type = :replace, &block)
+ agent.set_sql_obfuscator type, &block
+ end
+
+
+ # This method sets the state of sql recording in the transaction
+ # sampler feature. Within the given block, no sql will be recorded
+ #
+ # usage:
+ #
+ # NewRelic::Agent.disable_sql_recording do
+ # ...
+ # end
+ #
+ def disable_sql_recording
+ state = agent.set_record_sql(false)
+ begin
+ yield
+ ensure
+ agent.set_record_sql(state)
+ end
+ end
+
+
+ # Add parameters to the current transaction trace
+ #
+ def add_custom_parameters(params)
+ agent.add_custom_parameters(params)
+ end
+
+ alias add_request_parameters add_custom_parameters
+
+ # This method disables the recording of transaction traces in the given
+ # block.
+ def disable_transaction_tracing
+ state = agent.set_record_tt(false)
+ begin
+ yield
+ ensure
+ agent.set_record_tt(state)
+ end
+ end
+
+ # This method allows a filter to be applied to errors that RPM will track.
+ # The block should return the exception to track (which could be different from
+ # the original exception) or nil to ignore this exception
+ #
+ def ignore_error_filter(&block)
+ agent.error_collector.ignore_error_filter &block
+ end
+
+ end
+
+
+
+ # Implementation default for the NewRelic Agent
+ class Agent
+ # Specifies the version of the agent's communication protocol
+ # with the NewRelic hosted site.
+ #
+ # VERSION HISTORY
+ # 1: Private Beta, Jan 10, 2008. Serialized Marshalled Objects. Unsupported after 5/29/2008.
+ # 2: Private Beta, March 15, 2008. Compressed Serialzed Marshalled Objects (15-20x smaller)
+ # 3: June 19, 2008. Added transaction sampler capability with obfuscation
+ # 4: July 15, 2008. Added error capture
+ # 5: Sept 24, 2008. Optimized content coding
+ PROTOCOL_VERSION = 5
+
+ include Singleton
+
+ # Config object
+ attr_accessor :config
+ attr_reader :obfuscator
+ attr_reader :stats_engine
+ attr_reader :transaction_sampler
+ attr_reader :error_collector
+ attr_reader :worker_loop
+ attr_reader :license_key
+ attr_reader :remote_host
+ attr_reader :remote_port
+ attr_reader :record_sql
+ attr_reader :identifier
+
+ # This method is deprecated. Use start.
+ def manual_start(environment, identifier)
+ start(environment, identifier, true)
+ end
+
+ # Start up the agent, which will connect to the newrelic server and start
+ # reporting performance information. Typically this is done from the
+ # environment configuration file.
+ # environment identifies the host environment, like mongrel, thin, or take.
+ # identifier is an identifier which uniquely identifies the process hosting
+ # the agent. It should be ideally something like a server port, like 3000,
+ # a handler thread name, or a script name. It should not be a PID because
+ # that will change
+ # from invocation to invocation. For something like rake, you could use
+ # the task name.
+ # Return false if the agent was not started
+ def start(environment, identifier, force=false)
+
+ if @started
+ log! "Agent Started Already!"
+ raise "Duplicate attempt to start the NewRelic agent"
+ end
+ @environment = environment
+ @identifier = identifier && identifier.to_s
+ if @identifier
+ start_reporting(force)
+ return true
+ else
+ return false
+ end
+ end
+
+ # this method makes sure that the agent is running. it's important
+ # for passenger where processes are forked and the agent is dormant
+ #
+ def ensure_started
+ return unless @prod_mode_enabled && !@invalid_license
+ if @worker_pid != $$
+ launch_worker_thread
+ @stats_engine.spawn_sampler_thread
+ end
+ end
+
+ def start_reporting(force_enable=false)
+ @local_host = determine_host
+
+ setup_log
+
+ if @environment == :passenger
+ log.warn "Phusion Passenger has been detected. Some RPM memory statistics may have inaccuracies due to short process lifespans"
+ end
+
+ @worker_loop = WorkerLoop.new(log)
+ @started = true
+
+ @license_key = config.fetch('license_key', nil)
+
+ error_collector_config = config.fetch('error_collector', {})
+
+ @error_collector.enabled = error_collector_config.fetch('enabled', true)
+ @error_collector.capture_source = error_collector_config.fetch('capture_source', true)
+
+ log.info "Error collector is enabled in agent config" if @error_collector.enabled
+
+ ignore_errors = error_collector_config.fetch('ignore_errors', "")
+ ignore_errors = ignore_errors.split(",")
+ ignore_errors.each { |error| error.strip! }
+
+ @error_collector.ignore(ignore_errors)
+
+
+ @capture_params = config.fetch('capture_params', false)
+
+ sampler_config = config.fetch('transaction_tracer', {})
+
+ @use_transaction_sampler = sampler_config.fetch('enabled', false)
+ @record_sql = (sampler_config.fetch('record_sql', 'obfuscated') || 'off').intern
+ @slowest_transaction_threshold = sampler_config.fetch('transaction_threshold', '2.0').to_f
+ @explain_threshold = sampler_config.fetch('explain_threshold', '0.5').to_f
+ @explain_enabled = sampler_config.fetch('explain_enabled', true)
+ @stack_trace_threshold = sampler_config.fetch('stack_trace_threshold', '0.500').to_f
+
+ log.info "Transaction tracing is enabled in agent config" if @use_transaction_sampler
+ log.warn "Agent is configured to send raw SQL to RPM service" if @record_sql == :raw
+
+ @use_ssl = config.fetch('ssl', false)
+ default_port = @use_ssl ? 443 : 80
+
+ @remote_host = config.fetch('host', 'collector.newrelic.com')
+ @remote_port = config.fetch('port', default_port)
+
+ @proxy_host = config.fetch('proxy_host', nil)
+ @proxy_port = config.fetch('proxy_port', nil)
+ @proxy_user = config.fetch('proxy_user', nil)
+ @proxy_pass = config.fetch('proxy_pass', nil)
+
+ @prod_mode_enabled = force_enable || config['enabled']
+
+ # Initialize transaction sampler
+ TransactionSampler.capture_params = @capture_params
+ @transaction_sampler.stack_trace_threshold = @stack_trace_threshold
+ @error_collector.capture_params = @capture_params
+
+
+ # make sure the license key exists and is likely to be really a license key
+ # by checking it's string length (license keys are 40 character strings.)
+ if @prod_mode_enabled && (!@license_key || @license_key.length != 40)
+ log! "No license key found. Please insert your license key into agent/newrelic.yml"
+ return
+ end
+
+ instrument_app
+
+ if @prod_mode_enabled
+ load_samplers
+ launch_worker_thread
+ # When the VM shuts down, attempt to send a message to the server that
+ # this agent run is stopping, assuming it has successfully connected
+ at_exit { shutdown }
+ end
+ end
+
+ # Attempt a graceful shutdown of the agent.
+ def shutdown
+ return if !@started || !@worker_loop
+ @worker_loop.stop
+
+ log.debug "Starting Agent shutdown"
+
+ # if litespeed, then ignore all future SIGUSR1 - it's litespeed trying to shut us down
+
+ if @environment == :litespeed
+ Signal.trap("SIGUSR1", "IGNORE")
+ Signal.trap("SIGTERM", "IGNORE")
+ end
+
+ begin
+ graceful_disconnect
+ rescue => e
+ log.error e
+ log.error e.backtrace.join("\n")
+ end
+ @started = nil
+ end
+
+ def start_transaction
+ Thread::current[:custom_params] = nil
+ @stats_engine.start_transaction
+ end
+
+ def end_transaction
+ Thread::current[:custom_params] = nil
+ @stats_engine.end_transaction
+ end
+
+ def set_record_sql(should_record)
+ prev = Thread::current[:record_sql]
+ Thread::current[:record_sql] = should_record
+
+ prev || true
+ end
+
+ def set_record_tt(should_record)
+ prev = Thread::current[:record_tt]
+ Thread::current[:record_tt] = should_record
+
+ prev || true
+ end
+
+ def add_custom_parameters(params)
+ p = Thread::current[:custom_params] || (Thread::current[:custom_params] = {})
+
+ p.merge!(params)
+ end
+
+ def custom_params
+ Thread::current[:custom_params] || {}
+ end
+
+ def set_sql_obfuscator(type, &block)
+ if type == :before
+ @obfuscator = ChainedCall.new(block, @obfuscator)
+ elsif type == :after
+ @obfuscator = ChainedCall.new(@obfuscator, block)
+ elsif type == :replace
+ @obfuscator = block
+ else
+ fail "unknown sql_obfuscator type #{type}"
+ end
+ end
+
+ def instrument_app
+ return if @instrumented
+
+ @instrumented = true
+
+ # Instrumentation for the key code points inside rails for monitoring by NewRelic.
+ # note this file is loaded only if the newrelic agent is enabled (through config/newrelic.yml)
+ instrumentation_path = File.join(File.dirname(__FILE__), 'instrumentation')
+ instrumentation_files = [ ] <<
+ File.join(instrumentation_path, '*.rb') <<
+ File.join(instrumentation_path, config.app.to_s, '*.rb')
+
+ Dir.glob(instrumentation_files) do |file|
+ begin
+ require file
+ log.debug "Processed instrumentation file '#{file.split('/').last}'"
+ rescue => e
+ log.error "Error loading instrumentation file '#{file}': #{e}"
+ log.debug e.backtrace.join("\n")
+ end
+ end
+ end
+
+ def log
+ setup_log unless @log
+ @log
+ end
+
+ private
+
+ def initialize
+ @connected = false
+ @launch_time = Time.now
+
+ @metric_ids = {}
+ @environment = :unknown
+
+ @config = NewRelic::Config.instance
+
+ @stats_engine = NewRelic::Agent::StatsEngine.new
+ @transaction_sampler = NewRelic::Agent::TransactionSampler.new(self)
+ @error_collector = NewRelic::Agent::ErrorCollector.new(self)
+
+ @request_timeout = 15 * 60
+
+ @invalid_license = false
+
+ @last_harvest_time = Time.now
+
+ @worker_pid = 0
+ end
+
+ def setup_log
+ @log = config.setup_log(identifier)
+ log.info "Runtime environment: #{@environment.to_s}"
+ end
+
+
+ def launch_worker_thread
+ if (@environment == :passenger && $0 =~ /ApplicationSpawner/)
+ log.info "Process is passenger spawner - don't connect to RPM service"
+ return
+ end
+
+ @worker_pid = $$
+
+ @worker_thread = Thread.new do
+ begin
+ run_worker_loop
+ rescue StandardError => e
+ log! e
+ log! e.backtrace().join("\n")
+ end
+ end
+ end
+ @@first_try=nil
+ # Connect to the server, and run the worker loop forever
+ def run_worker_loop
+ until @connected or !connect; end
+ # We may not be connected now but keep going for dev mode
+
+ if @connected
+ # determine the reporting period (server based)
+ # note if the agent attempts to report more frequently than the specified
+ # report data, then it will be ignored.
+ report_period = invoke_remote :get_data_report_period, @agent_id
+
+ if @@first_try
+ log! "Here twice: #{caller.join("\n")}\n\nFirst: #{@@first_try}\n"
+ else
+ @@first_try = caller.join("\n")
+ end
+
+ log! "Reporting performance data every #{report_period} seconds"
+ @worker_loop.add_task(report_period) do
+ harvest_and_send_timeslice_data
+ end
+
+ if @should_send_samples && @use_transaction_sampler
+ @worker_loop.add_task(report_period) do
+ harvest_and_send_slowest_sample
+ end
+ elsif !config.developer_mode?
+ # We still need the sampler for dev mode.
+ @transaction_sampler.disable
+ end
+
+ if @should_send_errors && @error_collector.enabled
+ @worker_loop.add_task(report_period) do
+ harvest_and_send_errors
+ end
+ end
+ end
+ @worker_loop.run if @connected || config.developer_mode?
+ end
+
+ # Connect to the server and validate the license.
+ # If successful, @connected has true when finished.
+ # If not successful, you can keep calling this.
+ # Return false if we could not establish a connection with the
+ # server and we should not retry, such as if there's
+ # a bad license key.
+ def connect
+ @connect_retry_period ||= 5
+ @connect_attempts ||= 0
+
+ # wait a few seconds for the web server to boot
+ sleep @connect_retry_period.to_i
+ @agent_id = invoke_remote :launch, @local_host,
+ @identifier, determine_home_directory, $$, @launch_time.to_f, NewRelic::VERSION::STRING, config.gather_info
+
+ log! "Connected to NewRelic Service at #{@remote_host}:#{@remote_port}."
+ log.debug "Agent ID = #{@agent_id}."
+
+ # Ask the server for permission to send transaction samples. determined by subscription license.
+ @should_send_samples = invoke_remote :should_collect_samples, @agent_id
+
+ # Ask for mermission to collect error data
+ @should_send_errors = invoke_remote :should_collect_errors, @agent_id
+
+ log.info "Transaction traces will be sent to the RPM service" if @use_transaction_sampler && @should_send_samples
+ log.info "Errors will be sent to the RPM service" if @error_collector.enabled && @should_send_errors
+
+ @connected = true
+ return true
+
+ rescue LicenseException => e
+ log! e.message, :error
+ log! "Visit NewRelic.com to obtain a valid license key, or to upgrade your account."
+ @invalid_license = true
+ return false
+
+ rescue Timeout::Error, StandardError => e
+ log.error "Error attempting to connect to New Relic RPM Service at #{@remote_host}:#{@remote_port}"
+ log.error e.message
+ log.debug e.backtrace.join("\n")
+
+ # retry logic
+ @connect_attempts += 1
+ if @connect_attempts > 20
+ @connect_retry_period, period_msg = 10.minutes, "10 minutes"
+ elsif @connect_attempts > 10
+ @connect_retry_period, period_msg = 1.minutes, "1 minute"
+ elsif @connect_attempts > 5
+ @connect_retry_period, period_msg = 30, nil
+ else
+ @connect_retry_period, period_msg = 5, nil
+ end
+
+ log.info "Will re-attempt in #{period_msg}" if period_msg
+ return true
+ end
+
+ def load_samplers
+ sampler_files = File.join(File.dirname(__FILE__), 'samplers', '*.rb')
+ Dir.glob(sampler_files) do |file|
+ begin
+ require file
+ rescue => e
+ log.error "Error loading sampler '#{file}': #{e}"
+ end
+ end
+ end
+
+ def determine_host
+ Socket.gethostname
+ end
+
+ def determine_home_directory
+ config.root
+ end
+
+ def harvest_and_send_timeslice_data
+
+ NewRelic::BusyCalculator.harvest_busy
+
+ now = Time.now
+
+ @harvest_thread ||= Thread.current
+
+ log! "ERROR - two harvest threads are running" if @harvest_thread != Thread.current
+ log! "Agent sending data too frequently - #{now - @last_harvest_time} seconds" if (now.to_f - @last_harvest_time.to_f) < 45
+
+ @unsent_timeslice_data ||= {}
+ @unsent_timeslice_data = @stats_engine.harvest_timeslice_data(@unsent_timeslice_data, @metric_ids)
+
+ begin
+ metric_ids = invoke_remote(:metric_data, @agent_id,
+ @last_harvest_time.to_f,
+ now.to_f,
+ @unsent_timeslice_data.values)
+
+ rescue Timeout::Error
+ # assume that the data was received. chances are that it was
+ metric_ids = nil
+ end
+
+
+ @metric_ids.merge! metric_ids if metric_ids
+
+ log.debug "#{now}: sent #{@unsent_timeslice_data.length} timeslices (#{@agent_id}) in #{Time.now - now} seconds"
+
+ # if we successfully invoked this web service, then clear the unsent message cache.
+ @unsent_timeslice_data = {}
+ @last_harvest_time = now
+
+ # handle_messages
+
+ # note - exceptions are logged in invoke_remote. If an exception is encountered here,
+ # then the metric data is downsampled for another timeslices
+ rescue
+ end
+
+ def harvest_and_send_slowest_sample
+ @slowest_sample = @transaction_sampler.harvest_slowest_sample(@slowest_sample)
+
+ if @slowest_sample && @slowest_sample.duration > @slowest_transaction_threshold
+ now = Time.now
+ log.debug "Sending slowest sample: #{@slowest_sample.params[:path]}, #{@slowest_sample.duration.round_to(2)}s (explain=#{@explain_enabled})" if @slowest_sample
+
+ # take the slowest sample, and prepare it for sending across the wire. This includes
+ # gathering SQL explanations, stripping out stack traces, and normalizing SQL.
+ # note that we explain only the sql statements whose segments' execution times exceed
+ # our threshold (to avoid unnecessary overhead of running explains on fast queries.)
+ sample = @slowest_sample.prepare_to_send(:explain_sql => @explain_threshold, :record_sql => @record_sql, :keep_backtraces => true, :explain_enabled => @explain_enabled)
+
+ invoke_remote :transaction_sample_data, @agent_id, sample
+
+ log.debug "#{now}: sent slowest sample (#{@agent_id}) in #{Time.now - now} seconds"
+ end
+
+ # if we succeed sending this sample, then we don't need to keep the slowest sample
+ # around - it has been sent already and we can collect the next one
+ @slowest_sample = nil
+
+ # note - exceptions are logged in invoke_remote. If an exception is encountered here,
+ # then the slowest sample of is determined of the entire period since the last
+ # reported sample.
+ rescue
+ end
+
+ def harvest_and_send_errors
+ @unsent_errors = @error_collector.harvest_errors(@unsent_errors)
+ if @unsent_errors && @unsent_errors.length > 0
+ log.debug "Sending #{@unsent_errors.length} errors"
+
+ invoke_remote :error_data, @agent_id, @unsent_errors
+
+ # if the remote invocation fails, then we never clear @unsent_errors,
+ # and therefore we can re-attempt to send on the next heartbeat. Note
+ # the error collector maxes out at 20 instances to prevent leakage
+ @unsent_errors = []
+ end
+ rescue
+ end
+
+=begin
+ def handle_messages(messages)
+ messages.each do |message|
+ begin
+ message = Marshal.load(message)
+ message.execute(self)
+ log.debug("Received Message: #{message.to_yaml}")
+ rescue => e
+ log.error "Error handling message: #{e}"
+ log.debug e.backtrace.join("\n")
+ end
+ end
+ end
+=end
+
+ # send a message via post
+ # As of Version 2, the agent-server protocol is:
+ # params[:method] => method name(string)
+ # params[:license_key] => license key(string)
+ # params[:version] => protocol version(integer, 2 or higher)
+ def invoke_remote(method, *args)
+ # we currently optimize for CPU here since we get roughly a 10x reduction in
+ # message size with this, and CPU overhead is at a premium. If we wanted
+ # to go for higher compression instead, we could use Zlib::BEST_COMPRESSION and
+ # pay a little more CPU.
+ post_data = Zlib::Deflate.deflate(Marshal.dump(args), Zlib::BEST_SPEED)
+
+ # Proxy returns regular HTTP if @proxy_host is nil (the default)
+ http = Net::HTTP::Proxy(@proxy_host, @proxy_port, @proxy_user, @proxy_pass).new(@remote_host, @remote_port.to_i)
+ if @use_ssl
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+
+ http.read_timeout = @request_timeout
+
+ # params = {:method => method, :license_key => license_key, :protocol_version => PROTOCOL_VERSION }
+ # uri = "/agent_listener/invoke_raw_method?#{params.to_query}"
+ uri = "/agent_listener/invoke_raw_method?method=#{method}&license_key=#{license_key}&protocol_version=#{PROTOCOL_VERSION}"
+ uri += "&run_id=#{@agent_id}" if @agent_id
+
+ request = Net::HTTP::Post.new(uri, 'ACCEPT-ENCODING' => 'gzip')
+ request.content_type = "application/octet-stream"
+ request.body = post_data
+
+ log.debug "#{uri}"
+
+ response = http.request(request)
+
+ if response.is_a? Net::HTTPSuccess
+ body = nil
+ if response['content-encoding'] == 'gzip'
+ log.debug "Decompressing return value"
+ i = Zlib::GzipReader.new(StringIO.new(response.body))
+ body = i.read
+ else
+ log.debug "Uncompressed content returned"
+ body = response.body
+ end
+ return_value = Marshal.load(body)
+ if return_value.is_a? Exception
+ raise return_value
+ else
+ return return_value
+ end
+ else
+ if response.code == "405" || response.code == "503"
+ raise IgnoreSilentlyException.new
+ else
+ raise "#{response.code}: #{response.message}"
+ end
+ end
+ rescue ForceDisconnectException => e
+ log! "RPM forced this agent to disconnect", :error
+ log! e.message, :error
+ log! "Restart this process to resume RPM's agent communication with NewRelic.com"
+ # when a disconnect is requested, stop the current thread, which is the worker thread that
+ # gathers data and talks to the server.
+ @connected = false
+ Thread.exit
+
+ rescue IgnoreSilentlyException => e
+ raise e
+
+ rescue Exception => e
+ log.debug("Error communicating with RPM Service at #{@remote_host}:#{remote_port}: #{e} (#{e.class})")
+ #log.debug(e.backtrace.join("\n"))
+ raise e
+ end
+
+ # send the given message to STDERR as well as the agent log, so that it shows
+ # up in the console. This should be used for important informational messages at boot
+ def log!(msg, level = :info)
+ # only log to stderr when we are running as a mongrel process, so it doesn't
+ # muck with daemons and the like.
+ config.log!(msg, level)
+ end
+
+ def graceful_disconnect
+ if @connected && !(remote_host == "localhost" && @identifier == '3000')
+ begin
+ log.debug "Sending graceful shutdown message to #{remote_host}:#{remote_port}"
+
+ @request_timeout = 5
+
+# harvest_and_send_timeslice_data true
+#
+# if @should_send_samples && @use_transaction_sampler
+# harvest_and_send_slowest_sample
+# end
+#
+# if @should_send_errors
+# harvest_and_send_errors
+# end
+
+ if @environment != :litespeed
+ log.debug "Sending RPM service agent run shutdown message"
+ invoke_remote :shutdown, @agent_id, Time.now.to_f
+ end
+
+ log.debug "Graceful shutdown complete"
+
+ rescue Timeout::Error, StandardError => e
+ end
+ else
+ log.debug "Bypassing graceful shutdown - agent in development mode"
+ end
+ end
+ end
+
+end
+
+class ChainedCall
+
+ def initialize(call1, call2)
+ @call1 = call1
+ @call2 = call2
+ end
+
+ def call(sql)
+ sql = @call1.call(sql)
+ @call2.call(sql)
+ end
+end
76 lib/new_relic/agent/collection_helper.rb
@@ -0,0 +1,76 @@
+module NewRelic::Agent::CollectionHelper
+ # Transform parameter hash into a hash whose values are strictly
+ # strings
+ def normalize_params(params)
+ case params
+ when Symbol, FalseClass, TrueClass, nil:
+ params
+ when Numeric
+ truncate(params.to_s, 256)
+ when String
+ truncate(params, 256)
+ when Hash:
+ new_params = {}
+ params.each do | key, value |
+ new_params[truncate(normalize_params(key),32)] = normalize_params(value)
+ end
+ new_params
+ when Enumerable:
+ params.first(20).collect { | v | normalize_params(v)}
+ else
+ truncate(flatten(params), 256)
+ end
+ end
+
+
+ def clean_exception(exception)
+ exception = exception.original_exception if exception.respond_to? 'original_exception'
+
+ if exception.backtrace
+ clean_backtrace = exception.backtrace
+
+ # strip newrelic from the trace
+ clean_backtrace = clean_backtrace.reject {|line| line =~ /vendor\/plugins\/newrelic_rpm/ }
+
+ # rename methods back to their original state
+ clean_backtrace.collect {|line| line.gsub "_without_(newrelic|trace)", ""}
+ else
+ nil
+ end
+ end
+
+ private
+
+ # Convert any kind of object to a descriptive string
+ # Only call this on unknown objects. Otherwise call to_s.
+ def flatten(object)
+ s =
+ if object.respond_to? :inspect
+ object.inspect
+ elsif object.respond_to? :to_s
+ object.to_s
+ elsif object.nil?
+ "nil"
+ else
+ "#<#{object.class.to_s}>"
+ end
+
+ if !(s.instance_of? String)
+ s = "#<#{object.class.to_s}>"
+ end
+
+ s
+ end
+
+ def truncate(string, len)
+ if string.instance_of? Symbol
+ string
+ elsif string.nil?
+ ""
+ elsif string.instance_of? String
+ string.to_s.gsub(/^(.{#{len}})(.*)/) {$2.blank? ? $1 : $1 + "..."}
+ else
+ truncate(flatten(string), len)
+ end
+ end
+end
98 lib/new_relic/agent/error_collector.rb
@@ -0,0 +1,98 @@
+#require 'new_relic/agent/synchronize'
+#require 'noticed_error'
+#require 'new_relic/agent/collection_helper'
+require 'logger'
+
+module NewRelic::Agent
+ class ErrorCollector
+ include Synchronize
+ include CollectionHelper
+
+ MAX_ERROR_QUEUE_LENGTH = 20 unless defined? MAX_ERROR_QUEUE_LENGTH
+
+ attr_accessor :capture_params
+ attr_accessor :capture_source
+ attr_accessor :enabled
+
+ def initialize(agent = nil)
+ @agent = agent
+ @errors = []
+ @ignore = {}
+ @ignore_filter = nil
+ @capture_params = true
+ @capture_source = false
+ @enabled = true
+ end
+
+
+ def ignore_error_filter(&block)
+ @ignore_filter = block
+ end
+
+
+ # errors is an array of String exceptions
+ #
+ def ignore(errors)
+ errors.each { |error| @ignore[error] = true; log.debug("Ignoring error: '#{error}'") }
+ end
+
+
+ def notice_error(path, request_uri, params, exception)
+
+ return unless @enabled
+ return if @ignore[exception.class.name]
+
+ if @ignore_filter
+ exception = @ignore_filter.call(exception)
+
+ return if exception.nil?
+ end
+
+ @@error_stat ||= NewRelic::Agent.get_stats("Errors/all")
+
+ @@error_stat.increment_count
+
+ data = {}
+
+ data[:request_params] = normalize_params(params) if @capture_params
+ data[:custom_params] = normalize_params(@agent.custom_params) if @agent
+
+ data[:request_uri] = request_uri
+
+ data[:rails_root] = RAILS_ROOT if defined? RAILS_ROOT
+
+ data[:file_name] = exception.file_name if exception.respond_to?('file_name')
+ data[:line_number] = exception.line_number if exception.respond_to?('line_number')
+
+ if @capture_source && exception.respond_to?('source_extract')
+ data[:source] = exception.source_extract
+ end
+
+ data[:stack_trace] = clean_exception(exception)
+ noticed_error = NewRelic::NoticedError.new(path, data, exception)
+
+ synchronize do
+ if @errors.length >= MAX_ERROR_QUEUE_LENGTH
+ log.info("Not reporting error (queue exceeded maximum length): #{exception.message}")
+ else
+ @errors << noticed_error
+ end
+ end
+ end
+
+ def harvest_errors(unsent_errors)
+ synchronize do
+ errors = (unsent_errors || []) + @errors
+ @errors = []
+ return errors
+ end
+ end
+
+ private
+ def log
+ return @agent.log if @agent && @agent.log
+
+ @backup_log ||= Logger.new(STDERR)
+ end
+ end
+end
104 lib/new_relic/agent/instrumentation/action_controller.rb
@@ -0,0 +1,104 @@
+require 'set'
+
+# NewRelic instrumentation for controllers
+if defined? ActionController
+
+
+module ActionController
+ class Base
+ # Have NewRelic ignore actions in this controller. Specify the actions as hash options
+ # using :except and :only. If no actions are specified, all actions are ignored.
+ def self.newrelic_ignore(specifiers={})
+ if specifiers.empty?
+ write_inheritable_attribute('do_not_trace', true)
+ elsif ! (Hash === specifiers)
+ logger.error "newrelic_ignore takes an optional hash with :only and :except lists of actions (illegal argument type '#{specifiers.class}')"
+ else
+ write_inheritable_attribute('do_not_trace', specifiers)
+ end
+ end
+
+ def perform_action_with_newrelic_trace
+ agent = NewRelic::Agent.instance
+ ignore_actions = self.class.read_inheritable_attribute('do_not_trace')
+ # Skip instrumentation based on the value of 'do_not_trace'
+ if ignore_actions
+ should_skip = false
+
+ if Hash === ignore_actions
+ only_actions = Array(ignore_actions[:only])
+ except_actions = Array(ignore_actions[:except])
+ should_skip = true if only_actions.include? action_name.to_sym
+ should_skip = true if except_actions.any? && !except_actions.include?(action_name.to_sym)
+ else
+ should_skip = true
+ end
+
+ if should_skip
+ Thread.current[:controller_ignored] = true
+ return perform_action_without_newrelic_trace
+ end
+ end
+
+ agent.ensure_started
+
+ # generate metrics for all all controllers (no scope)
+ self.class.trace_method_execution_no_scope "Controller" do
+ # generate metrics for this specific action
+ path = _determine_metric_path
+
+ agent.stats_engine.transaction_name ||= "Controller/#{path}" if agent.stats_engine
+
+ self.class.trace_method_execution "Controller/#{path}", true, true do
+ # send request and parameter info to the transaction sampler
+
+ local_params = (respond_to? :filter_parameters) ? filter_parameters(params) : params
+
+ agent.transaction_sampler.notice_transaction(path, request, local_params)
+
+ t = Process.times.utime + Process.times.stime
+
+ begin
+ # run the action
+ perform_action_without_newrelic_trace
+ ensure
+ agent.transaction_sampler.notice_transaction_cpu_time((Process.times.utime + Process.times.stime) - t)
+ end
+ end
+ end
+
+ ensure
+ # clear out the name of the traced transaction under all circumstances
+ agent.stats_engine.transaction_name = nil
+ end
+
+ # Compare with #alias_method_chain, which is not available in
+ # Rails 1.1:
+ alias_method :perform_action_without_newrelic_trace, :perform_action
+ alias_method :perform_action, :perform_action_with_newrelic_trace
+ private :perform_action
+
+ add_method_tracer :render, 'View/#{_determine_metric_path}/Rendering'
+
+ # ActionWebService is now an optional part of Rails as of 2.0
+ if method_defined? :perform_invocation
+ add_method_tracer :perform_invocation, 'WebService/#{controller_name}/#{args.first}'
+ end
+
+ private
+ # determine the path that is used in the metric name for
+ # the called controller action
+ def _determine_metric_path
+ if self.class.action_methods.include?(action_name)
+ "#{self.class.controller_path}/#{action_name}"
+ else
+ "#{self.class.controller_path}/(other)"
+ end
+ end
+ end
+
+end
+
+end
+
+
23 lib/new_relic/agent/instrumentation/action_web_service.rb
@@ -0,0 +1,23 @@
+# NewRelic Agent instrumentation for WebServices
+
+# Note Action Web Service is removed from default package in rails 2.0
+if defined? ActionWebService
+
+# instrumentation for Web Service martialing - XML RPC
+class ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
+ add_method_tracer :decode_request, "WebService/Xml Rpc/XML Decode"
+ add_method_tracer :encode_request, "WebService/Xml Rpc/XML Encode"
+ add_method_tracer :decode_response, "WebService/Xml Rpc/XML Decode"
+ add_method_tracer :encode_response, "WebService/Xml Rpc/XML Encode"
+end
+
+# instrumentation for Web Service martialing - Soap
+class ActionWebService::Protocol::Soap::SoapProtocol
+ add_method_tracer :decode_request, "WebService/Soap/XML Decode"
+ add_method_tracer :encode_request, "WebService/Soap/XML Encode"
+ add_method_tracer :decode_response, "WebService/Soap/XML Decode"
+ add_method_tracer :encode_response, "WebService/Soap/XML Encode"
+end
+
+
+end
97 lib/new_relic/agent/instrumentation/active_record.rb
@@ -0,0 +1,97 @@
+
+# NewRelic instrumentation for ActiveRecord
+if defined? ActiveRecord
+
+module ActiveRecord
+ class Base
+ class << self
+ [:find, :count].each do |find_method|
+ add_method_tracer find_method, 'ActiveRecord/#{self.name}/find'
+ add_method_tracer find_method, 'ActiveRecord/find', :push_scope => false
+ add_method_tracer find_method, 'ActiveRecord/all', :push_scope => false
+ end
+
+ end
+ [:save, :save!].each do |save_method|
+ add_method_tracer save_method, 'ActiveRecord/#{self.class.name}/save'
+ add_method_tracer save_method, 'ActiveRecord/save', :push_scope => false
+ add_method_tracer save_method, 'ActiveRecord/all', :push_scope => false
+ end
+
+ add_method_tracer :destroy, 'ActiveRecord/#{self.class.name}/destroy'
+ add_method_tracer :destroy, 'ActiveRecord/destroy', :push_scope => false
+ add_method_tracer :destroy, 'ActiveRecord/all', :push_scope => false
+ end
+
+ # instrumentation to catch logged SQL statements in sampled transactions
+ module ConnectionAdapters
+ class AbstractAdapter
+
+ @@my_sql_defined = defined? ActiveRecord::ConnectionAdapters::MysqlAdapter
+ @@postgres_defined = defined? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+
+ def log_with_newrelic_instrumentation(sql, name, &block)
+ # if we aren't in a blamed context, then add one so that we can see that
+ # controllers are calling SQL directly
+ # we check scope_depth vs 2 since the controller is 1, and the
+ #
+ if NewRelic::Agent.instance.transaction_sampler.scope_depth < 2
+ self.class.trace_method_execution "Database/DirectSQL", true, true do
+ log_with_capture_sql(sql, name, &block)
+ end
+ else
+ log_with_capture_sql(sql, name, &block)
+ end
+ end
+
+ def log_with_capture_sql(sql, name, &block)
+ if @@my_sql_defined && self.is_a?(ActiveRecord::ConnectionAdapters::MysqlAdapter)
+ config = @config
+ elsif @@postgres_defined && self.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
+ config = @config
+ else
+ config = nil
+ end
+
+ t0 = Time.now
+ result = log_without_newrelic_instrumentation(sql, name, &block)
+
+ NewRelic::Agent.instance.transaction_sampler.notice_sql(sql, config, Time.now - t0)
+
+ result
+ end
+
+ # Compare with #alias_method_chain, which is not available in
+ # Rails 1.1:
+ alias_method :log_without_newrelic_instrumentation, :log
+ alias_method :log, :log_with_newrelic_instrumentation
+ protected :log
+
+ add_method_tracer :log, 'Database/#{adapter_name}/#{args[1]}', :metric => false
+ add_method_tracer :log, 'Database/all', :push_scope => false
+
+ end
+ end
+
+ # instrumentation for associations
+ module Associations
+ class AssociationCollection
+ add_method_tracer :delete, 'ActiveRecord/#{@owner.class.name}/association delete'
+ end
+
+ def HasAndBelongsToManyAssociation
+ add_method_tracer :find, 'ActiveRecord/#{@owner.class.name}/association find'
+ add_method_tracer :create_record, 'ActiveRecord/#{@owner.class.name}/association create'
+ add_method_tracer :insert_record, 'ActiveRecord/#{@owner.class.name}/association insert'
+ end
+
+ class HasManyAssociation
+ # add_method_tracer :find, 'ActiveRecord/#{@owner.class.name}/association find'
+ # add_method_tracer :insert_record, 'ActiveRecord/#{@owner.class.name}/association insert'
+ # add_method_tracer :create_record, 'ActiveRecord/#{@owner.class.name}/association create'
+ end
+ end
+
+end
+
+end
217 lib/new_relic/agent/instrumentation/dispatcher.rb
@@ -0,0 +1,217 @@
+require 'dispatcher'
+
+
+
+# We have to patch the mongrel dispatcher live since the classes
+# aren't defined when our instrumentation loads
+module NewRelic
+ class MutexWrapper
+
+ @@queue_length = 0
+
+ def MutexWrapper.queue_length
+ @@queue_length
+ end
+
+ def MutexWrapper.in_handler
+ Thread.critical = true
+ @@queue_length -= 1
+ Thread.critical = false
+ end
+
+ def initialize(mutex)
+ @mutex = mutex
+ end
+
+ def synchronize(&block)
+ Thread.critical = true
+ @@queue_length += 1
+ Thread.critical = false
+
+ Thread.current[:queue_start] = Time.now.to_f
+
+ @mutex.synchronize(&block)
+ rescue TimeoutError => e
+ MutexWrapper.in_handler
+ raise e
+ end
+ end
+
+ class BusyCalculator
+
+ # the fraction of the sample period that the dispatcher was busy
+ @@instance_busy = NewRelic::Agent.agent.stats_engine.get_stats('Instance/Busy')
+ @@harvest_start = Time.now.to_f
+ @@accumulator = 0
+ @@dispatcher_start = nil
+ def BusyCalculator.dispatcher_start(time)
+ Thread.critical = true
+ @@dispatcher_start = time
+ Thread.critical = false
+ end
+
+ def BusyCalculator.dispatcher_finish(time)
+ Thread.critical = true
+
+ @@accumulator += (time - @@dispatcher_start)
+ @@dispatcher_start = nil
+
+ Thread.critical = false
+ end
+
+ def BusyCalculator.add_busy(amount)
+ Thread.critical = true
+ @@accumulator += amount
+ Thread.critical = false
+ end
+
+ def BusyCalculator.harvest_busy
+ t0 = Time.now.to_f
+
+ Thread.critical = true
+
+ busy = @@accumulator
+ @@accumulator = 0
+
+ if @@dispatcher_start
+ busy += (t0 - @@dispatcher_start)
+ @@dispatcher_start = t0
+ end
+
+ Thread.critical = false
+
+ busy = 0.0 if busy < 0.0 # don't go below 0%
+
+ time_window = (t0 - @@harvest_start)
+ time_window = 1.0 if time_window == 0.0 # protect against divide by zero
+
+ busy = busy / time_window
+
+ busy = 1.0 if busy > 1.0 # cap at 100%
+
+ @@instance_busy.record_data_point busy
+
+ @@harvest_start = t0
+ end
+
+ end
+end
+
+
+module NewRelicDispatcherMixIn
+
+ @@mongrel = nil;
+ @@patch_guard = true
+
+ if defined? Mongrel
+ ObjectSpace.each_object(Mongrel::HttpServer) do |mongrel_instance|
+ @@mongrel = mongrel_instance
+ @@patch_guard = false
+ end
+ end
+
+ @@newrelic_agent = NewRelic::Agent.agent
+ @@newrelic_rails_dispatch_stat = @@newrelic_agent.stats_engine.get_stats 'Rails/HTTP Dispatch'
+ @@newrelic_mongrel_queue_stat = (@@mongrel) ? @@newrelic_agent.stats_engine.get_stats('WebFrontend/Mongrel/Average Queue Time'): nil
+ @@newrelic_mongrel_read_time = (@@mongrel) ? @@newrelic_agent.stats_engine.get_stats('WebFrontend/Mongrel/Average Read Time'): nil
+
+
+ def patch_guard
+ @@patch_guard = true
+
+ if defined? Mongrel::Rails::RailsHandler
+ handler = nil
+
+ ObjectSpace.each_object(Mongrel::Rails::RailsHandler) do |handler_instance|
+ # should only be one mongrel instance in the vm
+ if handler
+ agent.log.error("Discovered multiple Mongrel rails handler instances in one Ruby VM. "+
+ "This is unexpected and might affect the Accuracy of the Mongrel Request Queue metric.")
+ end
+
+ handler = handler_instance
+ end
+
+ if handler
+ def handler.new_relic_set_guard(guard)
+ @guard = guard
+ end
+
+ handler.new_relic_set_guard NewRelic::MutexWrapper.new(handler.guard)
+
+ NewRelic::Agent.instance.stats_engine.add_sampled_metric("Mongrel/Queue Length") do |stats|
+ stats.record_data_point NewRelic::MutexWrapper.queue_length
+ end
+ end
+ end
+ end
+
+
+ #
+ # Patch dispatch
+ def dispatch_newrelic(*args)
+ t0 = Time.now.to_f
+
+ if !@@patch_guard
+ patch_guard
+ return dispatch_without_newrelic(*args)
+ end
+
+ NewRelic::BusyCalculator.dispatcher_start t0
+
+ queue_start = Thread.current[:queue_start]
+
+ if queue_start
+ NewRelic::MutexWrapper.in_handler
+ read_start = Thread.current[:started_on]
+
+ @@newrelic_mongrel_queue_stat.trace_call(t0 - queue_start)
+
+ if read_start
+ read_time = queue_start - read_start.to_f
+ @@newrelic_mongrel_read_time.trace_call(read_time) if read_time > 0
+ NewRelic::BusyCalculator.add_busy(read_time)
+ end
+ end
+
+ @@newrelic_agent.start_transaction
+
+ Thread.current[:controller_ignored] = nil
+
+ begin
+ result = dispatch_without_newrelic(*args)
+ ensure
+ t1 = Time.now.to_f
+ @@newrelic_agent.end_transaction
+ @@newrelic_rails_dispatch_stat.trace_call(t1 - t0) if Thread.current[:controller_ignored].nil?
+ NewRelic::BusyCalculator.dispatcher_finish t1
+ end
+
+ result
+ end
+end
+
+
+
+# NewRelic RPM instrumentation for http request dispatching (Routes mapping)
+# Note, the dispatcher class from no module into into the ActionController modile
+# in rails 2.0. Thus we need to check for both
+if defined? ActionController::Dispatcher
+ class ActionController::Dispatcher
+ class << self
+ include NewRelicDispatcherMixIn
+
+ alias_method :dispatch_without_newrelic, :dispatch
+ alias_method :dispatch, :dispatch_newrelic
+ end
+ end
+elsif defined? Dispatcher
+ class Dispatcher
+ class << self
+ include NewRelicDispatcherMixIn
+
+ alias_method :dispatch_without_newrelic, :dispatch
+ alias_method :dispatch, :dispatch_newrelic
+ end
+ end
+end
32 lib/new_relic/agent/instrumentation/errors.rb
@@ -0,0 +1,32 @@
+
+
+# patch rescue_action and track how many are occuring and capture instances as well
+
+if defined? ActionController
+
+module ActionController
+ class Base
+
+ def newrelic_notice_error(exception)
+ local_params = (respond_to? :filter_parameters) ? filter_parameters(params) : params
+
+ NewRelic::Agent.agent.error_collector.notice_error(_determine_metric_path, (request) ? request.path : nil,
+ local_params, exception)
+ end
+
+
+ def rescue_action_with_newrelic_trace(exception)
+ newrelic_notice_error exception
+
+ rescue_action_without_newrelic_trace exception
+ end
+
+ # Compare with #alias_method_chain, which is not available in
+ # Rails 1.1:
+ alias_method :rescue_action_without_newrelic_trace, :rescue_action
+ alias_method :rescue_action, :rescue_action_with_newrelic_trace
+ protected :rescue_action
+ end
+end
+
+end
18 lib/new_relic/agent/instrumentation/memcache.rb
@@ -0,0 +1,18 @@
+
+if defined? MemCache
+
+# NOTE there are multiple implementations of the MemCache client in Ruby,
+# each with slightly different API's and semantics.
+# Currently we only cover memcache-client. Need to cover Ruby-MemCache.
+# See:
+# http://www.deveiate.org/code/Ruby-MemCache/ (Gem: Ruby-MemCache)
+# http://dev.robotcoop.com/Libraries/memcache-client/ (Gem: memcache-client)
+class MemCache
+
+ add_method_tracer :get, 'MemCache/read'
+ add_method_tracer :set, 'MemCache/write'
+
+ add_method_tracer :get_multi, 'MemCache/read'
+end
+
+end
14 lib/new_relic/agent/instrumentation/memcached.rb
@@ -0,0 +1,14 @@
+
+if defined? Memcached
+
+
+# Support for libmemcached through Evan Weaver's memcached wrapper
+# http://blog.evanweaver.com/files/doc/fauna/memcached/classes/Memcached.html
+
+class Memcached
+ add_method_tracer :get, 'Memcached/read'
+ add_method_tracer :set, 'Memcached/write'
+end
+
+
+end
10 lib/new_relic/agent/instrumentation/rails.rb
@@ -0,0 +1,10 @@
+# NewRelic Agent instrumentation for miscellaneous parts of the rails platform
+
+# instrumentation for dynamic application code loading (usually only happens a lot
+# in development environment)
+
+class ERB::Compiler
+ add_method_tracer :compile, 'View/.rhtml Processing'
+end
+
+
165 lib/new_relic/agent/method_tracer.rb
@@ -0,0 +1,165 @@
+require 'logger'
+
+class Module
+
+ # This is duplicated inline in add_method_tracer
+ def trace_method_execution_no_scope(metric_name)
+ t0 = Time.now.to_f
+ stats = @@newrelic_stats_engine.get_stats_no_scope metric_name
+
+ result = yield
+ duration = Time.now.to_f - t0 # for some reason this is 3 usec faster than Time - Time
+ stats.trace_call(duration, duration)
+ result
+ end
+
+
+ #
+ # it might be cleaner to have a hash for options, however that's going to be slower
+ # than direct parameters
+ #
+ def trace_method_execution(metric_name, produce_metric, deduct_call_time_from_parent)
+
+ t0 = Time.now.to_f
+ stats = nil
+
+ begin
+ expected_scope = @@newrelic_stats_engine.push_scope(metric_name, t0, deduct_call_time_from_parent)
+
+ stats = @@newrelic_stats_engine.get_stats metric_name, true if produce_metric
+ rescue => e
+ NewRelic::Config.instance.log.error("Caught exception in trace_method_execution header. Metric name = #{metric_name}, exception = #{e}")
+ NewRelic::Config.instance.log.error(e.backtrace.join("\n"))
+ end
+
+ begin
+ result = yield
+ ensure
+ t1 = Time.now.to_f
+ duration = t1 - t0
+
+ begin
+ if expected_scope
+ scope = @@newrelic_stats_engine.pop_scope expected_scope, duration, t1
+
+ exclusive = duration - scope.children_time
+ stats.trace_call duration, exclusive if stats
+ end
+ rescue => e
+ NewRelic::Config.instance.log.error("Caught exception in trace_method_execution footer. Metric name = #{metric_name}, exception = #{e}")
+ NewRelic::Config.instance.log.error(e.backtrace.join("\n"))
+ end
+
+ result
+ end
+ end
+
+ # Add a method tracer to the specified method.
+ # metric_name_code is ruby code that determines the name of the
+ # metric to be collected during tracing. As such, the code
+ # should be provided in 'single quote' strings rather than
+ # "double quote" strings, so that #{} evaluation happens
+ # at traced method execution time.
+ # Example: tracing a method :foo, where the metric name is
+ # the first argument converted to a string
+ # add_method_tracer :foo, '#{args.first.to_s}'
+ # statically defined metric names can be specified as regular strings
+ # push_scope specifies whether this method tracer should push
+ # the metric name onto the scope stack.
+ def add_method_tracer (method_name, metric_name_code, options = {})
+ return unless NewRelic::Agent.agent.config.tracers_enabled?
+
+ @@newrelic_stats_engine ||= NewRelic::Agent.agent.stats_engine
+
+
+ if !options.is_a?(Hash)
+ options = {:push_scope => options}
+ end
+
+ options[:push_scope] = true if options[:push_scope].nil?
+ options[:metric] = true if options[:metric].nil?
+ options[:deduct_call_time_from_parent] = false if options[:deduct_call_time_from_parent].nil? && !options[:metric]
+ options[:deduct_call_time_from_parent] = true if options[:deduct_call_time_from_parent].nil?
+ options[:code_header] ||= ""
+ options[:code_footer] ||= ""
+
+ klass = (self === Module) ? "self" : "self.class"
+
+ unless method_defined?(method_name) || private_method_defined?(method_name)
+ NewRelic::Config.instance.log.warn("Did not trace #{self}##{method_name} because that method does not exist")
+ return
+ end
+
+ traced_method_name = _traced_method_name(method_name, metric_name_code)
+ if method_defined? traced_method_name
+ NewRelic::Config.instance.log.warn("Attempt to trace a method twice with the same metric: Method = #{method_name}, Metric Name = #{metric_name_code}")
+ return
+ end
+
+ fail "Can't add a tracer where push_scope is false and metric is false" if options[:push_scope] == false && !options[:metric]
+
+ if options[:push_scope] == false
+ code = <<-CODE
+ def #{_traced_method_name(method_name, metric_name_code)}(*args, &block)
+ #{options[:code_header]}
+ @@newrelic_stats_engine ||= NewRelic::Agent.agent.stats_engine # wish I didn't have to duplicate this
+ t0 = Time.now.to_f
+ stats = @@newrelic_stats_engine.get_stats_no_scope "#{metric_name_code}"
+
+ result = #{_untraced_method_name(method_name, metric_name_code)}(*args, &block)
+ duration = Time.now.to_f - t0
+ stats.trace_call(duration, duration) # for some reason this is 3 usec faster than Time - Time
+ #{options[:code_footer]}
+ result
+ end
+ CODE
+ else
+ code = <<-CODE
+ def #{_traced_method_name(method_name, metric_name_code)}(*args, &block)
+ #{options[:code_header]}
+ result = #{klass}.trace_method_execution("#{metric_name_code}", #{options[:metric]}, #{options[:deduct_call_time_from_parent]}) do
+ #{_untraced_method_name(method_name, metric_name_code)}(*args, &block)
+ end
+ #{options[:code_footer]}
+ result
+ end
+ CODE
+ end
+
+ class_eval code, __FILE__, __LINE__
+
+ alias_method _untraced_method_name(method_name, metric_name_code), method_name
+ alias_method method_name, "#{_traced_method_name(method_name, metric_name_code)}"
+
+ NewRelic::Config.instance.log.debug("Traced method: class = #{self}, method = #{method_name}, "+
+ "metric = '#{metric_name_code}', options: #{options}, ")
+ end
+
+ # Not recommended for production use, because tracers must be removed in reverse-order
+ # from when they were added, or else other tracers that were added to the same method
+ # may get removed as well.
+ def remove_method_tracer(method_name, metric_name_code)
+ return unless NewRelic::Agent.agent.config.tracers_enabled?
+
+ if method_defined? "#{_traced_method_name(method_name, metric_name_code)}"
+ alias_method method_name, "#{_untraced_method_name(method_name, metric_name_code)}"
+ undef_method "#{_traced_method_name(method_name, metric_name_code)}"
+ else
+ raise "No tracer for '#{metric_name_code}' on method '#{method_name}'"
+ end
+ end
+
+private
+
+ def _untraced_method_name(method_name, metric_name)
+ "#{_sanitize_name(method_name)}_without_trace_#{_sanitize_name(metric_name)}"
+ end
+
+ def _traced_method_name(method_name, metric_name)
+ "#{_sanitize_name(method_name)}_with_trace_#{_sanitize_name(metric_name)}"
+ end
+
+ def _sanitize_name(name)
+ name.to_s.tr('^a-z,A-Z,0-9', '_')
+ end
+end
28 lib/new_relic/agent/samplers/cpu.rb
@@ -0,0 +1,28 @@
+module NewRelic::Agent
+ class CPUSampler
+ def initialize
+
+ agent = NewRelic::Agent.instance
+
+ agent.stats_engine.add_sampled_metric("CPU/User Time") do | stats |
+ t = Process.times
+ @last_utime ||= t.utime
+
+ utime = t.utime
+ stats.record_data_point(utime - @last_utime) if (utime - @last_utime) >= 0
+ @last_utime = utime
+ end
+
+ agent.stats_engine.add_sampled_metric("CPU/System Time") do | stats |
+ t = Process.times
+ @last_stime ||= t.stime
+
+ stime = t.stime
+ stats.record_data_point(stime - @last_stime) if (stime - @last_stime) >= 0
+ @last_stime = stime
+ end
+ end
+ end
+end
+
+NewRelic::Agent::CPUSampler.new
47 lib/new_relic/agent/samplers/memory.rb
@@ -0,0 +1,47 @@
+module NewRelic::Agent
+ class MemorySampler
+ def initialize
+ if RUBY_PLATFORM =~ /java/
+ platform = %x[uname -s].downcase
+ else
+ platform = RUBY_PLATFORM.downcase
+ end
+
+ # macos, linux, solaris
+ if platform =~ /darwin|linux/
+ @ps = "ps -o rsz"
+ elsif platform =~ /freebsd/
+ @ps = "ps -o rss"
+ elsif platform =~ /solaris/
+ @ps = "ps -o rss -p"
+ end
+ if !@ps
+ raise "Unsupported platform for getting memory: #{platform}"
+ end
+
+ if @ps
+ @broken = false
+
+ agent = NewRelic::Agent.instance
+ agent.stats_engine.add_sampled_metric("Memory/Physical") do |stats|
+ if !@broken
+ memory = `#{@ps} #{$$}`.split("\n")[1].to_f / 1024
+
+ # if for some reason the ps command doesn't work on the resident os,
+ # then don't execute it any more.
+ if memory > 0
+ stats.record_data_point memory
+
+ else
+ NewRelic::Agent.instance.log.error "Error attempting to determine resident memory (got result of #{memory}). Disabling this metric."
+ NewRelic::Agent.instance.log.error "Faulty command: `#{@ps}`"
+ @broken = true
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+NewRelic::Agent::MemorySampler.new
240 lib/new_relic/agent/stats_engine.rb
@@ -0,0 +1,240 @@
+#require 'new_relic/stats'
+#require 'new_relic/metric_data'
+require 'logger'
+
+module NewRelic::Agent
+ class StatsEngine
+ POLL_PERIOD = 10
+
+ attr_accessor :log
+
+ ScopeStackElement = Struct.new(:name, :children_time, :deduct_call_time_from_parent)
+
+ class SampledItem
+ def initialize(stats, &callback)
+ @stats = stats
+ @callback = callback
+ end
+
+ def poll
+ @callback.call @stats
+ end
+ end
+
+ def initialize(log = Logger.new(STDERR))
+ @stats_hash = {}
+ @sampled_items = []
+ @scope_stack_listener = nil
+ @log = log
+
+ # Makes the unit tests happy
+ Thread::current[:newrelic_scope_stack] = nil
+
+ spawn_sampler_thread
+ end
+
+ def spawn_sampler_thread
+
+ return if !@sampler_process.nil? && @sampler_process == $$
+
+ # start up a thread that will periodically poll for metric samples
+ @sampler_thread = Thread.new do
+ while true do
+ begin
+ sleep POLL_PERIOD
+ @sampled_items.each do |sampled_item|
+ begin
+ sampled_item.poll
+ rescue => e
+ log.error e
+ @sampled_items.delete sampled_item
+ log.error "Removing #{sampled_item} from list"
+ log.debug e.backtrace.to_s
+ end
+ end
+ end
+ end
+ end
+
+ @sampler_process = $$
+ end
+
+ def add_scope_stack_listener(l)
+ fail "Can't add a scope listener midflight in a transaction" if scope_stack.any?
+# fail "Can't add more than one scope stack listener" if @scope_stack_listener
+ @scope_stack_listener = l
+ end
+
+ def remove_scope_stack_listener(l)
+ fail "Unknown stack listener trying to be removed" if @scope_stack_listener != l
+ @scope_stack_listener = nil
+ end
+
+ def push_scope(metric, time = Time.now.to_f, deduct_call_time_from_parent = true)
+
+ stack = (Thread::current[:newrelic_scope_stack] ||= [])
+
+ if @scope_stack_listener
+ @scope_stack_listener.notice_first_scope_push(time) if stack.empty?
+ @scope_stack_listener.notice_push_scope metric, time
+ end
+
+ scope = ScopeStackElement.new(metric, 0, deduct_call_time_from_parent)
+ stack.push scope
+
+ scope
+ end
+
+ def pop_scope(expected_scope, duration, time=Time.now.to_f)
+
+ stack = Thread::current[:newrelic_scope_stack]
+
+ scope = stack.pop
+
+ fail "unbalanced pop from blame stack: #{scope.name} != #{expected_scope.name}" if scope != expected_scope
+
+ stack.last.children_time += duration unless (stack.empty? || !scope.deduct_call_time_from_parent)
+
+ if !scope.deduct_call_time_from_parent && !stack.empty?
+ stack.last.children_time += scope.children_time
+ end
+
+ if @scope_stack_listener
+ @scope_stack_listener.notice_pop_scope(scope.name, time)
+ @scope_stack_listener.notice_scope_empty(time) if stack.empty?
+ end
+
+ scope
+ end
+
+ def peek_scope
+ scope_stack.last
+ end
+
+ def add_sampled_metric(metric_name, &sampler_callback)
+ stats = get_stats(metric_name, false)
+ @sampled_items << SampledItem.new(stats, &sampler_callback)
+ end
+
+ # set the name of the transaction for the current thread, which will be used
+ # to define the scope of all traced methods called on this thread until the
+ # scope stack is empty.
+ #
+ # currently the transaction name is the name of the controller action that
+ # is invoked via the dispatcher, but conceivably we could use other transaction
+ # names in the future if the traced application does more than service http request
+ # via controller actions
+ def transaction_name=(transaction)
+ Thread::current[:newrelic_transaction_name] = transaction
+ end
+
+ def transaction_name
+ Thread::current[:newrelic_transaction_name]
+ end
+
+
+ def lookup_stat(metric_name)
+ return @stats_hash[metric_name]
+ end
+
+
+ def get_stats_no_scope(metric_name)
+ stats = @stats_hash[metric_name]
+ if stats.nil?
+ stats = NewRelic::MethodTraceStats.new
+ @stats_hash[metric_name] = stats
+ end
+ stats
+ end
+
+ def get_stats(metric_name, use_scope = true)
+ stats = @stats_hash[metric_name]
+ if stats.nil?
+ stats = NewRelic::MethodTraceStats.new
+ @stats_hash[metric_name] = stats
+ end
+
+ if use_scope && transaction_name
+ spec = NewRelic::MetricSpec.new metric_name, transaction_name
+
+ scoped_stats = @stats_hash[spec]
+ if scoped_stats.nil?
+ scoped_stats = NewRelic::ScopedMethodTraceStats.new stats
+ @stats_hash[spec] = scoped_stats
+ end
+
+ stats = scoped_stats
+ end
+ return stats
+ end
+
+ def harvest_timeslice_data(previous_timeslice_data, metric_ids)
+ timeslice_data = {}
+ @stats_hash.keys.each do |metric_spec|
+
+
+ # get a copy of the stats collected since the last harvest, and clear
+ # the stats inside our hash table for the next time slice.
+ stats = @stats_hash[metric_spec]
+
+ # we have an optimization for unscoped metrics
+ if !(metric_spec.is_a? NewRelic::MetricSpec)
+ metric_spec = NewRelic::MetricSpec.new metric_spec
+ end
+
+ if stats.nil?
+ raise "Nil stats for #{metric_spec.name} (#{metric_spec.scope})"
+ end
+
+ stats_copy = stats.clone
+ stats.reset
+
+ # if the previous timeslice data has not been reported (due to an error of some sort)
+ # then we need to merge this timeslice with the previously accumulated - but not sent
+ # data
+ previous_metric_data = previous_timeslice_data[metric_spec]
+ stats_copy.merge! previous_metric_data.stats unless previous_metric_data.nil?
+
+ stats_copy.round!
+
+ # don't bother collecting and reporting stats that have zero-values for this timeslice.
+ # significant performance boost and storage savings.
+ unless stats_copy.call_count == 0
+
+ metric_spec_for_transport = (metric_ids[metric_spec].nil?) ? metric_spec : nil
+
+ metric_data = NewRelic::MetricData.new(metric_spec_for_transport, stats_copy, metric_ids[metric_spec])
+
+ timeslice_data[metric_spec] = metric_data
+ end
+ end
+
+ timeslice_data
+ end
+
+
+ def start_transaction
+ Thread::current[:newrelic_scope_stack] = []
+ end
+
+
+ # Try to clean up gracefully, otherwise we leave things hanging around on thread locals
+ #
+ def end_transaction
+ stack = Thread::current[:newrelic_scope_stack]
+
+ if stack
+ @scope_stack_listener.notice_scope_empty(Time.now) if @scope_stack_listener && !stack.empty?
+ Thread::current[:newrelic_scope_stack] = nil
+ end
+
+ Thread::current[:newrelic_transaction_name] = nil
+ end
+
+ private
+
+ def scope_stack
+ Thread::current[:newrelic_scope_stack] ||= []
+ end
+ end
+end
43 lib/new_relic/agent/synchronize.rb
<
@@ -0,0 +1,43 @@
+
+require 'sync'
+
+
+module NewRelic::Agent
+
+ module Synchronize
+ def synchronize_sync
+ @_local_sync ||= Sync.new
+
+ @_local_sync.synchronize(:EX) do
+ yield
+ end
+ end
+
+