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

#initializeTransactionSampler

Returns a new instance of TransactionSampler.



33
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
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 33

def initialize
  @xray_sample_buffer = OneApm::Transaction::XraySampleBuffer.new
  @dev_mode_sample_buffer = OneApm::Transaction::DeveloperModeSampleBuffer.new

  @sample_buffers = []
  @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

#dev_mode_sample_bufferObject (readonly)



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

def dev_mode_sample_buffer
  @dev_mode_sample_buffer
end

#last_sampleObject (readonly)



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

def last_sample
  @last_sample
end

#xray_sample_bufferObject (readonly)



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

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



196
197
198
199
200
201
202
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 196

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.



276
277
278
279
280
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 276

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



206
207
208
209
210
211
212
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 206

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



239
240
241
242
243
244
245
246
247
248
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 239

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



313
314
315
316
317
318
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 313

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



101
102
103
104
105
106
107
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 101

def custom_parameters_from_transaction(txn)
  if OneApm::Manager.config[:'transaction_tracer.capture_attributes']
    txn.custom_parameters
  else
    {}
  end
end

#enabled?Boolean

Returns:

  • (Boolean)


65
66
67
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 65

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

#harvest!Object

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



283
284
285
286
287
288
289
290
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 283

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



320
321
322
323
324
325
326
327
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 320

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



161
162
163
164
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 161

def ignore_transaction(state)
  builder = state.transaction_sample_builder
  builder.ignore_transaction if builder
end

#merge!(previous) ⇒ Object



305
306
307
308
309
310
311
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 305

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



265
266
267
268
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 265

def notice_nosql(key, duration)
  builder = tl_builder
  notice_extra_data(builder, key, duration, :key)
end

#notice_nosql_statement(statement, duration) ⇒ Object



270
271
272
273
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 270

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



94
95
96
97
98
99
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 94

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



82
83
84
85
86
87
88
89
90
91
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 82

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.



228
229
230
231
232
233
234
235
236
237
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 228

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



167
168
169
170
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 167

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`



116
117
118
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
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 116

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

  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



69
70
71
72
73
74
75
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 69

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



292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 292

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)



330
331
332
333
334
335
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 330

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



343
344
345
346
347
348
349
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 343

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



151
152
153
154
155
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 151

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



352
353
354
# File 'lib/one_apm/collector/containers/transaction_sampler.rb', line 352

def tl_builder
  OneApm::TransactionState.tl_get.transaction_sample_builder
end