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.

API:

  • public

Defined Under Namespace

Modules: Shim

Constant Summary collapse

OA_MAX_DATA_LENGTH =

API:

  • public

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.

API:

  • public



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)

API:

  • public



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)

API:

  • public



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)

API:

  • public



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

def last_sample
  @last_sample
end

#xray_sample_bufferObject (readonly)

API:

  • public



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

API:

  • public



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.

API:

  • public



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

API:

  • public



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

API:

  • public



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

API:

  • public



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

API:

  • public



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

API:

  • public



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.

API:

  • public



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

API:

  • public



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

API:

  • public



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

API:

  • public



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.

API:

  • public



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

API:

  • public



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

API:

  • public



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

API:

  • public



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.

API:

  • public



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

API:

  • public



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`

API:

  • public



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

API:

  • public



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

API:

  • public



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)

API:

  • public



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

API:

  • public



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

API:

  • public



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

API:

  • public



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