# encoding: utf-8 require 'one_apm/inst/support/queue_time' require 'one_apm/transaction/transaction_timings' require 'one_apm/transaction/transaction_metrics' require 'one_apm/transaction/thread_local_access' require 'one_apm/transaction/metric_constants' require 'one_apm/transaction/transaction_synthetics' require 'one_apm/transaction/transaction_ignore' require 'one_apm/transaction/transaction_name' require 'one_apm/transaction/transaction_finish_append' require 'one_apm/transaction/class_methods' require 'one_apm/transaction/instance_helpers' require 'one_apm/transaction/transaction_jruby_functions' require 'one_apm/transaction/transaction_summary' require 'one_apm/transaction/transaction_cpu' require 'one_apm/transaction/transaction_apdex' require 'one_apm/support/method_tracer/helpers' module OneApm class Transaction extend ThreadLocalAccess include TransactionSynthetics include TransactionIgnore include TransactionName include TransactionFinishAppend include InstanceHelpers attr_accessor :start_time, :apdex_start, :exceptions, :filtered_params, :jruby_cpu_start, :process_cpu_start, :http_response_code, :request, :ignore_frames attr_reader :database_metric_name, :guid, :metrics, :gc_start_snapshot, :category, :frame_stack, :cat_path_hashes, :transaction_trace def initialize(category, options) @frame_stack = [] @has_children = false self.default_name = options[:transaction_name] @overridden_name = nil @frozen_name = nil @category = category @start_time = Time.now @apdex_start = options[:apdex_start_time] || @start_time @jruby_cpu_start = jruby_cpu_time @process_cpu_start = process_cpu @gc_start_snapshot = OneApm::Collector::StatsEngine::GCProfiler.take_snapshot @filtered_params = options[:filtered_params] || {} @request = options[:request] @exceptions = {} @metrics = TransactionMetrics.new @guid = generate_guid @cat_path_hashes = nil @ignore_this_transaction = false @ignore_apdex = false @ignore_enduser = false @ignore_frames = options[:ignore_frames] || [] end def start(state) return if !state.is_execution_traced? transaction_sampler.on_start_transaction(state, start_time, uri) sql_sampler.on_start_transaction(state, start_time, uri) agent.events.notify(:start_transaction) OneApm::Agent::BusyCalculator.dispatcher_start(start_time) frame_stack.push OneApm::Support::MethodTracer::Helpers.trace_execution_scoped_header(state, start_time.to_f) name_last_frame @default_name end def stop(state, end_time, outermost_frame) return if !state.is_execution_traced? freeze_name_and_execute_if_not_ignored ignore! if user_defined_rules_ignore? if @has_children name = Transaction.nested_transaction_name(outermost_frame.name) trace_options = OA_TRACE_OPTIONS_SCOPED else name = @frozen_name trace_options = OA_TRACE_OPTIONS_UNSCOPED end trace_options = OA_TRACE_IGNORE_OPTIONS if ignore_frame?(outermost_frame.name) # These metrics are recorded here instead of in record_summary_metrics # in order to capture the exclusive time associated with the outer-most # TT node. if needs_middleware_summary_metrics?(name) summary_metrics_with_exclusive_time = OA_MIDDLEWARE_SUMMARY_METRICS else summary_metrics_with_exclusive_time = OA_EMPTY_SUMMARY_METRICS end OneApm::Support::MethodTracer::Helpers.trace_execution_scoped_footer( state, start_time.to_f, name, summary_metrics_with_exclusive_time, outermost_frame, trace_options, end_time.to_f) OneApm::Agent::BusyCalculator.dispatcher_finish(end_time) commit!(state, end_time, name) unless @ignore_this_transaction end def commit!(state, end_time, outermost_segment_name) record_transaction_cpu(state) record_gc(state, end_time) sql_sampler.on_finishing_transaction(state, @frozen_name) record_summary_metrics(outermost_segment_name, end_time) record_apdex(state, end_time) unless ignore_apdex? record_queue_time record_exceptions merge_metrics send_transaction_finished_event(state, start_time, end_time) end # This event is fired when the transaction is fully completed. The metric # values and sampler can't be successfully modified from this event. def send_transaction_finished_event(state, start_time, end_time) duration = end_time.to_f - start_time.to_f payload = { :name => @frozen_name, :start_timestamp => start_time.to_f, :duration => duration, :metrics => @metrics, :custom_params => custom_parameters } append_cat_info(state, duration, payload) append_apdex_perf_zone(duration, payload) append_synthetics_to(state, payload) append_referring_transaction_guid_to(state, payload) append_http_response_code(payload) agent.events.notify(:transaction_finished, payload) end # Call this to ensure that the current transaction is not saved def abort_transaction!(state) transaction_sampler.ignore_transaction(state) end def record_transaction_cpu(state) burn = cpu_burn if burn transaction_sampler.notice_transaction_cpu_time(state, burn) end end def record_gc(state, end_time) gc_stop_snapshot = OneApm::Collector::StatsEngine::GCProfiler.take_snapshot gc_delta = OneApm::Collector::StatsEngine::GCProfiler.record_delta(gc_start_snapshot, gc_stop_snapshot) @transaction_trace = transaction_sampler.on_finishing_transaction(state, self, end_time, gc_delta) end # The summary metrics recorded by this method all end up with a duration # equal to the transaction itself, and an exclusive time of zero. def record_summary_metrics(outermost_segment_name, end_time) metrics = summary_metrics metrics << @frozen_name unless @frozen_name == outermost_segment_name @metrics.record_unscoped(metrics, end_time.to_f - start_time.to_f, 0) end def record_apdex(state, end_time=Time.now) return unless recording_web_transaction? && state.is_execution_traced? freeze_name_and_execute_if_not_ignored do action_duration = end_time - start_time total_duration = end_time - apdex_start apdex_bucket_global = apdex_bucket(total_duration) apdex_bucket_txn = apdex_bucket(action_duration) @metrics.record_unscoped(OA_APDEX_METRIC, apdex_bucket_global, apdex_t) txn_apdex_metric = @frozen_name.gsub(/^[^\/]+\//, 'Apdex/') @metrics.record_unscoped(txn_apdex_metric, apdex_bucket_txn, apdex_t) end end def record_queue_time value = queue_time if value > 0.0 if value < OneApm::Support::MethodTracer::Helpers::OA_MAX_ALLOWED_METRIC_DURATION @metrics.record_unscoped(OA_QUEUE_TIME_METRIC, value) else OneApm::Manager.logger.log_once(:warn, :too_high_queue_time, "Not recording unreasonably large queue time of #{value} s") end end end def record_exceptions @exceptions.each do |exception, options| options[:metric] = best_name agent.error_collector.notice_error(exception, options) end end def merge_metrics return if ignore_frame?(best_name) agent.stats_engine.merge_transaction_metrics!(@metrics, best_name) end def instrumentation_state @instrumentation_state ||= {} end def noticed_error_ids @noticed_error_ids ||= [] end def create_nested_frame(state, category, options) @has_children = true if options[:filtered_params] && !options[:filtered_params].empty? @filtered_params = options[:filtered_params] end frame_stack.push OneApm::Support::MethodTracer::Helpers.trace_execution_scoped_header(state, Time.now.to_f) name_last_frame(options[:transaction_name]) set_default_transaction_name(options[:transaction_name], category) end def user_defined_rules_ignore? return unless uri return if (rules = OneApm::Manager.config[:"rules.ignore_url_regexes"]).empty? parsed = OneApm::Support::HTTPClients::URIUtil.parse_url(uri) filtered_uri = OneApm::Support::HTTPClients::URIUtil.filter_uri(parsed) rules.any? do |rule| filtered_uri.match(rule) end rescue URI::InvalidURIError => e OneApm::Manager.logger.debug("Error parsing URI: #{uri}", e) false end def cat_trip_id(state) agent.cross_app_monitor.client_referring_transaction_trip_id(state) || guid end def cat_path_hash(state) referring_path_hash = cat_referring_path_hash(state) || '0' seed = referring_path_hash.to_i(16) result = agent.cross_app_monitor.path_hash(best_name, seed) record_cat_path_hash(result) result end def record_cat_path_hash(hash) @cat_path_hashes ||= [] if @cat_path_hashes.size < 10 && !@cat_path_hashes.include?(hash) @cat_path_hashes << hash end end def cat_referring_path_hash(state) agent.cross_app_monitor.client_referring_transaction_path_hash(state) end # Do not call this. Invoke the class method instead. def notice_error(error, options={}) # :nodoc: options[:uri] ||= uri if uri options[:referer] ||= referer if referer if filtered_params && !filtered_params.empty? options[:request_params] = filtered_params end options.merge!(custom_parameters) if @exceptions[error] @exceptions[error].merge! options else @exceptions[error] = options end end def ignore_frame? tx_name return false if ignore_frames.empty? ignore_frames.any?{|iframe| tx_name.to_s.match(/#{iframe}/)} end end end