# encoding: utf-8
require 'one_apm/transaction'
require 'one_apm/transaction/transaction_namer'
require 'one_apm/inst/support/queue_time'
require 'one_apm/inst/support/ignore_actions'
module OneApm
module Agent
module Instrumentation
#
# OneApm instrumentation for transactions
#
# * controller actions
# * background tasks
# * external web calls
#
# see
# * add_transaction_tracer
# * perform_action_with_oneapm_trace
#
module TransactionBase
def self.included(clazz)
clazz.extend(ClassMethods)
end
module Shim
module ClassMethodsShim
def oneapm_ignore(*args); end
def oneapm_ignore_apdex(*args); end
def oneapm_ignore_enduser(*args); end
end
def self.included(clazz)
clazz.extend(ClassMethodsShim)
end
def oneapm_notice_error(*args); end
def one_apm_trace_controller_action(*args); yield; end
def perform_action_with_oneapm_trace(*args); yield; end
end
OA_DO_NOT_TRACE_KEY = :'@do_not_trace' unless defined?(OA_DO_NOT_TRACE_KEY )
OA_IGNORE_APDEX_KEY = :'@ignore_apdex' unless defined?(OA_IGNORE_APDEX_KEY )
OA_IGNORE_ENDUSER_KEY = :'@ignore_enduser' unless defined?(OA_IGNORE_ENDUSER_KEY)
OA_DEFAULT_OPTIONS = {}.freeze unless defined?(OA_DEFAULT_OPTIONS )
module ClassMethods
# Have OneApm 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 oneapm_ignore(options = {})
oneapm_ignore_aspect(OA_DO_NOT_TRACE_KEY, options)
end
# Have OneApm 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.
#
# @api public
#
def oneapm_ignore_apdex(options = {})
oneapm_ignore_aspect(OA_IGNORE_APDEX_KEY, options)
end
# Have OneApm skip install javascript_instrumentation
#
# @api public
#
def oneapm_ignore_enduser(options = {})
oneapm_ignore_aspect(OA_IGNORE_ENDUSER_KEY, options)
end
def oneapm_ignore_aspect(property, options = {})
if options.empty?
oneapm_write_attr property, true
elsif !options.is_a?(Hash)
OneApm::Manager.logger.error "oneapm_#{property} takes an optional hash with :only and :except lists of actions (illegal argument type '#{options.class}')"
else
oneapm_write_attr property, options
end
end
def oneapm_write_attr(attr_name, value)
instance_variable_set(attr_name, value)
end
def oneapm_read_attr(attr_name)
instance_variable_get(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 OneApm::Agent::Instrumentation::TransactionBase
# 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 OneApm::Agent::Instrumentation::TransactionBase
# # 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
# oneapm_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 OneApm::Agent::Instrumentation::TransactionBase
# 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 OneApm::Agent::Instrumentation::TransactionBase#perform_action_with_oneapm_trace
# for the full list of available options.
#
# @api public
#
def add_transaction_tracer(method, options = {})
options[:name] ||= method.to_s
argument_list = generate_argument_list(options)
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)
OneApm::Manager.logger.warn("Transaction tracer already in place for class = #{self.name}, method = #{method.to_s}, skipping")
return
end
class_eval <<-EOC
def #{with_method_name}(*args, &block)
perform_action_with_oneapm_trace(#{argument_list.join(',')}) do
#{without_method_name}(*args, &block)
end
end
EOC
visibility = OneApm::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
OneApm::Manager.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_oneapm_transaction_trace#{punctuation}",
"#{traced_method.to_s}_without_oneapm_transaction_trace#{punctuation}"
]
end
def already_added_transaction_tracer?(target, with_method_name)
if OneApm::Helper.instance_methods_include?(target, with_method_name)
true
else
false
end
end
end
# Yield to the given block with OneApm 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 OneApm as if they were first class controller
# actions
#
# MyController < ActionController::Base
# include OneApm::Agent::Instrumentation::TransactionBase
# # dispatch the given op to the method given by the service parameter.
# def invoke_operation
# op = params['operation']
# perform_action_with_oneapm_trace(:name => op) do
# send op, params['message']
# end
# end
# # Ignore the invoker to avoid double counting
# oneapm_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 OneApm 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 => aClass.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 uri and referer.
#
# @api public
#
def perform_action_with_oneapm_trace(*args, &block)
state = OneApm::TransactionState.tl_get
state.request = oneapm_request(args)
skip_tracing = do_not_trace? || !state.is_execution_traced?
if skip_tracing
state.current_transaction.ignore! if state.current_transaction
OneApm::Manager.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 : OA_DEFAULT_OPTIONS
category = trace_options[:category] || :controller
txn_options = create_transaction_options(trace_options, category, state)
begin
txn = Transaction.start(state, category, txn_options)
begin
yield
rescue => e
OneApm::Manager.notice_error(e)
raise
end
ensure
if txn
txn.ignore_apdex! if ignore_apdex?
txn.ignore_enduser! if ignore_enduser?
end
Transaction.stop(state)
end
end
protected
def oneapm_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
end
end
# Should be implemented in the dispatcher class
def oneapm_response_code; end
def oneapm_request_headers(state)
request = state.request
if request
if request.respond_to?(:headers)
request.headers
elsif request.respond_to?(:env)
request.env
end
end
end
# overrideable 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?(OA_DO_NOT_TRACE_KEY)
end
# overrideable 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?(OA_IGNORE_APDEX_KEY)
end
def ignore_enduser?
_is_filtered?(OA_IGNORE_ENDUSER_KEY)
end
private
def create_transaction_options(trace_options, category, state)
txn_options = {}
txn_options[:request] = trace_options[:request]
txn_options[:request] ||= request if respond_to?(:request)
# params should have been filtered before calling perform_action_with_oneapm_trace
txn_options[:filtered_params] = trace_options[:params]
txn_options[:transaction_name] = OneApm::TransactionNamer.name_for(nil, self, category, trace_options)
txn_options[:apdex_start_time] = detect_queue_start_time(state)
txn_options
end
# Filter out a request if it matches one of our parameters for
# ignoring it - the key is either OA_DO_NOT_TRACE_KEY or OA_IGNORE_APDEX_KEY
def _is_filtered?(key)
name = if respond_to?(:action_name)
action_name
else
:'[action_name_missing]'
end
OneApm::Agent::Instrumentation::IgnoreActions.is_filtered?(
key,
self.class,
name)
end
def detect_queue_start_time(state)
headers = oneapm_request_headers(state)
QueueTime.parse_frontend_timestamp(headers) if headers
end
end
end
end
end