# 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