Class: OneApm::Collector::TransactionSampler

Inherits:
Object
  • Object
show all
Defined in:
lib/one_apm/collector/containers/transaction_sampler.rb

Overview

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.

Defined Under Namespace

Modules: Shim

Constant Summary collapse

OA_MAX_DATA_LENGTH =
16384

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(events = OneApm::Manager.agent.events) ⇒ TransactionSampler

Returns a new instance of TransactionSampler.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 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

Instance Attribute Details

#cross_sample_bufferObject (readonly)



32
33
34
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 32

def cross_sample_buffer
  @cross_sample_buffer
end

#dev_mode_sample_bufferObject (readonly)



32
33
34
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 32

def dev_mode_sample_buffer
  @dev_mode_sample_buffer
end

#last_sampleObject (readonly)



32
33
34
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 32

def last_sample
  @last_sample
end

#xray_sample_bufferObject (readonly)



32
33
34
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 32

def xray_sample_buffer
  @xray_sample_buffer
end

Class Method Details

.truncate_message(message) ⇒ Object

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



200
201
202
203
204
205
206
# 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

Instance Method Details

#add_segment_parameters(params) ⇒ Object

Set parameters on the current segment.



280
281
282
283
284
# 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) ⇒ Object

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



210
211
212
213
214
215
216
# 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) ⇒ Object



243
244
245
246
247
248
249
250
251
252
# 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

#countObject



317
318
319
320
321
322
# 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) ⇒ Object



104
105
106
107
108
109
110
# 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?Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 68

def enabled?
  OneApm::Manager.config[:'transaction_tracer.enabled']
end

#harvest!Object

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



287
288
289
290
291
292
293
294
# 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_buffersObject



324
325
326
327
328
329
330
331
# 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) ⇒ Object

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



165
166
167
168
# 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) ⇒ Object



309
310
311
312
313
314
315
# 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) ⇒ Object

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.

Parameters:

  • key (String)

    the name of the key that was queried

  • duration (Float)

    number of seconds the query took to execute



269
270
271
272
# 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) ⇒ Object



274
275
276
277
# 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) ⇒ Object

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



97
98
99
100
101
102
# 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) ⇒ Object

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



85
86
87
88
89
90
91
92
93
94
# 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) ⇒ Object

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.

Parameters:

  • sql (String)

    the SQL query being recorded

  • config (Object)

    the driver configuration for the connection

  • duration (Float)

    number of seconds the query took to execute

  • explainer (Proc)

    for internal use only - 3rd-party clients must not pass this parameter.



232
233
234
235
236
237
238
239
240
241
# 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) ⇒ Object

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



171
172
173
174
# 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) ⇒ Object

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`



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 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) ⇒ Object



72
73
74
75
76
77
78
# 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) ⇒ Object



296
297
298
299
300
301
302
303
304
305
306
307
# 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!Object

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



334
335
336
337
338
339
# 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) ⇒ Object

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



347
348
349
350
351
352
353
# 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) ⇒ Object



155
156
157
158
159
# 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_builderObject

The current thread-local transaction sample builder



356
357
358
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 356

def tl_builder
  OneApm::TransactionState.tl_get.transaction_sample_builder
end