# This file is distributed under New Relic's license terms. # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. # frozen_string_literal: true # This file may be independently required to set up method tracing prior to # the full agent loading. In those cases, we do need at least this require to # bootstrap things. require 'new_relic/control' unless defined?(NewRelic::Control) require 'new_relic/agent/method_tracer_helpers' module NewRelic module Agent # This module contains class methods added to support installing custom # metric tracers and executing for individual metrics. # # == Examples # # When the agent initializes, it extends Module with these methods. # However if you want to use the API in code that might get loaded # before the agent is initialized you will need to require # this file: # # require 'new_relic/agent/method_tracer' # class A # include NewRelic::Agent::MethodTracer # def process # ... # end # add_method_tracer :process # end # # To instrument a class method: # # require 'new_relic/agent/method_tracer' # class An # def self.process # ... # end # class << self # include NewRelic::Agent::MethodTracer # add_method_tracer :process # end # end # # @api public # module MethodTracer def self.included(klass) klass.extend(ClassMethods) end def self.extended(klass) klass.extend(ClassMethods) end # Trace a given block with stats and keep track of the caller. # See NewRelic::Agent::MethodTracer::ClassMethods#add_method_tracer for a description of the arguments. # +metric_names+ is either a single name or an array of metric names. # If more than one metric is passed, the +produce_metric+ option only applies to the first. The # others are always recorded. Only the first metric is pushed onto the scope stack. # # Generally you pass an array of metric names if you want to record the metric under additional # categories, but generally this *should never ever be done*. Most of the time you can aggregate # on the server. # # @api public # def trace_execution_scoped(metric_names, options = NewRelic::EMPTY_HASH) # THREAD_LOCAL_ACCESS NewRelic::Agent.record_api_supportability_metric(:trace_execution_scoped) unless options[:internal] NewRelic::Agent::MethodTracerHelpers.trace_execution_scoped(metric_names, options) do # Using an implicit block avoids object allocation for a &block param yield end end # Trace a given block with stats assigned to the given metric_name. It does not # provide scoped measurements, meaning whatever is being traced will not 'blame the # Controller'--that is to say appear in the breakdown chart. # # * metric_names is a single name or an array of names of metrics # # @api public # def trace_execution_unscoped(metric_names, options = NewRelic::EMPTY_HASH) # THREAD_LOCAL_ACCESS NewRelic::Agent.record_api_supportability_metric(:trace_execution_unscoped) unless options[:internal] return yield unless NewRelic::Agent.tl_is_execution_traced? t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin yield ensure duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 NewRelic::Agent.instance.stats_engine.tl_record_unscoped_metrics(metric_names, duration) end end # Defines methods used at the class level, for adding instrumentation # @api public module ClassMethods # contains methods refactored out of the #add_method_tracer method module AddMethodTracer ALLOWED_KEYS = [:metric, :push_scope, :code_header, :code_information, :code_footer].freeze DEFAULT_SETTINGS = {:push_scope => true, :metric => true, :code_header => '', :code_footer => ''}.freeze # Checks the provided options to make sure that they make # sense. Raises an error if the options are incorrect to # assist with debugging, so that errors occur at class # construction time rather than instrumentation run time def _nr_validate_method_tracer_options(method_name, options) unless options.is_a?(Hash) raise TypeError.new("Error adding method tracer to #{method_name}: provided options must be a Hash") end unrecognized_keys = options.keys - ALLOWED_KEYS if unrecognized_keys.any? raise "Unrecognized options when adding method tracer to #{method_name}: " + unrecognized_keys.join(', ') end options = DEFAULT_SETTINGS.merge(options) unless options[:push_scope] || options[:metric] raise "Can't add a tracer where push_scope is false and metric is false" end options end # Default to the class where the method is defined. # # Example: # Foo._nr_default_metric_name_code('bar') #=> "Custom/#{Foo.name}/bar" def _nr_default_metric_name(method_name) class_name = _nr_derived_class_name ->(*) { "Custom/#{class_name}/#{method_name}" } end # Checks to see if the method we are attempting to trace # actually exists or not. #add_method_tracer can't do # anything if the method doesn't exist. def newrelic_method_exists?(method_name) exists = method_defined?(method_name) || private_method_defined?(method_name) ::NewRelic::Agent.logger.error("Did not trace #{_nr_derived_class_name}##{method_name} because that method does not exist") unless exists exists end # Checks to see if we have already traced a method with a # given metric by checking to see if the traced method # exists. Warns the user if methods are being double-traced # to help with debugging custom instrumentation. def method_traced?(method_name) exists = method_name && _nr_traced_method_module.method_defined?(method_name) ::NewRelic::Agent.logger.error("Attempt to trace a method twice: Method = #{method_name}") if exists exists end # Returns an anonymous module that stores prepended trace methods. def _nr_traced_method_module @_nr_traced_method_module ||= Module.new end # for testing only def _nr_clear_traced_methods! _nr_traced_method_module.module_eval do self.instance_methods.each { |m| remove_method(m) } end end def _nr_derived_class_name return self.name if self.name && !self.name.empty? return 'AnonymousModule' if self.to_s.start_with?('#" name = self.to_s[/^#$/, 1] if name.start_with?('0x') 'AnonymousClass' elsif name.start_with?('# { "Custom/#{self.class.name}/foo" } # # This would name the metric according to the class of the runtime # instance, as opposed to the class where +foo+ is defined. # # If not provided, the metric name will be Custom/ClassName/method_name. # # @param method_name [Symbol] the name of the method to trace # @param metric_name [String,Proc,Array] the metric name to record calls to # the traced method under. This may be either a String, or a Proc # to be evaluated at call-time in order to determine the metric # name dynamically. # This method also accepts an array of Strings/Procs, in which case the # first metric given will be scoped, while the remaining metrics will be # recorded as though passed with :push_scope => false. If an Array of # metric names is given with :push_scope => false, all metrics will be # unscoped. # @param [Hash] options additional options controlling how the method is # traced. # @option options [Boolean] :push_scope (true) If false, the traced method will # not appear in transaction traces or breakdown charts, and it will # only be visible in custom dashboards. # @option options [Boolean] :metric (true) If false, the traced method will # only appear in transaction traces, but no metrics will be recorded # for it. # @option options [Proc] :code_header ('') Ruby code to be inserted and run # before the tracer begins timing. # @option options [Proc] :code_footer ('') Ruby code to be inserted and run # after the tracer stops timing. # # @example # add_method_tracer :foo # # # With a custom metric name # add_method_tracer :foo, "Custom/MyClass/foo" # add_method_tracer :bar, -> { "Custom/#{self.class.name}/bar" } # # # Instrument foo only for custom dashboards (not in transaction # # traces or breakdown charts) # add_method_tracer :foo, 'Custom/foo', :push_scope => false # # # Instrument foo in transaction traces only # add_method_tracer :foo, 'Custom/foo', :metric => false # # @api public # def add_method_tracer(method_name, metric_name = nil, options = {}) ::NewRelic::Agent.add_or_defer_method_tracer(self, method_name, metric_name, options) end # For tests only 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) # :nodoc: return unless Agent.config[:agent_enabled] if _nr_traced_method_module.method_defined?(method_name) _nr_traced_method_module.send(:remove_method, method_name) ::NewRelic::Agent.logger.debug("removed method tracer #{method_name}\n") else raise "No tracer on method '#{method_name}'" end end private def _nr_add_method_tracer_now(method_name, metric_name, options) NewRelic::Agent.record_api_supportability_metric(:add_method_tracer) return unless newrelic_method_exists?(method_name) remove_method_tracer(method_name) if method_traced?(method_name) options = _nr_validate_method_tracer_options(method_name, options) visibility = NewRelic::Helper.instance_method_visibility(self, method_name) scoped_metric, unscoped_metrics = _nr_scoped_unscoped_metrics(metric_name, method_name, push_scope: options[:push_scope]) _nr_define_traced_method(method_name, scoped_metric: scoped_metric, unscoped_metrics: unscoped_metrics, code_header: options[:code_header], code_footer: options[:code_footer], record_metrics: options[:metric], visibility: visibility, code_information: options[:code_information]) prepend(_nr_traced_method_module) ::NewRelic::Agent.logger.debug("Traced method: class = #{_nr_derived_class_name}," + "method = #{method_name}, " + "metric = '#{metric_name}'") end # See #add_method_tracer; if multiple metric names are given, the first is # treated as scoped, the rest unscoped. If options[:push_scope] is false, # all given metrics are unscoped. def _nr_scoped_unscoped_metrics(metric_name, method_name, push_scope: true) if metric_name.is_a?(Array) && push_scope [metric_name.shift, metric_name] elsif push_scope [metric_name || _nr_default_metric_name(method_name), []] else [nil, Array(metric_name)] end end def _nr_define_traced_method(method_name, scoped_metric: nil, unscoped_metrics: [], code_header: nil, code_footer: nil, record_metrics: true, visibility: :public, code_information: {}) _nr_traced_method_module.module_eval do define_method(method_name) do |*args, &block| return super(*args, &block) unless NewRelic::Agent.tl_is_execution_traced? scoped_metric_eval, unscoped_metrics_eval = nil, [] scoped_metric_eval = case scoped_metric when Proc instance_exec(*args, &scoped_metric) when String scoped_metric end unscoped_metrics_eval = unscoped_metrics.map do |metric| metric.kind_of?(Proc) ? instance_exec(*args, &metric) : metric.to_s end instance_exec(&code_header) if code_header.kind_of?(Proc) # NOTE: Calling ::NewRelic::Agent::MethodTracer.trace_execution_scoped and # .trace_execution_unscoped below relies on the fact that MethodTracer is included # in Module on agent startup. If/when this changes, these methods should be # explicitly namespaced and extended in MethodTracer. # If tracing multiple metrics on this method, nest one unscoped trace inside the scoped trace. begin if scoped_metric_eval ::NewRelic::Agent::MethodTracer.trace_execution_scoped(scoped_metric_eval, metric: record_metrics, internal: true, code_information: code_information) do if unscoped_metrics_eval.empty? super(*args, &block) else ::NewRelic::Agent::MethodTracer.trace_execution_unscoped(unscoped_metrics_eval, internal: true) do super(*args, &block) end end end elsif !unscoped_metrics_eval.empty? ::NewRelic::Agent::MethodTracer.trace_execution_unscoped(unscoped_metrics_eval, internal: true) do super(*args, &block) end end ensure instance_exec(&code_footer) if code_footer.kind_of?(Proc) end end send(visibility, method_name) ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true) end end end # @!parse extend ClassMethods end end end