# 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
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
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 = TRACE_OPTIONS_SCOPED
else
name = @frozen_name
trace_options = TRACE_OPTIONS_UNSCOPED
end
# 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 = MIDDLEWARE_SUMMARY_METRICS
else
summary_metrics_with_exclusive_time = 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(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::MAX_ALLOWED_METRIC_DURATION
@metrics.record_unscoped(QUEUE_TIME_METRIC, value)
else
::OneApm::Agent.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
agent.stats_engine.merge_transaction_metrics!(@metrics, best_name)
end
def instrumentation_state
@instrumentation_state ||= {}
end
def with_database_metric_name(model, method)
previous = self.instrumentation_state[:datastore_override]
model_name = case model
when Class
model.name
when String
model
else
model.to_s
end
self.instrumentation_state[:datastore_override] = [method, model_name]
yield
ensure
self.instrumentation_state[:datastore_override] = previous
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::Agent.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::Agent.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
# 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
end
end