# encoding: utf-8 # This file is distributed under New Relic's license terms. # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details. require 'new_relic/agent/transaction/pop' require 'new_relic/agent/transaction_timings' # A struct holding the information required to measure a controller # action. This is put on the thread local. Handles the issue of # re-entrancy, or nested action calls. # # This class is not part of the public API. Avoid making calls on it directly. # module NewRelic module Agent class Transaction # helper module refactored out of the `pop` method include Pop attr_accessor :start_time # A Time instance for the start time, never nil attr_accessor :apdex_start # A Time instance used for calculating the apdex score, which # might end up being @start, or it might be further upstream if # we can find a request header for the queue entry time attr_accessor(:type, :exceptions, :filtered_params, :force_flag, :jruby_cpu_start, :process_cpu_start, :database_metric_name) attr_reader :name attr_reader :stats_hash # Give the current transaction a request context. Use this to # get the URI and referer. The request is interpreted loosely # as a Rack::Request or an ActionController::AbstractRequest. attr_accessor :request # Return the currently active transaction, or nil. def self.current self.stack.last end def self.parent self.stack[-2] end def self.start(transaction_type, options={}) txn = Transaction.new(transaction_type, options) txn.start(transaction_type) self.stack.push(txn) return txn end def self.stop(metric_name=nil, end_time=Time.now) txn = self.stack.last txn.stop(metric_name, end_time) if txn return self.stack.pop end def self.stack TransactionState.get.current_transaction_stack end def self.in_transaction? !self.stack.empty? end # This is the name of the model currently assigned to database # measurements, overriding the default. def self.database_metric_name current && current.database_metric_name end def self.referer current && current.referer end def self.agent NewRelic::Agent.instance end def self.freeze_name self.current && self.current.freeze_name end @@java_classes_loaded = false if defined? JRuby begin require 'java' java_import 'java.lang.management.ManagementFactory' java_import 'com.sun.management.OperatingSystemMXBean' @@java_classes_loaded = true rescue => e end end attr_reader :depth def initialize(type=:controller, options={}) @type = type @start_time = Time.now @apdex_start = @start_time @jruby_cpu_start = jruby_cpu_time @process_cpu_start = process_cpu @filtered_params = options[:filtered_params] || {} @force_flag = options[:force] @request = options[:request] @exceptions = {} @stats_hash = StatsHash.new TransactionState.get.transaction = self end def noticed_error_ids @noticed_error_ids ||= [] end def name=(name) if !@name_frozen @name = name else NewRelic::Agent.logger.warn("Attempted to rename transaction to '#{name}' after transaction name was already frozen as '#{@name}'.") end end def name_set? @name && @name != NewRelic::Agent::UNKNOWN_METRIC end def freeze_name return if name_frozen? @name = NewRelic::Agent.instance.transaction_rules.rename(@name) @name_frozen = true end def name_frozen? @name_frozen end def parent has_parent? && self.class.stack[-2] end def root? self.class.stack.size == 1 end def has_parent? self.class.stack.size > 1 end # Indicate that we are entering a measured controller action or task. # Make sure you unwind every push with a pop call. def start(transaction_type) @name = NewRelic::Agent::UNKNOWN_METRIC transaction_sampler.notice_first_scope_push(start_time) sql_sampler.notice_first_scope_push(start_time) NewRelic::Agent::StatsEngine::GCProfiler.init agent.stats_engine.start_transaction transaction_sampler.notice_transaction(uri, filtered_params) sql_sampler.notice_transaction(uri, filtered_params) end # Indicate that you don't want to keep the currently saved transaction # information def self.abort_transaction! current.abort_transaction! if current end # For the current web transaction, return the path of the URI minus the host part and query string, or nil. def uri @uri ||= self.class.uri_from_request(@request) unless @request.nil? end # For the current web transaction, return the full referer, minus the host string, or nil. def referer @referer ||= self.class.referer_from_request(@request) end # Call this to ensure that the current transaction is not saved def abort_transaction! transaction_sampler.ignore_transaction end # Unwind one stack level. It knows if it's back at the outermost caller and # does the appropriate wrapup of the context. def stop(fallback_name=::NewRelic::Agent::UNKNOWN_METRIC, end_time=Time.now) @name = fallback_name unless name_set? || name_frozen? freeze_name log_underflow if @type.nil? # these record metrics so need to be done before merging stats if self.root? # this one records metrics and wants to happen # before the transaction sampler is finished if traced? record_transaction_cpu gc_time = NewRelic::Agent::StatsEngine::GCProfiler.capture end @transaction_trace = transaction_sampler.notice_scope_empty(self, Time.now, gc_time) sql_sampler.notice_scope_empty(@name) overview_metrics = transaction_overview_metrics end record_exceptions merge_stats_hash # these tear everything down so need to be done after merging stats if self.root? send_transaction_finished_event(start_time, end_time, overview_metrics) agent.stats_engine.end_transaction end end def send_transaction_finished_event(start_time, end_time, overview_metrics) payload = { :name => @name, :type => @type, :start_timestamp => start_time.to_f, :duration => end_time.to_f - start_time.to_f, :overview_metrics => overview_metrics } agent.events.notify(:transaction_finished, payload) end def merge_stats_hash stats_hash.resolve_scopes!(@name) NewRelic::Agent.instance.stats_engine.merge!(stats_hash) end def record_exceptions @exceptions.each do |exception, options| options[:metric] = @name agent.error_collector.notice_error(exception, options) end end OVERVIEW_SPECS = [ [:webDuration, MetricSpec.new('HttpDispatcher')], [:queueDuration, MetricSpec.new('WebFrontend/QueueTime')], [:externalDuration, MetricSpec.new('External/allWeb')], [:databaseDuration, MetricSpec.new('ActiveRecord/all')], [:gcCumulative, MetricSpec.new("GC/cumulative")], [:memcacheDuration, MetricSpec.new('Memcache/allWeb')] ] def transaction_overview_metrics metrics = {} stats = @stats_hash OVERVIEW_SPECS.each do |(dest_key, spec)| metrics[dest_key] = stats[spec].total_call_time if stats.key?(spec) end metrics end # If we have an active transaction, notice the error and increment the error metric. # Options: # * :request => Request object to get the uri and referer # * :uri => The request path, minus any request params or query string. # * :referer => The URI of the referer # * :metric => The metric name associated with the transaction # * :request_params => Request parameters, already filtered if necessary # * :custom_params => Custom parameters # Anything left over is treated as custom params def self.notice_error(e, options={}) request = options.delete(:request) if request options[:referer] = referer_from_request(request) options[:uri] = uri_from_request(request) end if current current.notice_error(e, options) else agent.error_collector.notice_error(e, options) end end # Do not call this. Invoke the class method instead. def notice_error(e, options={}) # :nodoc: params = custom_parameters options[:referer] = referer if referer options[:request_params] = filtered_params if filtered_params options[:uri] = uri if uri options.merge!(custom_parameters) if !@exceptions.keys.include?(e) @exceptions[e] = options end end # Add context parameters to the transaction. This information will be passed in to errors # and transaction traces. Keys and Values should be strings, numbers or date/times. def self.add_custom_parameters(p) current.add_custom_parameters(p) if current end def self.custom_parameters (current && current.custom_parameters) ? current.custom_parameters : {} end def self.set_user_attributes(attributes) current.set_user_attributes(attributes) if current end def self.user_attributes (current) ? current.user_attributes : {} end APDEX_METRIC_SPEC = NewRelic::MetricSpec.new('Apdex').freeze def record_apdex(end_time=Time.now, is_error=nil) return unless recording_web_transaction? && NewRelic::Agent.is_execution_traced? freeze_name action_duration = end_time - start_time total_duration = end_time - apdex_start is_error = is_error.nil? ? !exceptions.empty? : is_error apdex_bucket_global = self.class.apdex_bucket(total_duration, is_error, apdex_t) apdex_bucket_txn = self.class.apdex_bucket(action_duration, is_error, apdex_t) @stats_hash.record(APDEX_METRIC_SPEC, apdex_bucket_global, apdex_t) txn_apdex_metric = NewRelic::MetricSpec.new(@name.gsub(/^[^\/]+\//, 'Apdex/')) @stats_hash.record(txn_apdex_metric, apdex_bucket_txn, apdex_t) end def apdex_t transaction_specific_apdex_t || Agent.config[:apdex_t] end def transaction_specific_apdex_t key = :web_transactions_apdex Agent.config[key] && Agent.config[key][self.name] end # Yield to a block that is run with a database metric name context. This means # the Database instrumentation will use this for the metric name if it does not # otherwise know about a model. This is re-entrant. # # * model is the DB model class # * method is the name of the finder method or other method to identify the operation with. # def with_database_metric_name(model, method) previous = @database_metric_name model_name = case model when Class model.name when String model else model.to_s end @database_metric_name = "ActiveRecord/#{model_name}/#{method}" yield ensure @database_metric_name=previous end def custom_parameters @custom_parameters ||= {} end def user_attributes @user_atrributes ||= {} end def queue_time @apdex_start ? @start_time - @apdex_start : 0 end def add_custom_parameters(p) custom_parameters.merge!(p) end def set_user_attributes(attributes) user_attributes.merge!(attributes) end # Returns truthy if the current in-progress transaction is considered a # a web transaction (as opposed to, e.g., a background transaction). def self.recording_web_transaction? self.current && self.current.recording_web_transaction? end def self.transaction_type_is_web?(type) [:controller, :uri, :rack, :sinatra].include?(type) end def recording_web_transaction? self.class.transaction_type_is_web?(@type) end # Make a safe attempt to get the referer from a request object, generally successful when # it's a Rack request. def self.referer_from_request(request) if request && request.respond_to?(:referer) request.referer.to_s.split('?').first end end # Make a safe attempt to get the URI, without the host and query string. def self.uri_from_request(request) approximate_uri = case when request.respond_to?(:fullpath) then request.fullpath when request.respond_to?(:path) then request.path when request.respond_to?(:request_uri) then request.request_uri when request.respond_to?(:uri) then request.uri when request.respond_to?(:url) then request.url end return approximate_uri[%r{^(https?://.*?)?(/[^?]*)}, 2] || '/' if approximate_uri # ' end def self.record_apdex(end_time, is_error) current && current.record_apdex(end_time, is_error) end def self.apdex_bucket(duration, failed, apdex_t) case when failed :apdex_f when duration <= apdex_t :apdex_s when duration <= 4 * apdex_t :apdex_t else :apdex_f end end private def process_cpu return nil if defined? JRuby p = Process.times p.stime + p.utime end def jruby_cpu_time # :nodoc: return nil unless @@java_classes_loaded threadMBean = ManagementFactory.getThreadMXBean() java_utime = threadMBean.getCurrentThreadUserTime() # ns -1 == java_utime ? 0.0 : java_utime/1e9 end def agent NewRelic::Agent.instance end def transaction_sampler agent.transaction_sampler end def sql_sampler agent.sql_sampler end end end end