appsignal 4.10.1-java → 5.0.0.rc.1-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Rakefile +87 -0
- data/appsignal.gemspec +8 -0
- data/build_matrix.yml +14 -1
- data/ext/appsignal_extension.c +14 -0
- data/lib/appsignal/backends.rb +55 -0
- data/lib/appsignal/config.rb +140 -1
- data/lib/appsignal/demo.rb +1 -1
- data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
- data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
- data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
- data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
- data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
- data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
- data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
- data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
- data/lib/appsignal/event_formatter.rb +78 -0
- data/lib/appsignal/extension.rb +4 -0
- data/lib/appsignal/helpers/instrumentation.rb +236 -20
- data/lib/appsignal/helpers/metrics.rb +3 -24
- data/lib/appsignal/hooks/action_cable.rb +18 -6
- data/lib/appsignal/hooks/active_job.rb +128 -39
- data/lib/appsignal/hooks/at_exit.rb +4 -1
- data/lib/appsignal/hooks/excon.rb +20 -0
- data/lib/appsignal/hooks/faraday.rb +16 -0
- data/lib/appsignal/hooks/http.rb +5 -0
- data/lib/appsignal/hooks/resque.rb +1 -1
- data/lib/appsignal/hooks/sequel.rb +32 -2
- data/lib/appsignal/hooks/shoryuken.rb +3 -3
- data/lib/appsignal/hooks/sidekiq.rb +1 -1
- data/lib/appsignal/integrations/action_cable.rb +5 -2
- data/lib/appsignal/integrations/active_support_notifications.rb +59 -14
- data/lib/appsignal/integrations/data_mapper.rb +14 -2
- data/lib/appsignal/integrations/delayed_job_plugin.rb +45 -5
- data/lib/appsignal/integrations/dry_monitor.rb +39 -15
- data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
- data/lib/appsignal/integrations/excon.rb +52 -15
- data/lib/appsignal/integrations/faraday.rb +47 -12
- data/lib/appsignal/integrations/http.rb +43 -1
- data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
- data/lib/appsignal/integrations/net_http.rb +31 -2
- data/lib/appsignal/integrations/puma.rb +4 -1
- data/lib/appsignal/integrations/que.rb +224 -37
- data/lib/appsignal/integrations/railtie.rb +4 -1
- data/lib/appsignal/integrations/rake.rb +9 -3
- data/lib/appsignal/integrations/redis.rb +22 -1
- data/lib/appsignal/integrations/redis_client.rb +22 -1
- data/lib/appsignal/integrations/resque.rb +57 -10
- data/lib/appsignal/integrations/shoryuken.rb +140 -12
- data/lib/appsignal/integrations/sidekiq.rb +73 -16
- data/lib/appsignal/integrations/webmachine.rb +47 -4
- data/lib/appsignal/loaders/padrino.rb +2 -1
- data/lib/appsignal/logger/extension_backend.rb +24 -0
- data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
- data/lib/appsignal/logger.rb +13 -9
- data/lib/appsignal/metrics/extension_backend.rb +47 -0
- data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
- data/lib/appsignal/opentelemetry/attributes.rb +31 -0
- data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
- data/lib/appsignal/opentelemetry/error_type.rb +37 -0
- data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
- data/lib/appsignal/opentelemetry/http_method.rb +59 -0
- data/lib/appsignal/opentelemetry/http_response.rb +30 -0
- data/lib/appsignal/opentelemetry/http_server_request.rb +45 -0
- data/lib/appsignal/opentelemetry/messaging.rb +82 -0
- data/lib/appsignal/opentelemetry/rendering.rb +29 -0
- data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
- data/lib/appsignal/opentelemetry.rb +337 -0
- data/lib/appsignal/rack/abstract_middleware.rb +63 -4
- data/lib/appsignal/rack/body_wrapper.rb +18 -5
- data/lib/appsignal/rack/event_handler.rb +36 -3
- data/lib/appsignal/rack/grape_middleware.rb +1 -0
- data/lib/appsignal/rack/hanami_middleware.rb +2 -1
- data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
- data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
- data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
- data/lib/appsignal/rack.rb +39 -11
- data/lib/appsignal/sample_data.rb +4 -0
- data/lib/appsignal/transaction/base_backend.rb +107 -0
- data/lib/appsignal/transaction/extension_backend.rb +203 -0
- data/lib/appsignal/transaction/opentelemetry_backend.rb +796 -0
- data/lib/appsignal/transaction.rb +537 -144
- data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +9 -0
- data/sig/appsignal.rbi +426 -37
- data/sig/appsignal.rbs +386 -27
- metadata +23 -1
|
@@ -22,14 +22,37 @@ module Appsignal
|
|
|
22
22
|
ERROR_CAUSES_LIMIT = 10
|
|
23
23
|
# @!visibility private
|
|
24
24
|
ERRORS_LIMIT = 10
|
|
25
|
+
# Guards the process-wide `add_params`/`set_params` deprecation warn-once
|
|
26
|
+
# flag, which transactions touch concurrently on threaded servers. A
|
|
27
|
+
# constant so it is created once at load time rather than lazily.
|
|
28
|
+
# @!visibility private
|
|
29
|
+
PARAMS_DEPRECATION_LOCK = Mutex.new
|
|
25
30
|
|
|
26
31
|
class << self
|
|
27
32
|
# Create a new transaction and set it as the currently active
|
|
28
33
|
# transaction.
|
|
29
34
|
#
|
|
30
35
|
# @param namespace [String] Namespace of the to be created transaction.
|
|
36
|
+
# @param opentelemetry_kind [Symbol] In collector mode, the OpenTelemetry
|
|
37
|
+
# span kind: one of `:server`, `:consumer`, `:producer` or `:internal`.
|
|
38
|
+
# Defaults to `:server`.
|
|
39
|
+
# @param opentelemetry_relationship [Symbol] In collector mode, how an
|
|
40
|
+
# incoming `opentelemetry_context` relates to this transaction's span:
|
|
41
|
+
# one of `:parent`, `:link`, `:both` or `:none`. Defaults to `:parent`.
|
|
42
|
+
# @param opentelemetry_context In collector mode, an incoming OpenTelemetry
|
|
43
|
+
# trace context to relate this transaction's span to.
|
|
44
|
+
# @param opentelemetry_scope [Array(String, String)] In collector mode, the
|
|
45
|
+
# OpenTelemetry instrumentation scope to record this transaction's spans
|
|
46
|
+
# under, given as a `[name, version]` pair. Defaults to the AppSignal
|
|
47
|
+
# scope.
|
|
31
48
|
# @return [Transaction]
|
|
32
|
-
def create(
|
|
49
|
+
def create(
|
|
50
|
+
namespace,
|
|
51
|
+
opentelemetry_context: nil,
|
|
52
|
+
opentelemetry_scope: nil,
|
|
53
|
+
opentelemetry_kind: nil,
|
|
54
|
+
opentelemetry_relationship: nil
|
|
55
|
+
)
|
|
33
56
|
# Reset the transaction if it was already completed but not cleared
|
|
34
57
|
if Thread.current[:appsignal_transaction]&.completed?
|
|
35
58
|
Thread.current[:appsignal_transaction] = nil
|
|
@@ -37,7 +60,15 @@ module Appsignal
|
|
|
37
60
|
|
|
38
61
|
if Thread.current[:appsignal_transaction].nil?
|
|
39
62
|
# If not, start a new transaction
|
|
40
|
-
set_current_transaction(
|
|
63
|
+
set_current_transaction(
|
|
64
|
+
Appsignal::Transaction.new(
|
|
65
|
+
namespace,
|
|
66
|
+
:opentelemetry_context => opentelemetry_context,
|
|
67
|
+
:opentelemetry_scope => opentelemetry_scope,
|
|
68
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
69
|
+
:opentelemetry_relationship => opentelemetry_relationship
|
|
70
|
+
)
|
|
71
|
+
)
|
|
41
72
|
else
|
|
42
73
|
transaction = current
|
|
43
74
|
# Otherwise, log the issue about trying to start another transaction
|
|
@@ -150,6 +181,26 @@ module Appsignal
|
|
|
150
181
|
|
|
151
182
|
# @!visibility private
|
|
152
183
|
attr_writer :last_errors
|
|
184
|
+
|
|
185
|
+
# Runs the block to emit the collector-mode `add_params`/`set_params`
|
|
186
|
+
# deprecation warning the first time it is called in the process, then
|
|
187
|
+
# stays quiet. The check-and-set is done under a lock so concurrent
|
|
188
|
+
# transactions on threaded runtimes don't race and warn more than once.
|
|
189
|
+
# @!visibility private
|
|
190
|
+
def warn_params_deprecation_once
|
|
191
|
+
should_warn = PARAMS_DEPRECATION_LOCK.synchronize do
|
|
192
|
+
next false if @params_deprecation_warned
|
|
193
|
+
|
|
194
|
+
@params_deprecation_warned = true
|
|
195
|
+
end
|
|
196
|
+
yield if should_warn
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# @!visibility private
|
|
200
|
+
# Resets the warn-once state. Only used to keep test runs isolated.
|
|
201
|
+
def reset_params_deprecation_warning!
|
|
202
|
+
PARAMS_DEPRECATION_LOCK.synchronize { @params_deprecation_warned = false }
|
|
203
|
+
end
|
|
153
204
|
end
|
|
154
205
|
|
|
155
206
|
# @!visibility private
|
|
@@ -160,7 +211,15 @@ module Appsignal
|
|
|
160
211
|
# @param namespace [String] Namespace of the to be created transaction.
|
|
161
212
|
# @see create
|
|
162
213
|
# @!visibility private
|
|
163
|
-
def initialize(
|
|
214
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
215
|
+
namespace,
|
|
216
|
+
id: SecureRandom.uuid,
|
|
217
|
+
backend: nil,
|
|
218
|
+
opentelemetry_context: nil,
|
|
219
|
+
opentelemetry_scope: nil,
|
|
220
|
+
opentelemetry_kind: nil,
|
|
221
|
+
opentelemetry_relationship: nil
|
|
222
|
+
)
|
|
164
223
|
@transaction_id = id
|
|
165
224
|
@action = nil
|
|
166
225
|
@namespace = namespace
|
|
@@ -168,22 +227,47 @@ module Appsignal
|
|
|
168
227
|
@discarded = false
|
|
169
228
|
@completed = false
|
|
170
229
|
@tags = {}
|
|
171
|
-
@breadcrumbs = []
|
|
172
230
|
@store = Hash.new { |hash, key| hash[key] = {} }
|
|
231
|
+
# The distinct errors added to this transaction, in add order. Drives
|
|
232
|
+
# `last_errors`, the dedup check and the `ERRORS_LIMIT`, in both modes.
|
|
233
|
+
# A Set, so membership is by object identity (`eql?`/`hash`), matching how
|
|
234
|
+
# these errors used to be deduplicated as Hash keys. `Exception#==` would
|
|
235
|
+
# instead collapse distinct errors with the same class, message and
|
|
236
|
+
# backtrace, which we don't want.
|
|
237
|
+
@errors = Set.new
|
|
238
|
+
# Blocks per error, only populated in agent mode, where they run against
|
|
239
|
+
# the duplicate transactions at completion. Collector mode runs blocks
|
|
240
|
+
# when the error is added and never touches this.
|
|
173
241
|
@error_blocks = Hash.new { |hash, key| hash[key] = [] }
|
|
174
242
|
@is_duplicate = false
|
|
175
243
|
@error_set = nil
|
|
176
244
|
|
|
177
|
-
@params = Appsignal::SampleData.new(:params)
|
|
178
245
|
@session_data = Appsignal::SampleData.new(:session_data, Hash)
|
|
179
246
|
@headers = Appsignal::SampleData.new(:headers, Hash)
|
|
180
247
|
@custom_data = Appsignal::SampleData.new(:custom_data)
|
|
181
248
|
|
|
182
|
-
@
|
|
249
|
+
@backend = backend || Appsignal::Backends.transaction.new(
|
|
183
250
|
@transaction_id,
|
|
184
251
|
@namespace,
|
|
185
|
-
|
|
186
|
-
|
|
252
|
+
:opentelemetry_context => opentelemetry_context,
|
|
253
|
+
:opentelemetry_scope => opentelemetry_scope,
|
|
254
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
255
|
+
:opentelemetry_relationship => opentelemetry_relationship
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# The backend decides how the params channels are stored. Its
|
|
259
|
+
# `params_mapping` maps each logical channel to a storage bucket: the
|
|
260
|
+
# extension backend maps them all to one `:params` bucket, so agent mode
|
|
261
|
+
# keeps a single merged blob, while the OpenTelemetry backend keeps the
|
|
262
|
+
# request payload, function parameters and query parameters apart.
|
|
263
|
+
#
|
|
264
|
+
# Each distinct bucket gets its own `SampleData`, named after the bucket.
|
|
265
|
+
# That symbol is also the sample-data key the backend receives, so it can
|
|
266
|
+
# route the bucket to the right storage.
|
|
267
|
+
@params_mapping = @backend.params_mapping
|
|
268
|
+
@params_buckets = @params_mapping.values.uniq.to_h do |bucket|
|
|
269
|
+
[bucket, Appsignal::SampleData.new(bucket)]
|
|
270
|
+
end
|
|
187
271
|
|
|
188
272
|
run_after_create_hooks
|
|
189
273
|
end
|
|
@@ -205,9 +289,22 @@ module Appsignal
|
|
|
205
289
|
|
|
206
290
|
# @!visibility private
|
|
207
291
|
def complete
|
|
292
|
+
# Completing is idempotent: a transaction can be completed explicitly and
|
|
293
|
+
# then again by a `complete_current!` cleanup path. Re-running would, for a
|
|
294
|
+
# multi-error transaction, re-record the extra errors (a second duplicate
|
|
295
|
+
# in agent mode, or an event on an already-finished span in collector mode).
|
|
296
|
+
return if completed?
|
|
297
|
+
|
|
208
298
|
if discarded?
|
|
209
299
|
Appsignal.internal_logger.debug "Skipping transaction '#{transaction_id}' " \
|
|
210
300
|
"because it was manually discarded."
|
|
301
|
+
# Let the backend tear itself down. The agent backend drops the
|
|
302
|
+
# transaction (nothing is sent); the OpenTelemetry backend still
|
|
303
|
+
# finishes and exports the root span, but flags it with
|
|
304
|
+
# `appsignal.ignore_subtrace` so the collector ignores the subtrace.
|
|
305
|
+
# `@completed` stays false either way: a discarded transaction was
|
|
306
|
+
# never reported.
|
|
307
|
+
@backend.discard
|
|
211
308
|
return
|
|
212
309
|
end
|
|
213
310
|
|
|
@@ -219,40 +316,18 @@ module Appsignal
|
|
|
219
316
|
should_sample = true
|
|
220
317
|
|
|
221
318
|
unless duplicate?
|
|
222
|
-
self.class.last_errors = @
|
|
223
|
-
should_sample = @
|
|
319
|
+
self.class.last_errors = @errors.to_a
|
|
320
|
+
should_sample = @backend.finish
|
|
224
321
|
end
|
|
225
322
|
|
|
226
|
-
|
|
227
|
-
# Ignore the error that is already set in this transaction.
|
|
228
|
-
next if error == @error_set
|
|
229
|
-
|
|
230
|
-
duplicate.tap do |transaction|
|
|
231
|
-
# In the duplicate transaction for each error, set an error
|
|
232
|
-
# with a block that calls all the blocks set for that error
|
|
233
|
-
# in the original transaction.
|
|
234
|
-
transaction.internal_set_error(error) do
|
|
235
|
-
blocks.each { |block| block.call(transaction) }
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
transaction.complete
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
if @error_set && @error_blocks[@error_set].any?
|
|
243
|
-
self.class.with_transaction(self) do
|
|
244
|
-
@error_blocks[@error_set].each do |block|
|
|
245
|
-
block.call(self)
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
323
|
+
report_errors
|
|
249
324
|
|
|
250
325
|
run_before_complete_hooks
|
|
251
326
|
|
|
252
327
|
sample_data if should_sample
|
|
253
328
|
|
|
254
329
|
@completed = true
|
|
255
|
-
@
|
|
330
|
+
@backend.complete
|
|
256
331
|
end
|
|
257
332
|
|
|
258
333
|
# @!visibility private
|
|
@@ -328,12 +403,13 @@ module Appsignal
|
|
|
328
403
|
end
|
|
329
404
|
|
|
330
405
|
# @!visibility private
|
|
406
|
+
#
|
|
407
|
+
# True when an outer integration (Active Job) is already recording this
|
|
408
|
+
# enqueue. Nested integrations use it to skip their own enqueue event, but
|
|
409
|
+
# they must still propagate trace context: the outer integration's producer
|
|
410
|
+
# span is what the performing job links back to, and only the nested
|
|
411
|
+
# integration owns the carrier that job travels on.
|
|
331
412
|
def job_enqueue_events_suppressed?
|
|
332
|
-
# When enqueue instrumentation is disabled, every enqueue integration
|
|
333
|
-
# treats its event as suppressed. That is how the config option turns the
|
|
334
|
-
# enqueue events off across all integrations at once.
|
|
335
|
-
return true if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
|
|
336
|
-
|
|
337
413
|
store("job_enqueue")[:suppressed] == true
|
|
338
414
|
end
|
|
339
415
|
|
|
@@ -356,17 +432,28 @@ module Appsignal
|
|
|
356
432
|
# @see https://docs.appsignal.com/guides/custom-data/sample-data.html
|
|
357
433
|
# Sample data guide
|
|
358
434
|
def add_params(given_params = nil, &block)
|
|
359
|
-
|
|
435
|
+
warn_params_deprecation
|
|
436
|
+
params_data(:params).add(given_params, &block)
|
|
360
437
|
end
|
|
361
438
|
alias set_params add_params
|
|
362
439
|
|
|
440
|
+
# Marks every params channel as explicitly empty, so no parameters are
|
|
441
|
+
# reported for this transaction whatever their source.
|
|
442
|
+
#
|
|
443
|
+
# This is a deliberate choice for collector mode, where the request payload
|
|
444
|
+
# and the function parameters live in separate buckets: it empties both, not
|
|
445
|
+
# only the request payload the legacy `:params` channel maps to. Otherwise
|
|
446
|
+
# an integration's `_if_nil` setter could still add function parameters (a
|
|
447
|
+
# background job's arguments) after params were emptied, and the behavior
|
|
448
|
+
# would differ from agent mode, where all channels share one bucket.
|
|
449
|
+
#
|
|
363
450
|
# @since 4.0.0
|
|
364
451
|
# @return [void]
|
|
365
452
|
# @!visibility private
|
|
366
453
|
#
|
|
367
454
|
# @see Helpers::Instrumentation#set_empty_params!
|
|
368
455
|
def set_empty_params!
|
|
369
|
-
@
|
|
456
|
+
@params_buckets.each_value(&:set_empty_value!)
|
|
370
457
|
end
|
|
371
458
|
|
|
372
459
|
# Add parameters to the transaction if not already set.
|
|
@@ -382,10 +469,119 @@ module Appsignal
|
|
|
382
469
|
#
|
|
383
470
|
# @see #add_params
|
|
384
471
|
def add_params_if_nil(given_params = nil, &block)
|
|
385
|
-
add_params(given_params, &block) if
|
|
472
|
+
add_params(given_params, &block) if params_unset?(:params)
|
|
386
473
|
end
|
|
387
474
|
alias set_params_if_nil add_params_if_nil
|
|
388
475
|
|
|
476
|
+
# Add the request payload to the transaction.
|
|
477
|
+
#
|
|
478
|
+
# These are the parameters of an incoming request, such as the query string
|
|
479
|
+
# and the request body. In collector mode they map to the request payload
|
|
480
|
+
# attribute. In agent mode they are the transaction's params.
|
|
481
|
+
#
|
|
482
|
+
# Behaves like {#add_params}: merges when called multiple times, and a
|
|
483
|
+
# block takes precedence over the argument.
|
|
484
|
+
#
|
|
485
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
486
|
+
# transaction.
|
|
487
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
488
|
+
# return value will become the new parameters.
|
|
489
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
490
|
+
# @return [void]
|
|
491
|
+
#
|
|
492
|
+
# @see #add_function_parameters
|
|
493
|
+
def add_request_payload(given_params = nil, &block)
|
|
494
|
+
params_data(:request_payload).add(given_params, &block)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Add the request payload to the transaction if not already set.
|
|
498
|
+
#
|
|
499
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
500
|
+
# transaction if none are already set.
|
|
501
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
502
|
+
# return value will become the new parameters.
|
|
503
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
504
|
+
# @return [void]
|
|
505
|
+
# @!visibility private
|
|
506
|
+
#
|
|
507
|
+
# @see #add_request_payload
|
|
508
|
+
def add_request_payload_if_nil(given_params = nil, &block)
|
|
509
|
+
add_request_payload(given_params, &block) if params_unset?(:request_payload)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Add the function parameters to the transaction.
|
|
513
|
+
#
|
|
514
|
+
# These are the arguments a background job or function was called with. In
|
|
515
|
+
# collector mode they map to the function parameters attribute. In agent
|
|
516
|
+
# mode they are the transaction's params.
|
|
517
|
+
#
|
|
518
|
+
# Behaves like {#add_params}: merges when called multiple times, and a
|
|
519
|
+
# block takes precedence over the argument.
|
|
520
|
+
#
|
|
521
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
522
|
+
# transaction.
|
|
523
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
524
|
+
# return value will become the new parameters.
|
|
525
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
526
|
+
# @return [void]
|
|
527
|
+
#
|
|
528
|
+
# @see #add_request_payload
|
|
529
|
+
def add_function_parameters(given_params = nil, &block)
|
|
530
|
+
params_data(:function_parameters).add(given_params, &block)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Add the function parameters to the transaction if not already set.
|
|
534
|
+
#
|
|
535
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
536
|
+
# transaction if none are already set.
|
|
537
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
538
|
+
# return value will become the new parameters.
|
|
539
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
540
|
+
# @return [void]
|
|
541
|
+
# @!visibility private
|
|
542
|
+
#
|
|
543
|
+
# @see #add_function_parameters
|
|
544
|
+
def add_function_parameters_if_nil(given_params = nil, &block)
|
|
545
|
+
add_function_parameters(given_params, &block) if params_unset?(:function_parameters)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Add the query parameters to the transaction.
|
|
549
|
+
#
|
|
550
|
+
# These are the parameters parsed from an incoming request's query string.
|
|
551
|
+
# In collector mode they map to their own attribute, separate from the
|
|
552
|
+
# request payload and the function parameters. In agent mode they are the
|
|
553
|
+
# transaction's params.
|
|
554
|
+
#
|
|
555
|
+
# Behaves like {#add_params}: merges when called multiple times, and a
|
|
556
|
+
# block takes precedence over the argument.
|
|
557
|
+
#
|
|
558
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
559
|
+
# transaction.
|
|
560
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
561
|
+
# return value will become the new parameters.
|
|
562
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
563
|
+
# @return [void]
|
|
564
|
+
#
|
|
565
|
+
# @see #add_request_payload
|
|
566
|
+
def add_query_parameters(given_params = nil, &block)
|
|
567
|
+
params_data(:query_parameters).add(given_params, &block)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Add the query parameters to the transaction if not already set.
|
|
571
|
+
#
|
|
572
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
573
|
+
# transaction if none are already set.
|
|
574
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
575
|
+
# return value will become the new parameters.
|
|
576
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
577
|
+
# @return [void]
|
|
578
|
+
# @!visibility private
|
|
579
|
+
#
|
|
580
|
+
# @see #add_query_parameters
|
|
581
|
+
def add_query_parameters_if_nil(given_params = nil, &block)
|
|
582
|
+
add_query_parameters(given_params, &block) if params_unset?(:query_parameters)
|
|
583
|
+
end
|
|
584
|
+
|
|
389
585
|
# Add tags to the transaction.
|
|
390
586
|
#
|
|
391
587
|
# When this method is called multiple times, it will merge the tags.
|
|
@@ -524,14 +720,16 @@ module Appsignal
|
|
|
524
720
|
return
|
|
525
721
|
end
|
|
526
722
|
|
|
527
|
-
|
|
723
|
+
# The backend owns how breadcrumbs are stored: the agent backend buffers
|
|
724
|
+
# them and flushes at completion, the OpenTelemetry backend emits each as a
|
|
725
|
+
# span event right away (by completion its target span has finished).
|
|
726
|
+
@backend.add_breadcrumb(
|
|
528
727
|
:time => time.to_i,
|
|
529
728
|
:category => category,
|
|
530
729
|
:action => action,
|
|
531
730
|
:message => message,
|
|
532
731
|
:metadata => metadata
|
|
533
732
|
)
|
|
534
|
-
@breadcrumbs = @breadcrumbs.last(BREADCRUMB_LIMIT)
|
|
535
733
|
end
|
|
536
734
|
|
|
537
735
|
# Set an action name for the transaction.
|
|
@@ -548,7 +746,7 @@ module Appsignal
|
|
|
548
746
|
return unless action
|
|
549
747
|
|
|
550
748
|
@action = action
|
|
551
|
-
@
|
|
749
|
+
@backend.set_action(action)
|
|
552
750
|
end
|
|
553
751
|
|
|
554
752
|
# Set an action name only if there is no current action set.
|
|
@@ -596,7 +794,7 @@ module Appsignal
|
|
|
596
794
|
return unless namespace
|
|
597
795
|
|
|
598
796
|
@namespace = namespace
|
|
599
|
-
@
|
|
797
|
+
@backend.set_namespace(namespace)
|
|
600
798
|
end
|
|
601
799
|
|
|
602
800
|
# Set queue start time for transaction.
|
|
@@ -610,7 +808,7 @@ module Appsignal
|
|
|
610
808
|
def set_queue_start(start)
|
|
611
809
|
return unless start
|
|
612
810
|
|
|
613
|
-
@
|
|
811
|
+
@backend.set_queue_start(start)
|
|
614
812
|
rescue RangeError
|
|
615
813
|
Appsignal.internal_logger.warn("Queue start value #{start} is too big")
|
|
616
814
|
end
|
|
@@ -620,12 +818,47 @@ module Appsignal
|
|
|
620
818
|
return unless key && value
|
|
621
819
|
return if Appsignal.config[:filter_metadata].include?(key.to_s)
|
|
622
820
|
|
|
623
|
-
@
|
|
821
|
+
@backend.set_metadata(key, value)
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Add OpenTelemetry attributes to the span AppSignal is currently
|
|
825
|
+
# recording.
|
|
826
|
+
#
|
|
827
|
+
# In collector mode, AppSignal records a transaction as an OpenTelemetry
|
|
828
|
+
# span, and every instrumented event as a child span. This adds attributes
|
|
829
|
+
# to whichever of those spans is open right now: the innermost event
|
|
830
|
+
# started by {Appsignal::Helpers::Instrumentation#instrument}, or the
|
|
831
|
+
# transaction's own span when no event is open.
|
|
832
|
+
#
|
|
833
|
+
# Use this to describe what is being instrumented in OpenTelemetry's own
|
|
834
|
+
# terms, following the OpenTelemetry semantic conventions where they apply.
|
|
835
|
+
# Attributes have no equivalent outside collector mode, so this does
|
|
836
|
+
# nothing when collector mode is not active.
|
|
837
|
+
#
|
|
838
|
+
# @example Describing a database query
|
|
839
|
+
# Appsignal.instrument("query.my_database") do
|
|
840
|
+
# Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
841
|
+
# "db.system.name" => "mysql"
|
|
842
|
+
# )
|
|
843
|
+
# run_the_query
|
|
844
|
+
# end
|
|
845
|
+
#
|
|
846
|
+
# @param attributes [Hash<String, Object>, nil] Attributes to add to the
|
|
847
|
+
# current span. Values that are not a String, Integer, Float or boolean
|
|
848
|
+
# are converted to a String. Nothing is added when this is nil or empty.
|
|
849
|
+
# @return [void]
|
|
850
|
+
#
|
|
851
|
+
# @see https://opentelemetry.io/docs/specs/semconv/
|
|
852
|
+
# OpenTelemetry semantic conventions
|
|
853
|
+
def add_opentelemetry_attributes(attributes = {})
|
|
854
|
+
return if attributes.nil? || attributes.empty?
|
|
855
|
+
|
|
856
|
+
@backend.set_attributes(attributes)
|
|
624
857
|
end
|
|
625
858
|
|
|
626
859
|
# @!visibility private
|
|
627
860
|
# @see Appsignal::Helpers::Instrumentation#report_error
|
|
628
|
-
def add_error(error, &block)
|
|
861
|
+
def add_error(error, source: nil, &block)
|
|
629
862
|
unless error.is_a?(Exception)
|
|
630
863
|
Appsignal.internal_logger.error "Appsignal::Transaction#add_error: Cannot add error. " \
|
|
631
864
|
"The given value is not an exception: #{error.inspect}"
|
|
@@ -635,10 +868,15 @@ module Appsignal
|
|
|
635
868
|
return unless error
|
|
636
869
|
return unless Appsignal.active?
|
|
637
870
|
|
|
638
|
-
if error.instance_variable_get(:@__appsignal_error_reported) && !@
|
|
871
|
+
if error.instance_variable_get(:@__appsignal_error_reported) && !@errors.include?(error)
|
|
639
872
|
return
|
|
640
873
|
end
|
|
641
874
|
|
|
875
|
+
# Wrap the block here, at the entry point, so it stays protected wherever
|
|
876
|
+
# it later runs: right away in collector mode, or at completion in agent
|
|
877
|
+
# mode. `source` names the helper the block was given to, when known, so
|
|
878
|
+
# the log points the customer at the right call.
|
|
879
|
+
block = protect(source ? "the block passed to #{source}" : "the error block", &block) if block
|
|
642
880
|
internal_set_error(error, &block)
|
|
643
881
|
|
|
644
882
|
# Mark errors and their causes as tracked so we don't report duplicates,
|
|
@@ -653,10 +891,13 @@ module Appsignal
|
|
|
653
891
|
|
|
654
892
|
# @!visibility private
|
|
655
893
|
# @see Helpers::Instrumentation#instrument
|
|
656
|
-
def start_event
|
|
894
|
+
def start_event(opentelemetry_kind: nil, opentelemetry_scope: nil)
|
|
657
895
|
return if paused?
|
|
658
896
|
|
|
659
|
-
@
|
|
897
|
+
@backend.start_event(
|
|
898
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
899
|
+
:opentelemetry_scope => opentelemetry_scope
|
|
900
|
+
)
|
|
660
901
|
end
|
|
661
902
|
|
|
662
903
|
# @!visibility private
|
|
@@ -664,98 +905,280 @@ module Appsignal
|
|
|
664
905
|
def finish_event(name, title, body, body_format = Appsignal::EventFormatter::DEFAULT)
|
|
665
906
|
return if paused?
|
|
666
907
|
|
|
667
|
-
@
|
|
908
|
+
@backend.finish_event(
|
|
668
909
|
name,
|
|
669
910
|
title || BLANK,
|
|
670
911
|
body || BLANK,
|
|
671
|
-
body_format || Appsignal::EventFormatter::DEFAULT
|
|
672
|
-
0
|
|
912
|
+
body_format || Appsignal::EventFormatter::DEFAULT
|
|
673
913
|
)
|
|
674
914
|
end
|
|
675
915
|
|
|
676
916
|
# @!visibility private
|
|
677
917
|
# @see Helpers::Instrumentation#instrument
|
|
678
|
-
def record_event(
|
|
918
|
+
def record_event( # rubocop:disable Metrics/ParameterLists
|
|
919
|
+
name,
|
|
920
|
+
title,
|
|
921
|
+
body,
|
|
922
|
+
duration,
|
|
923
|
+
body_format = Appsignal::EventFormatter::DEFAULT,
|
|
924
|
+
opentelemetry_kind: nil,
|
|
925
|
+
opentelemetry_scope: nil,
|
|
926
|
+
opentelemetry_attributes: nil
|
|
927
|
+
)
|
|
679
928
|
return if paused?
|
|
680
929
|
|
|
681
|
-
@
|
|
930
|
+
@backend.record_event(
|
|
682
931
|
name,
|
|
683
932
|
title || BLANK,
|
|
684
933
|
body || BLANK,
|
|
685
934
|
body_format || Appsignal::EventFormatter::DEFAULT,
|
|
686
935
|
duration,
|
|
687
|
-
|
|
936
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
937
|
+
:opentelemetry_scope => opentelemetry_scope,
|
|
938
|
+
:opentelemetry_attributes => opentelemetry_attributes
|
|
688
939
|
)
|
|
689
940
|
end
|
|
690
941
|
|
|
691
942
|
# @!visibility private
|
|
692
943
|
# @see Helpers::Instrumentation#instrument
|
|
693
|
-
def instrument(
|
|
694
|
-
|
|
944
|
+
def instrument( # rubocop:disable Metrics/ParameterLists
|
|
945
|
+
name,
|
|
946
|
+
title = nil,
|
|
947
|
+
body = nil,
|
|
948
|
+
body_format = Appsignal::EventFormatter::DEFAULT,
|
|
949
|
+
opentelemetry_kind: nil,
|
|
950
|
+
opentelemetry_scope: nil
|
|
951
|
+
)
|
|
952
|
+
start_event(
|
|
953
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
954
|
+
:opentelemetry_scope => opentelemetry_scope
|
|
955
|
+
)
|
|
695
956
|
yield if block_given?
|
|
957
|
+
rescue Exception => error
|
|
958
|
+
# The block raised, so the operation this event describes failed. Say what
|
|
959
|
+
# kind of failure it was, which the OpenTelemetry semantic conventions ask
|
|
960
|
+
# for. This runs before the `ensure` below finishes the event, so the
|
|
961
|
+
# attribute lands on the event's own span. The error itself is not reported
|
|
962
|
+
# here; whatever catches it decides that.
|
|
963
|
+
#
|
|
964
|
+
# A paused transaction never started an event span, so there would be no
|
|
965
|
+
# span of this event's to describe.
|
|
966
|
+
unless paused?
|
|
967
|
+
add_opentelemetry_attributes(
|
|
968
|
+
Appsignal::OpenTelemetry::ErrorType.attributes_for(error.class.name)
|
|
969
|
+
)
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
raise
|
|
696
973
|
ensure
|
|
697
974
|
finish_event(name, title, body, body_format)
|
|
698
975
|
end
|
|
699
976
|
|
|
700
977
|
# @!visibility private
|
|
701
978
|
def to_h
|
|
702
|
-
JSON.parse(@
|
|
979
|
+
JSON.parse(@backend.to_json)
|
|
703
980
|
end
|
|
704
981
|
alias to_hash to_h
|
|
705
982
|
|
|
706
983
|
protected
|
|
707
984
|
|
|
708
985
|
# @!visibility private
|
|
709
|
-
attr_writer :is_duplicate, :tags, :custom_data, :
|
|
986
|
+
attr_writer :is_duplicate, :tags, :custom_data, :params_buckets,
|
|
710
987
|
:session_data, :headers
|
|
711
988
|
|
|
712
989
|
# @!visibility private
|
|
713
990
|
def internal_set_error(error, &block)
|
|
714
|
-
|
|
991
|
+
is_new_error = !@errors.include?(error)
|
|
715
992
|
|
|
716
|
-
if
|
|
993
|
+
if is_new_error && @errors.length >= ERRORS_LIMIT
|
|
717
994
|
Appsignal.internal_logger.warn "Appsignal::Transaction#add_error: Transaction has more " \
|
|
718
995
|
"than #{ERRORS_LIMIT} distinct errors. Only the first " \
|
|
719
996
|
"#{ERRORS_LIMIT} distinct errors will be reported."
|
|
720
997
|
return
|
|
721
998
|
end
|
|
722
|
-
|
|
723
|
-
@
|
|
999
|
+
|
|
1000
|
+
if @errors.empty?
|
|
1001
|
+
_set_error(error)
|
|
1002
|
+
elsif is_new_error && @backend.supports_multiple_errors?
|
|
1003
|
+
# Record additional errors immediately so each exception event lands on
|
|
1004
|
+
# the span current now, not the root span at completion. The agent
|
|
1005
|
+
# backend instead reports extras as duplicate transactions.
|
|
1006
|
+
_send_error_to_backend(error)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
@errors.add(error)
|
|
1010
|
+
|
|
1011
|
+
if @backend.supports_multiple_errors?
|
|
1012
|
+
# Collector mode: the error is already recorded, so run its block now
|
|
1013
|
+
# rather than at completion. Anything a block attaches to the current
|
|
1014
|
+
# span -- breadcrumbs, nested errors, custom instrumentation -- then
|
|
1015
|
+
# lands where the error was reported, not on the root span at
|
|
1016
|
+
# completion.
|
|
1017
|
+
if block
|
|
1018
|
+
self.class.with_transaction(self) do
|
|
1019
|
+
block.call(self)
|
|
1020
|
+
end
|
|
1021
|
+
end
|
|
1022
|
+
else
|
|
1023
|
+
@error_blocks[error] << block
|
|
1024
|
+
@error_blocks[error].compact!
|
|
1025
|
+
end
|
|
724
1026
|
end
|
|
725
1027
|
|
|
726
1028
|
private
|
|
727
1029
|
|
|
728
|
-
|
|
1030
|
+
# The `SampleData` bucket a logical params channel is stored in, per the
|
|
1031
|
+
# backend's `params_mapping`. In agent mode every channel resolves to the
|
|
1032
|
+
# same `:params` bucket (so they merge and share an `_if_nil` guard); in
|
|
1033
|
+
# collector mode the request payload, function parameters and query
|
|
1034
|
+
# parameters resolve to separate buckets. `fetch` raises if a backend's
|
|
1035
|
+
# mapping omits a channel.
|
|
1036
|
+
def params_data(channel)
|
|
1037
|
+
@params_buckets.fetch(@params_mapping.fetch(channel))
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
# Whether a params channel's bucket has had nothing set yet, so the
|
|
1041
|
+
# `_if_nil` setters do not overwrite params the caller already provided.
|
|
1042
|
+
def params_unset?(channel)
|
|
1043
|
+
bucket = params_data(channel)
|
|
1044
|
+
!bucket.value? && !bucket.empty?
|
|
1045
|
+
end
|
|
729
1046
|
|
|
1047
|
+
# `add_params`/`set_params` don't say whether the params are a request
|
|
1048
|
+
# payload or function parameters, so in collector mode they always map to
|
|
1049
|
+
# the request payload. Warn once per process to nudge callers toward the
|
|
1050
|
+
# explicit methods.
|
|
1051
|
+
def warn_params_deprecation
|
|
1052
|
+
return unless Appsignal.config&.collector_mode?
|
|
1053
|
+
|
|
1054
|
+
Appsignal::Transaction.warn_params_deprecation_once do
|
|
1055
|
+
Appsignal::Utils::StdoutAndLoggerMessage.warning(
|
|
1056
|
+
"`add_params`/`set_params` is deprecated in collector mode. Use " \
|
|
1057
|
+
"`add_request_payload` or `add_function_parameters` instead, or " \
|
|
1058
|
+
"`add_custom_data` for data that is neither."
|
|
1059
|
+
)
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Wrap a block handed to the transaction by user code so that, wherever it
|
|
1064
|
+
# later runs, a failure is logged and swallowed instead of breaking the
|
|
1065
|
+
# transaction lifecycle. A raise would otherwise skip the rest of creation or
|
|
1066
|
+
# completion, including the backend teardown that detaches the transaction's
|
|
1067
|
+
# OpenTelemetry context, which would then become the parent of the next
|
|
1068
|
+
# request's spans on that thread. Re-raising is wrong because the block runs
|
|
1069
|
+
# far from where its caller defined it.
|
|
1070
|
+
#
|
|
1071
|
+
# Returns `nil` when no block is given, so it can wrap an optional block.
|
|
1072
|
+
def protect(description, &block)
|
|
1073
|
+
return unless block
|
|
1074
|
+
|
|
1075
|
+
proc do |*args|
|
|
1076
|
+
block.call(*args)
|
|
1077
|
+
rescue => error
|
|
1078
|
+
location = block.source_location&.join(":") || "an unknown location"
|
|
1079
|
+
Appsignal.internal_logger.error(
|
|
1080
|
+
"Error in #{description}, defined at #{location}: " \
|
|
1081
|
+
"#{error.class}: #{error.message}\n#{error.backtrace&.join("\n")}"
|
|
1082
|
+
)
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
# Hooks are registered both as blocks and as method objects pushed onto the
|
|
1087
|
+
# set directly, so there is no single entry point to wrap them at. They are
|
|
1088
|
+
# protected here instead, at the one place that runs all of them.
|
|
730
1089
|
def run_after_create_hooks
|
|
731
1090
|
self.class.after_create.each do |block|
|
|
732
|
-
block.call(self)
|
|
1091
|
+
protect("the after_create hook", &block).call(self)
|
|
733
1092
|
end
|
|
734
1093
|
end
|
|
735
1094
|
|
|
736
1095
|
def run_before_complete_hooks
|
|
737
1096
|
self.class.before_complete.each do |block|
|
|
738
|
-
block.call(self, @error_set)
|
|
1097
|
+
protect("the before_complete hook", &block).call(self, @error_set)
|
|
1098
|
+
end
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
# Reports the errors stored on the transaction at completion.
|
|
1102
|
+
#
|
|
1103
|
+
# In eager (collector) mode nothing is left to do: each error was recorded
|
|
1104
|
+
# and its block run when the error was added. In deferred (agent) mode the
|
|
1105
|
+
# extension holds a single error, so the primary error's blocks run on this
|
|
1106
|
+
# transaction and every additional error is reported as a duplicate
|
|
1107
|
+
# transaction.
|
|
1108
|
+
def report_errors
|
|
1109
|
+
return if @backend.supports_multiple_errors?
|
|
1110
|
+
|
|
1111
|
+
report_errors_as_duplicates
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Agent-only legacy path. The extension transaction holds a single error, so
|
|
1115
|
+
# extra errors are reported as duplicate transactions. This whole method
|
|
1116
|
+
# disappears once agent mode is dropped: collector mode records every error
|
|
1117
|
+
# eagerly and leaves nothing to do at completion.
|
|
1118
|
+
def report_errors_as_duplicates
|
|
1119
|
+
@errors.each do |error|
|
|
1120
|
+
# Ignore the error that is already set in this transaction.
|
|
1121
|
+
next if error == @error_set
|
|
1122
|
+
|
|
1123
|
+
duplicate.tap do |transaction|
|
|
1124
|
+
# In the duplicate transaction for each error, set an error
|
|
1125
|
+
# with a block that calls all the blocks set for that error
|
|
1126
|
+
# in the original transaction. Those blocks were already wrapped
|
|
1127
|
+
# when they were added, so they are called directly here.
|
|
1128
|
+
transaction.internal_set_error(error) do
|
|
1129
|
+
@error_blocks[error].each do |block|
|
|
1130
|
+
block.call(transaction)
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
transaction.complete
|
|
1135
|
+
end
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
return unless @error_set && @error_blocks[@error_set].any?
|
|
1139
|
+
|
|
1140
|
+
self.class.with_transaction(self) do
|
|
1141
|
+
@error_blocks[@error_set].each do |block|
|
|
1142
|
+
block.call(self)
|
|
1143
|
+
end
|
|
739
1144
|
end
|
|
740
1145
|
end
|
|
741
1146
|
|
|
742
1147
|
def _set_error(error)
|
|
743
|
-
|
|
744
|
-
|
|
1148
|
+
@error_set = error
|
|
1149
|
+
_send_error_to_backend(error)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
# Records an error on the backend. The cause chain is walked once into
|
|
1153
|
+
# neutral data ({name, message, backtrace}); each backend projects what it
|
|
1154
|
+
# needs -- the agent's first-line `error_causes` sample data, or the
|
|
1155
|
+
# OpenTelemetry `appsignal.error_causes` attribute. Called for the first
|
|
1156
|
+
# error and, in collector mode, for each additional error as it is added.
|
|
1157
|
+
def _send_error_to_backend(error)
|
|
1158
|
+
causes, root_cause_missing = _error_causes(error)
|
|
1159
|
+
@backend.set_error(
|
|
745
1160
|
error.class.name,
|
|
746
1161
|
cleaned_error_message(error),
|
|
747
|
-
|
|
1162
|
+
cleaned_backtrace(error.backtrace),
|
|
1163
|
+
causes.map do |cause|
|
|
1164
|
+
{
|
|
1165
|
+
:name => cause.class.name,
|
|
1166
|
+
:message => cleaned_error_message(cause),
|
|
1167
|
+
:backtrace => cleaned_backtrace(cause.backtrace)
|
|
1168
|
+
}
|
|
1169
|
+
end,
|
|
1170
|
+
root_cause_missing
|
|
748
1171
|
)
|
|
749
|
-
|
|
1172
|
+
end
|
|
750
1173
|
|
|
1174
|
+
# Walks the `error.cause` chain (without mutating `error`), collecting up to
|
|
1175
|
+
# `ERROR_CAUSES_LIMIT` causes. Returns the causes and whether the chain was
|
|
1176
|
+
# truncated (the root cause is missing).
|
|
1177
|
+
def _error_causes(error)
|
|
751
1178
|
root_cause_missing = false
|
|
752
|
-
|
|
753
1179
|
causes = []
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
break unless error
|
|
758
|
-
|
|
1180
|
+
cause = error
|
|
1181
|
+
while (cause = cause.cause)
|
|
759
1182
|
if causes.length >= ERROR_CAUSES_LIMIT
|
|
760
1183
|
Appsignal.internal_logger.debug "Appsignal::Transaction#add_error: Error has more " \
|
|
761
1184
|
"than #{ERROR_CAUSES_LIMIT} error causes. Only the first #{ERROR_CAUSES_LIMIT} " \
|
|
@@ -764,55 +1187,10 @@ module Appsignal
|
|
|
764
1187
|
break
|
|
765
1188
|
end
|
|
766
1189
|
|
|
767
|
-
causes <<
|
|
1190
|
+
causes << cause
|
|
768
1191
|
end
|
|
769
1192
|
|
|
770
|
-
|
|
771
|
-
{
|
|
772
|
-
:name => e.class.name,
|
|
773
|
-
:message => cleaned_error_message(e),
|
|
774
|
-
:first_line => first_formatted_backtrace_line(e)
|
|
775
|
-
}
|
|
776
|
-
end
|
|
777
|
-
|
|
778
|
-
causes_sample_data.last[:is_root_cause] = false if root_cause_missing
|
|
779
|
-
|
|
780
|
-
set_sample_data(
|
|
781
|
-
"error_causes",
|
|
782
|
-
causes_sample_data
|
|
783
|
-
)
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
BACKTRACE_REGEX =
|
|
787
|
-
%r{(?<gem>[\w-]+ \(.+\) )?(?<path>:?/?\w+?.+?):(?<line>:?\d+)(?::in `(?<method>.+)')?$}.freeze
|
|
788
|
-
private_constant :BACKTRACE_REGEX
|
|
789
|
-
|
|
790
|
-
def first_formatted_backtrace_line(error)
|
|
791
|
-
backtrace = cleaned_backtrace(error.backtrace)
|
|
792
|
-
first_line = backtrace&.first
|
|
793
|
-
return unless first_line
|
|
794
|
-
|
|
795
|
-
captures = BACKTRACE_REGEX.match(first_line)
|
|
796
|
-
return unless captures
|
|
797
|
-
|
|
798
|
-
captures.named_captures
|
|
799
|
-
.merge("original" => first_line)
|
|
800
|
-
.tap do |c|
|
|
801
|
-
config = Appsignal.config
|
|
802
|
-
# Strip of whitespace at the end of the gem name
|
|
803
|
-
c["gem"] = c["gem"]&.strip
|
|
804
|
-
# Strip the app path from the path if present
|
|
805
|
-
root_path = config.root_path
|
|
806
|
-
if c["path"].start_with?(root_path)
|
|
807
|
-
c["path"].delete_prefix!(root_path)
|
|
808
|
-
# Relative paths shouldn't start with a slash
|
|
809
|
-
c["path"].delete_prefix!("/")
|
|
810
|
-
end
|
|
811
|
-
# Add revision for linking to the repository from the UI
|
|
812
|
-
c["revision"] = config[:revision]
|
|
813
|
-
# Convert line number to an integer
|
|
814
|
-
c["line"] = c["line"].to_i
|
|
815
|
-
end
|
|
1193
|
+
[causes, root_cause_missing]
|
|
816
1194
|
end
|
|
817
1195
|
|
|
818
1196
|
def set_sample_data(key, data)
|
|
@@ -825,10 +1203,11 @@ module Appsignal
|
|
|
825
1203
|
return
|
|
826
1204
|
end
|
|
827
1205
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1206
|
+
# Pass raw Ruby through to the backend. ExtensionBackend serializes to a
|
|
1207
|
+
# C-extension `Data` object; OpenTelemetryBackend reads the Hash/Array
|
|
1208
|
+
# directly. The `RuntimeError` rescue still covers ExtensionBackend's
|
|
1209
|
+
# `Data.generate`, which now runs inside the backend call.
|
|
1210
|
+
@backend.set_sample_data(key.to_s, data)
|
|
832
1211
|
rescue RuntimeError => e
|
|
833
1212
|
begin
|
|
834
1213
|
inspected_data = data.inspect
|
|
@@ -843,15 +1222,21 @@ module Appsignal
|
|
|
843
1222
|
end
|
|
844
1223
|
|
|
845
1224
|
def sample_data
|
|
846
|
-
{
|
|
847
|
-
:params => sanitized_params,
|
|
1225
|
+
data = {
|
|
848
1226
|
:environment => sanitized_request_headers,
|
|
849
1227
|
:session_data => sanitized_session_data,
|
|
850
1228
|
:tags => sanitized_tags,
|
|
851
|
-
:breadcrumbs => breadcrumbs,
|
|
852
1229
|
:custom_data => custom_data
|
|
853
|
-
}
|
|
854
|
-
|
|
1230
|
+
}
|
|
1231
|
+
# Each params bucket is emitted under its own key. The extension backend
|
|
1232
|
+
# has a single `:params` bucket; the OpenTelemetry backend has separate
|
|
1233
|
+
# `:request_payload` and `:function_parameters` buckets. The backend maps
|
|
1234
|
+
# each key to its storage (C-extension slot or OpenTelemetry attribute).
|
|
1235
|
+
@params_buckets.each do |bucket, sample|
|
|
1236
|
+
data[bucket] = sanitized_params(sample)
|
|
1237
|
+
end
|
|
1238
|
+
data.each do |key, value|
|
|
1239
|
+
set_sample_data(key, value)
|
|
855
1240
|
end
|
|
856
1241
|
end
|
|
857
1242
|
|
|
@@ -860,30 +1245,38 @@ module Appsignal
|
|
|
860
1245
|
self.class.new(
|
|
861
1246
|
namespace,
|
|
862
1247
|
:id => new_transaction_id,
|
|
863
|
-
:
|
|
1248
|
+
:backend => @backend.duplicate(new_transaction_id)
|
|
864
1249
|
).tap do |transaction|
|
|
865
1250
|
transaction.is_duplicate = true
|
|
866
1251
|
transaction.tags = @tags.dup
|
|
867
1252
|
transaction.custom_data = @custom_data.dup
|
|
868
|
-
transaction.
|
|
869
|
-
transaction.params = @params.dup
|
|
1253
|
+
transaction.params_buckets = @params_buckets.transform_values(&:dup)
|
|
870
1254
|
transaction.session_data = @session_data.dup
|
|
871
1255
|
transaction.headers = @headers.dup
|
|
872
1256
|
end
|
|
873
1257
|
end
|
|
874
1258
|
|
|
875
1259
|
def params
|
|
876
|
-
|
|
877
|
-
rescue => e
|
|
878
|
-
Appsignal.internal_logger.error("Exception while fetching params: #{e.class}: #{e}")
|
|
879
|
-
nil
|
|
1260
|
+
params_value(params_data(:params))
|
|
880
1261
|
end
|
|
881
1262
|
|
|
882
|
-
def sanitized_params
|
|
1263
|
+
def sanitized_params(sample = params_data(:params))
|
|
883
1264
|
return unless Appsignal.config[:send_params]
|
|
884
1265
|
|
|
885
1266
|
filter_keys = Appsignal.config[:filter_parameters] || []
|
|
886
|
-
Appsignal::Utils::SampleDataSanitizer.sanitize(
|
|
1267
|
+
Appsignal::Utils::SampleDataSanitizer.sanitize(params_value(sample), filter_keys)
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
# Reads a params bucket's value. Evaluating it runs any block the caller
|
|
1271
|
+
# passed to `add_params`/`add_request_payload`/`add_function_parameters`,
|
|
1272
|
+
# which is user code that can raise, so a failure is logged and swallowed.
|
|
1273
|
+
def params_value(sample)
|
|
1274
|
+
sample.value
|
|
1275
|
+
rescue => e
|
|
1276
|
+
Appsignal.internal_logger.error(
|
|
1277
|
+
"Exception while fetching params (#{sample.key}): #{e.class}: #{e}"
|
|
1278
|
+
)
|
|
1279
|
+
nil
|
|
887
1280
|
end
|
|
888
1281
|
|
|
889
1282
|
def session_data
|