# 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
require 'new_relic/agent/transaction'
require 'new_relic/agent/instrumentation/queue_time'
require 'new_relic/agent/instrumentation/ignore_actions'
module NewRelic
module Agent
# @api public
module Instrumentation
# == NewRelic instrumentation for controller actions and tasks
#
# This module can also be used to capture performance information for
# background tasks and other non-web transactions, including
# detailed transaction traces and traced errors.
#
# For details on how to instrument background tasks see
# {ClassMethods#add_transaction_tracer} and
# {#perform_action_with_newrelic_trace}
#
# @api public
#
module ControllerInstrumentation
def self.included(clazz) # :nodoc:
clazz.extend(ClassMethods)
end
# This module is for importing stubs when the agent is disabled
module ClassMethodsShim # :nodoc:
def newrelic_ignore(*args); end
def newrelic_ignore_apdex(*args); end
def newrelic_ignore_enduser(*args); end
end
module Shim # :nodoc:
def self.included(clazz)
clazz.extend(ClassMethodsShim)
end
def new_relic_trace_controller_action(*args); yield; end
def perform_action_with_newrelic_trace(*args); yield; end
end
NR_DO_NOT_TRACE_KEY = :'@do_not_trace'
NR_IGNORE_APDEX_KEY = :'@ignore_apdex'
NR_IGNORE_ENDUSER_KEY = :'@ignore_enduser'
NR_DEFAULT_OPTIONS = NewRelic::EMPTY_HASH
# @api public
module ClassMethods
# 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.
#
# @api public
#
def newrelic_ignore(specifiers = {})
NewRelic::Agent.record_api_supportability_metric(:newrelic_ignore)
newrelic_ignore_aspect(NR_DO_NOT_TRACE_KEY, specifiers)
end
# Have NewRelic omit apdex measurements on the given actions. Typically used for
# actions that are not user facing or that skew your overall apdex measurement.
# Accepts :except and :only options, as with #newrelic_ignore.
#
# @api public
#
def newrelic_ignore_apdex(specifiers = {})
NewRelic::Agent.record_api_supportability_metric(:newrelic_ignore_apdex)
newrelic_ignore_aspect(NR_IGNORE_APDEX_KEY, specifiers)
end
# @api public
def newrelic_ignore_enduser(specifiers = {})
NewRelic::Agent.record_api_supportability_metric(:newrelic_ignore_enduser)
newrelic_ignore_aspect(NR_IGNORE_ENDUSER_KEY, specifiers)
end
def newrelic_ignore_aspect(property, specifiers = {}) # :nodoc:
if specifiers.empty?
self.newrelic_write_attr(property, true)
elsif !(Hash === specifiers)
::NewRelic::Agent.logger.error("newrelic_#{property} takes an optional hash with :only and :except lists of actions (illegal argument type '#{specifiers.class}')")
else
# symbolize the incoming values
specifiers = specifiers.inject({}) do |memo, (key, values)|
if values.is_a?(Array)
memo[key] = values.map(&:to_sym)
else
memo[key] = values.to_sym
end
memo
end
self.newrelic_write_attr(property, specifiers)
end
end
# Should be monkey patched into the controller class implemented
# with the inheritable attribute mechanism.
def newrelic_write_attr(attr_name, value) # :nodoc:
instance_variable_set(attr_name, value)
end
def newrelic_read_attr(attr_name) # :nodoc:
instance_variable_get(attr_name) if instance_variable_defined?(attr_name)
end
# Add transaction tracing to the given method. This will treat
# the given method as a main entrypoint for instrumentation, just
# like controller actions are treated by default. Useful especially
# for background tasks.
#
# Example for background job:
# class Job
# include NewRelic::Agent::Instrumentation::ControllerInstrumentation
# def run(task)
# ...
# end
# # Instrument run so tasks show up under task.name. Note single
# # quoting to defer eval to runtime.
# add_transaction_tracer :run, :name => '#{args[0].name}'
# end
#
# Here's an example of a controller that uses a dispatcher
# action to invoke operations which you want treated as top
# level actions, so they aren't all lumped into the invoker
# action.
#
# MyController < ActionController::Base
# include NewRelic::Agent::Instrumentation::ControllerInstrumentation
# # dispatch the given op to the method given by the service parameter.
# def invoke_operation
# op = params['operation']
# send op
# end
# # Ignore the invoker to avoid double counting
# newrelic_ignore :only => 'invoke_operation'
# # Instrument the operations:
# add_transaction_tracer :print
# add_transaction_tracer :show
# add_transaction_tracer :forward
# end
#
# Here's an example of how to pass contextual information into the transaction
# so it will appear in transaction traces:
#
# class Job
# include NewRelic::Agent::Instrumentation::ControllerInstrumentation
# def process(account)
# ...
# end
# # Include the account name in the transaction details. Note the single
# # quotes to defer eval until call time.
# add_transaction_tracer :process, :params => '{ :account_name => args[0].name }'
# end
#
# See NewRelic::Agent::Instrumentation::ControllerInstrumentation#perform_action_with_newrelic_trace
# for the full list of available options.
#
# @api public
#
def add_transaction_tracer(method, options = {})
NewRelic::Agent.record_api_supportability_metric(:add_transaction_tracer)
traced_method, punctuation = parse_punctuation(method)
with_method_name, without_method_name = build_method_names(traced_method, punctuation)
if already_added_transaction_tracer?(self, with_method_name)
::NewRelic::Agent.logger.warn("Transaction tracer already in place for class = #{self.name}, method = #{method.to_s}, skipping")
return
end
# The metric path:
options[:name] ||= method.to_s
code_info = NewRelic::Agent::MethodTracerHelpers.code_information(self, method)
argument_list = generate_argument_list(options.merge(code_info))
class_eval(<<-EOC)
def #{with_method_name}(*args, &block)
perform_action_with_newrelic_trace(#{argument_list.join(',')}) do
#{without_method_name}(*args, &block)
end
end
ruby2_keywords(:#{with_method_name}) if respond_to?(:ruby2_keywords, true)
EOC
visibility = NewRelic::Helper.instance_method_visibility(self, method)
alias_method(without_method_name, method.to_s)
alias_method(method.to_s, with_method_name)
send(visibility, method)
send(visibility, with_method_name)
::NewRelic::Agent.logger.debug("Traced transaction: class = #{self.name}, method = #{method.to_s}, options = #{options.inspect}")
end
def parse_punctuation(method)
[method.to_s.sub(/([?!=])$/, ''), $1]
end
def generate_argument_list(options)
options.map do |key, value|
value = if value.is_a?(Symbol)
value.inspect
elsif key == :params
value.to_s
else
%Q("#{value.to_s}")
end
%Q(:#{key} => #{value})
end
end
def build_method_names(traced_method, punctuation)
["#{traced_method.to_s}_with_newrelic_transaction_trace#{punctuation}",
"#{traced_method.to_s}_without_newrelic_transaction_trace#{punctuation}"]
end
def already_added_transaction_tracer?(target, with_method_name)
NewRelic::Helper.instance_methods_include?(target, with_method_name)
end
end
# @!parse extend ClassMethods
class TransactionNamer
def self.name_for(txn, traced_obj, category, options = {})
return options[:transaction_name] if options[:transaction_name]
"#{prefix_for_category(txn, category)}#{path_name(traced_obj, options)}"
end
def self.prefix_for_category(txn, category = nil)
category ||= (txn && txn.category)
case category
when :controller then ::NewRelic::Agent::Transaction::CONTROLLER_PREFIX
when :web then ::NewRelic::Agent::Transaction::CONTROLLER_PREFIX
when :task then ::NewRelic::Agent::Transaction::TASK_PREFIX
when :background then ::NewRelic::Agent::Transaction::TASK_PREFIX
when :rack then ::NewRelic::Agent::Transaction::RACK_PREFIX
when :uri then ::NewRelic::Agent::Transaction::CONTROLLER_PREFIX
when :sinatra then ::NewRelic::Agent::Transaction::SINATRA_PREFIX
when :middleware then ::NewRelic::Agent::Transaction::MIDDLEWARE_PREFIX
when :grape then ::NewRelic::Agent::Transaction::GRAPE_PREFIX
when :rake then ::NewRelic::Agent::Transaction::RAKE_PREFIX
when :action_cable then ::NewRelic::Agent::Transaction::ACTION_CABLE_PREFIX
when :message then ::NewRelic::Agent::Transaction::MESSAGE_PREFIX
else "#{category.to_s}/" # for internal use only
end
end
def self.path_name(traced_obj, options = {})
return options[:path] if options[:path]
class_name = class_name(traced_obj, options)
if options[:name]
if class_name
"#{class_name}/#{options[:name]}"
else
options[:name]
end
elsif traced_obj.respond_to?(:newrelic_metric_path)
traced_obj.newrelic_metric_path
else
class_name
end
end
def self.class_name(traced_obj, options = {})
return options[:class_name] if options[:class_name]
if traced_obj.is_a?(Class) || traced_obj.is_a?(Module)
traced_obj.name
else
traced_obj.class.name
end
end
end
# Yield to the given block with NewRelic tracing. Used by
# default instrumentation on controller actions in Rails.
# But it can also be used in custom instrumentation of controller
# methods and background tasks.
#
# This is the method invoked by instrumentation added by the
# ClassMethods#add_transaction_tracer.
#
# Here's a more verbose version of the example shown in
# ClassMethods#add_transaction_tracer using this method instead of
# #add_transaction_tracer.
#
# Below is a controller with an +invoke_operation+ action which
# dispatches to more specific operation methods based on a
# parameter (very dangerous, btw!). With this instrumentation,
# the +invoke_operation+ action is ignored but the operation
# methods show up in New Relic as if they were first class controller
# actions
#
# MyController < ActionController::Base
# include NewRelic::Agent::Instrumentation::ControllerInstrumentation
# # dispatch the given op to the method given by the service parameter.
# def invoke_operation
# op = params['operation']
# perform_action_with_newrelic_trace(:name => op) do
# send op, params['message']
# end
# end
# # Ignore the invoker to avoid double counting
# newrelic_ignore :only => 'invoke_operation'
# end
#
#
# When invoking this method explicitly as in the example above, pass in a
# block to measure with some combination of options:
#
# * :category => :controller indicates that this is a
# controller action and will appear with all the other actions. This
# is the default.
# * :category => :task indicates that this is a
# background task and will show up in New Relic with other background
# tasks instead of in the controllers list
# * :category => :middleware if you are instrumenting a rack
# middleware call. The :name is optional, useful if you
# have more than one potential transaction in the #call.
# * :category => :uri indicates that this is a
# web transaction whose name is a normalized URI, where 'normalized'
# means the URI does not have any elements with data in them such
# as in many REST URIs.
# * :name => action_name is used to specify the action
# name used as part of the metric name
# * :params => {...} to provide information about the context
# of the call, used in transaction trace display, for example:
# :params => { :account => @account.name, :file => file.name }
# These are treated similarly to request parameters in web transactions.
#
# Seldomly used options:
#
# * :class_name => Class.name is used to override the name
# of the class when used inside the metric name. Default is the
# current class.
# * :path => metric_path is *deprecated* in the public API. It
# allows you to set the entire metric after the category part. Overrides
# all the other options.
# * :request => Rack::Request#new(env) is used to pass in a
# request object that may respond to path and referer.
#
# @api public
#
def perform_action_with_newrelic_trace(*args, &block) # THREAD_LOCAL_ACCESS
NewRelic::Agent.record_api_supportability_metric(:perform_action_with_newrelic_trace)
state = NewRelic::Agent::Tracer.state
request = newrelic_request(args)
queue_start_time = detect_queue_start_time(request)
skip_tracing = do_not_trace? || !state.is_execution_traced?
if skip_tracing
state.current_transaction.ignore! if state.current_transaction
NewRelic::Agent.disable_all_tracing { return yield }
end
# This method has traditionally taken a variable number of arguments, but the
# only one that is expected / used is a single options hash. We are preserving
# the *args method signature to ensure backwards compatibility.
trace_options = args.last.is_a?(Hash) ? args.last : NR_DEFAULT_OPTIONS
category = trace_options[:category] || :controller
txn_options = create_transaction_options(trace_options, category, state, queue_start_time)
begin
finishable = Tracer.start_transaction_or_segment(
name: txn_options[:transaction_name],
category: category,
options: txn_options
)
begin
yield
rescue => e
NewRelic::Agent.notice_error(e)
raise
end
ensure
finishable.finish if finishable
end
end
protected
def newrelic_request(args)
opts = args.first
# passed as a parameter to add_transaction_tracer
if opts.respond_to?(:keys) && opts.respond_to?(:[]) && opts[:request]
opts[:request]
# in a Rails app
elsif self.respond_to?(:request)
self.request rescue nil
end
end
# Should be implemented in the dispatcher class
def newrelic_response_code; end
def newrelic_request_headers(request)
if request
if request.respond_to?(:headers)
request.headers
elsif request.respond_to?(:env)
request.env
end
end
end
# overridable method to determine whether to trace an action
# or not - you may override this in your controller and supply
# your own logic for ignoring transactions.
def do_not_trace?
_is_filtered?(NR_DO_NOT_TRACE_KEY)
end
# overridable method to determine whether to trace an action
# for purposes of apdex measurement - you can use this to
# ignore things like api calls or other fast non-user-facing
# actions
def ignore_apdex?
_is_filtered?(NR_IGNORE_APDEX_KEY)
end
def ignore_enduser?
_is_filtered?(NR_IGNORE_ENDUSER_KEY)
end
private
def create_transaction_options(trace_options, category, state, queue_start_time)
txn_options = {}
txn_options[:request] = trace_options[:request]
txn_options[:request] ||= request if respond_to?(:request) rescue nil
# params should have been filtered before calling perform_action_with_newrelic_trace
txn_options[:filtered_params] = trace_options[:params]
txn_options[:transaction_name] = TransactionNamer.name_for(nil, self, category, trace_options)
txn_options[:apdex_start_time] = queue_start_time
txn_options[:ignore_apdex] = ignore_apdex?
txn_options[:ignore_enduser] = ignore_enduser?
NewRelic::Agent::MethodTracerHelpers::SOURCE_CODE_INFORMATION_PARAMETERS.each do |parameter|
txn_options[parameter] = trace_options[parameter]
end
txn_options
end
# Filter out a request if it matches one of our parameters for
# ignoring it - the key is either NR_DO_NOT_TRACE_KEY or NR_IGNORE_APDEX_KEY
def _is_filtered?(key)
name = if respond_to?(:action_name)
action_name
else
:'[action_name_missing]'
end
NewRelic::Agent::Instrumentation::IgnoreActions.is_filtered?(
key,
self.class,
name
)
end
def detect_queue_start_time(request)
headers = newrelic_request_headers(request)
QueueTime.parse_frontend_timestamp(headers) if headers
end
end
end
end
end