class OneApm::Collector::TransactionSampler

This class contains the logic for recording and storing transaction traces (sometimes referred to as 'transaction samples').

A transaction trace is a detailed timeline of the events that happened during the processing of a single transaction, including database calls, template rendering calls, and other instrumented method calls.

@api public

Constants

OA_MAX_DATA_LENGTH

Attributes

cross_sample_buffer[R]
dev_mode_sample_buffer[R]
last_sample[R]
xray_sample_buffer[R]

Public Class Methods

new(events = OneApm::Manager.agent.events) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 34
def initialize(events = OneApm::Manager.agent.events)
  @xray_sample_buffer = OneApm::Transaction::XraySampleBuffer.new
  @dev_mode_sample_buffer = OneApm::Transaction::DeveloperModeSampleBuffer.new
  @cross_sample_buffer = OneApm::Transaction::CrossSampleBuffer.new(events)

  @sample_buffers = []
  @sample_buffers << @cross_sample_buffer
  @sample_buffers << @xray_sample_buffer
  @sample_buffers << @dev_mode_sample_buffer
  @sample_buffers << OneApm::Transaction::SlowestSampleBuffer.new
  @sample_buffers << OneApm::Transaction::SyntheticsSampleBuffer.new
  @sample_buffers << OneApm::Transaction::ForcePersistSampleBuffer.new

  # This lock is used to synchronize access to the @last_sample
  # and related variables. It can become necessary on JRuby or
  # any 'honest-to-god'-multithreaded system
  @samples_lock = Mutex.new

  OneApm::Manager.config.register_callback(:'transaction_tracer.enabled') do |enabled|
    if enabled
      threshold = OneApm::Manager.config[:'transaction_tracer.transaction_threshold']
      OneApm::Manager.logger.debug "Transaction tracing threshold is #{threshold} seconds."
    else
      OneApm::Manager.logger.debug "Transaction traces will not be sent to the OneApm service."
    end
  end

  OneApm::Manager.config.register_callback(:'transaction_tracer.record_sql') do |config|
    if config == 'raw'
      OneApm::Manager.logger.warn("Agent is configured to send raw SQL to the service")
    end
  end
end
truncate_message(message) click to toggle source

Truncates the message to `OA_MAX_DATA_LENGTH` if needed, and appends an ellipsis because it makes the trucation clearer in the UI

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 200
def self.truncate_message(message)
  if message.length > (OA_MAX_DATA_LENGTH - 4)
    message[0..OA_MAX_DATA_LENGTH - 4] + '...'
  else
    message
  end
end

Public Instance Methods

add_segment_parameters(params) click to toggle source

Set parameters on the current segment.

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 280
def add_segment_parameters(params)
  builder = tl_builder
  return unless builder
  params.each { |k,v| builder.current_segment[k] = v }
end
append_backtrace(segment, duration) click to toggle source

Appends a backtrace to a segment if that segment took longer than the specified duration

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 210
def append_backtrace(segment, duration)
  if duration >= OneApm::Manager.config[:'transaction_tracer.stack_trace_threshold']
    backtrace = caller
    backtrace.reject! { |t| t.include?('one_apm') }
    segment[:backtrace] = backtrace.join("\n")
  end
end
build_database_statement(sql, config, explainer) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 243
def build_database_statement(sql, config, explainer)
  statement = OneApm::Agent::Database::Statement.new(OneApm::Agent::Database.capture_query(sql))
  if config
    statement.adapter = config[:adapter]
    statement.config = config
  end
  statement.explainer = explainer

  statement
end
count() click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 317
def count
  @samples_lock.synchronize do
    samples = @sample_buffers.inject([]) { |all, b| all.concat(b.samples) }
    samples.uniq.size
  end
end
custom_parameters_from_transaction(txn) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 104
def custom_parameters_from_transaction(txn)
  if OneApm::Manager.config[:'transaction_tracer.capture_attributes']
    txn.custom_parameters
  else
    {}
  end
end
enabled?() click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 68
def enabled?
  OneApm::Manager.config[:'transaction_tracer.enabled']
end
harvest!() click to toggle source

Gather transaction traces that we'd like to transmit to the server.

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 287
def harvest!
  return [] unless enabled?
  samples = @samples_lock.synchronize do
    @last_sample = nil
    harvest_from_sample_buffers
  end
  prepare_samples(samples)
end
harvest_from_sample_buffers() click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 324
def harvest_from_sample_buffers
  # map + flatten hit mocking issues calling to_ary on 1.9.2.  We only
  # want a single level flatten anyway, but, as you probably already
  # know, Ruby 1.8.6 :/
  result = []
  @sample_buffers.each { |buffer| result.concat(buffer.harvest_samples) }
  result.uniq
end
ignore_transaction(state) click to toggle source

Tells the builder to ignore a transaction, if we are currently creating one. Only causes the sample to be ignored upon end of the transaction, and does not change the metrics gathered outside of the sampler

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 165
def ignore_transaction(state)
  builder = state.transaction_sample_builder
  builder.ignore_transaction if builder
end
merge!(previous) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 309
def merge!(previous)
  @samples_lock.synchronize do
    @sample_buffers.each do |buffer|
      buffer.store_previous(previous)
    end
  end
end
notice_nosql(key, duration) click to toggle source

Attaches an additional non-SQL query parameter to the current transaction trace segment.

This may be used for recording a query against a key-value store like memcached or redis.

This method should be used only by gem authors wishing to extend the Ruby agent to instrument uninstrumented key-value stores - it should generally not be called directly from application code.

@param key [String] the name of the key that was queried @param duration [Float] number of seconds the query took to execute

@api public

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 269
def notice_nosql(key, duration)
  builder = tl_builder
  notice_extra_data(builder, key, duration, :key)
end
notice_nosql_statement(statement, duration) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 274
def notice_nosql_statement(statement, duration)
  builder = tl_builder
  notice_extra_data(builder, statement, duration, :statement)
end
notice_pop_frame(state, frame, time = Time.now) click to toggle source

Informs the transaction sample builder about the end of a traced frame

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 97
def notice_pop_frame(state, frame, time = Time.now)
  builder = state.transaction_sample_builder
  return unless builder
  raise "finished already???" if builder.sample.finished
  builder.trace_exit(frame, time.to_f)
end
notice_push_frame(state, time=Time.now) click to toggle source

This delegates to the builder to create a new open transaction segment for the frame, beginning at the optionally specified time.

Note that in developer mode, this captures a stacktrace for the beginning of each segment, which can be fairly slow

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 85
def notice_push_frame(state, time=Time.now)
  builder = state.transaction_sample_builder
  return unless builder

  segment = builder.trace_entry(time.to_f)
  if @dev_mode_sample_buffer
    @dev_mode_sample_buffer.visit_segment(segment)
  end
  segment
end
notice_sql(sql, config, duration, state=nil, &explainer) click to toggle source

Attaches an SQL query on the current transaction trace segment.

This method should be used only by gem authors wishing to extend the Ruby agent to instrument new database interfaces - it should generally not be called directly from application code.

@param sql [String] the SQL query being recorded @param config [Object] the driver configuration for the connection @param duration [Float] number of seconds the query took to execute @param explainer [Proc] for internal use only - 3rd-party clients must

not pass this parameter.

@api public

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 232
def notice_sql(sql, config, duration, state=nil, &explainer)
  # some statements (particularly INSERTS with large BLOBS
  # may be very large; we should trim them to a maximum usable length
  state ||= OneApm::TransactionState.tl_get
  builder = state.transaction_sample_builder
  if state.is_sql_recorded?
    statement = build_database_statement(sql, config, explainer)
    notice_extra_data(builder, statement, duration, :sql)
  end
end
notice_transaction_cpu_time(state, cpu_time) click to toggle source

Sets the CPU time used by a transaction, delegates to the builder

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 171
def notice_transaction_cpu_time(state, cpu_time)
  builder = state.transaction_sample_builder
  builder.set_transaction_cpu_time(cpu_time) if builder
end
on_finishing_transaction(state, txn, time=Time.now, gc_time=nil) click to toggle source

This is called when we are done with the transaction. We've unwound the stack to the top level. It also clears the transaction sample builder so that it won't continue to have frames appended to it.

It sets various instance variables to the finished sample, depending on which settings are active. See `store_sample`

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 119
def on_finishing_transaction(state, txn, time=Time.now, gc_time=nil)
  last_builder = state.transaction_sample_builder
  return unless last_builder && enabled?

  state.transaction_sample_builder = nil
  return if last_builder.ignored?

  last_builder.set_request_params(txn.filtered_params)
  last_builder.set_transaction_name(txn.best_name)
  last_builder.finish_trace(time.to_f, custom_parameters_from_transaction(txn))

  last_sample = last_builder.sample
  last_sample.guid = txn.guid
  last_sample.set_custom_param(:gc_time, gc_time) if gc_time
  last_sample.force_persist = txn.apdex_bucket(time - txn.apdex_start) != :apdex_s

  if state.is_cross_app?
    last_sample.set_custom_param(:'bw.trip_id', txn.cat_trip_id(state))
    last_sample.set_custom_param(:'bw.path_hash', txn.cat_path_hash(state))
  end

  if txn.is_synthetics_request?
    last_sample.set_custom_param(:'bw.synthetics_resource_id', txn.synthetics_resource_id)
    last_sample.set_custom_param(:'bw.synthetics_job_id', txn.synthetics_job_id)
    last_sample.set_custom_param(:'bw.synthetics_monitor_id', txn.synthetics_monitor_id)

    last_sample.synthetics_resource_id = txn.synthetics_resource_id
  end

  @samples_lock.synchronize do
    @last_sample = last_sample
    store_sample(@last_sample)
    @last_sample
  end
end
on_start_transaction(state, start_time, uri=nil) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 72
def on_start_transaction(state, start_time, uri=nil)
  if enabled?
    start_builder(state, start_time.to_f)
    builder = state.transaction_sample_builder
    builder.set_transaction_uri(uri) if builder
  end
end
prepare_samples(samples) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 296
def prepare_samples(samples)
  samples.select do |sample|
    begin
      sample.prepare_to_send!
    rescue => e
      OneApm::Manager.logger.error("Failed to prepare transaction trace. Error: ", e)
      false
    else
      true
    end
  end
end
reset!() click to toggle source

reset samples without rebooting the web server (used by dev mode)

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 334
def reset!
  @samples_lock.synchronize do
    @last_sample = nil
    @sample_buffers.each { |sample_buffer| sample_buffer.reset! }
  end
end
start_builder(state, time=nil) click to toggle source

Checks to see if the transaction sampler is disabled, if transaction trace recording is disabled by a thread local, or if execution is untraced - if so it clears the transaction sample builder from the thread local, otherwise it generates a new transaction sample builder with the stated time as a starting point and saves it in the thread local variable

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 347
def start_builder(state, time=nil)
  if !enabled? || !state.is_transaction_traced? || !state.is_execution_traced?
    state.transaction_sample_builder = nil
  else
    state.transaction_sample_builder ||= OneApm::TransactionSampleBuilder.new(time)
  end
end
store_sample(sample) click to toggle source
# File lib/one_apm/collector/containers/transaction_sampler.rb, line 155
def store_sample(sample)
  @sample_buffers.each do |sample_buffer|
    sample_buffer.store(sample)
  end
end
tl_builder() click to toggle source

The current thread-local transaction sample builder

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 356
def tl_builder
  OneApm::TransactionState.tl_get.transaction_sample_builder
end

Private Instance Methods

notice_extra_data(builder, message, duration, key) click to toggle source

This method is used to record metadata into the currently active segment like a sql query, memcache key, or Net::HTTP uri

duration is seconds, float value.

# File lib/one_apm/collector/containers/transaction_sampler.rb, line 181
def notice_extra_data(builder, message, duration, key)
  return unless builder
  segment = builder.current_segment
  if segment
    if key != :sql
      segment[key] = self.class.truncate_message(message)
    else
      # message is expected to have been pre-truncated by notice_sql
      segment[key] = message
    end
    append_backtrace(segment, duration)
  end
end