appsignal 4.10.4-java → 5.0.0-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 +108 -0
- data/Rakefile +195 -4
- data/appsignal.gemspec +8 -0
- data/build_matrix.yml +12 -0
- data/ext/appsignal_extension.c +14 -0
- data/lib/appsignal/backends.rb +55 -0
- data/lib/appsignal/cli/diagnose.rb +2 -10
- data/lib/appsignal/config.rb +377 -13
- data/lib/appsignal/demo.rb +12 -10
- 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 +324 -20
- data/lib/appsignal/helpers/metrics.rb +3 -24
- data/lib/appsignal/hooks/action_cable.rb +26 -8
- data/lib/appsignal/hooks/active_job.rb +184 -47
- 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 -19
- data/lib/appsignal/integrations/data_mapper.rb +14 -2
- data/lib/appsignal/integrations/delayed_job_plugin.rb +81 -8
- 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 +256 -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 +81 -11
- data/lib/appsignal/integrations/shoryuken.rb +159 -12
- data/lib/appsignal/integrations/sidekiq.rb +94 -16
- data/lib/appsignal/integrations/webmachine.rb +56 -5
- 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 +79 -0
- data/lib/appsignal/opentelemetry/messaging.rb +82 -0
- data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
- data/lib/appsignal/opentelemetry/rendering.rb +29 -0
- data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
- data/lib/appsignal/opentelemetry.rb +495 -0
- data/lib/appsignal/rack/abstract_middleware.rb +66 -4
- data/lib/appsignal/rack/body_wrapper.rb +18 -5
- data/lib/appsignal/rack/event_handler.rb +44 -4
- 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 +68 -12
- data/lib/appsignal/sample_data.rb +4 -0
- data/lib/appsignal/transaction/base_backend.rb +128 -0
- data/lib/appsignal/transaction/extension_backend.rb +229 -0
- data/lib/appsignal/transaction/opentelemetry_backend.rb +847 -0
- data/lib/appsignal/transaction.rb +714 -164
- data/lib/appsignal/utils/request_headers.rb +78 -0
- data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
- data/lib/appsignal/utils.rb +1 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +10 -0
- data/sig/appsignal.rbi +630 -37
- data/sig/appsignal.rbs +582 -27
- metadata +25 -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,55 @@ 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
|
-
@headers = Appsignal::SampleData.new(:headers, Hash)
|
|
180
246
|
@custom_data = Appsignal::SampleData.new(:custom_data)
|
|
181
247
|
|
|
182
|
-
@
|
|
248
|
+
@backend = backend || Appsignal::Backends.transaction.new(
|
|
183
249
|
@transaction_id,
|
|
184
250
|
@namespace,
|
|
185
|
-
|
|
186
|
-
|
|
251
|
+
:opentelemetry_context => opentelemetry_context,
|
|
252
|
+
:opentelemetry_scope => opentelemetry_scope,
|
|
253
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
254
|
+
:opentelemetry_relationship => opentelemetry_relationship
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# The backend decides how the params channels are stored. Its
|
|
258
|
+
# `params_mapping` maps each logical channel to a storage bucket: the
|
|
259
|
+
# extension backend maps them all to one `:params` bucket, so agent mode
|
|
260
|
+
# keeps a single merged blob, while the OpenTelemetry backend keeps the
|
|
261
|
+
# request payload, function parameters and query parameters apart.
|
|
262
|
+
#
|
|
263
|
+
# Each distinct bucket gets its own `SampleData`, named after the bucket.
|
|
264
|
+
# That symbol is also the sample-data key the backend receives, so it can
|
|
265
|
+
# route the bucket to the right storage.
|
|
266
|
+
@params_mapping = @backend.params_mapping
|
|
267
|
+
@params_options = @backend.params_options
|
|
268
|
+
@params_buckets = @params_mapping.values.uniq.to_h do |bucket|
|
|
269
|
+
[bucket, Appsignal::SampleData.new(bucket)]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
@headers_mapping = @backend.headers_mapping
|
|
273
|
+
@headers_allowlist = @backend.headers_allowlist
|
|
274
|
+
@headers_buckets = @headers_mapping.each_value.map(&:first).uniq.to_h do |bucket|
|
|
275
|
+
[bucket, Appsignal::SampleData.new(bucket, Hash)]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
@channels_set = []
|
|
187
279
|
|
|
188
280
|
run_after_create_hooks
|
|
189
281
|
end
|
|
@@ -205,9 +297,22 @@ module Appsignal
|
|
|
205
297
|
|
|
206
298
|
# @!visibility private
|
|
207
299
|
def complete
|
|
300
|
+
# Completing is idempotent: a transaction can be completed explicitly and
|
|
301
|
+
# then again by a `complete_current!` cleanup path. Re-running would, for a
|
|
302
|
+
# multi-error transaction, re-record the extra errors (a second duplicate
|
|
303
|
+
# in agent mode, or an event on an already-finished span in collector mode).
|
|
304
|
+
return if completed?
|
|
305
|
+
|
|
208
306
|
if discarded?
|
|
209
307
|
Appsignal.internal_logger.debug "Skipping transaction '#{transaction_id}' " \
|
|
210
308
|
"because it was manually discarded."
|
|
309
|
+
# Let the backend tear itself down. The agent backend drops the
|
|
310
|
+
# transaction (nothing is sent); the OpenTelemetry backend still
|
|
311
|
+
# finishes and exports the root span, but flags it with
|
|
312
|
+
# `appsignal.ignore_subtrace` so the collector ignores the subtrace.
|
|
313
|
+
# `@completed` stays false either way: a discarded transaction was
|
|
314
|
+
# never reported.
|
|
315
|
+
@backend.discard
|
|
211
316
|
return
|
|
212
317
|
end
|
|
213
318
|
|
|
@@ -219,40 +324,18 @@ module Appsignal
|
|
|
219
324
|
should_sample = true
|
|
220
325
|
|
|
221
326
|
unless duplicate?
|
|
222
|
-
self.class.last_errors = @
|
|
223
|
-
should_sample = @
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
@error_blocks.each do |error, blocks|
|
|
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
|
|
327
|
+
self.class.last_errors = @errors.to_a
|
|
328
|
+
should_sample = @backend.finish
|
|
240
329
|
end
|
|
241
330
|
|
|
242
|
-
|
|
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
|
|
331
|
+
report_errors
|
|
249
332
|
|
|
250
333
|
run_before_complete_hooks
|
|
251
334
|
|
|
252
335
|
sample_data if should_sample
|
|
253
336
|
|
|
254
337
|
@completed = true
|
|
255
|
-
@
|
|
338
|
+
@backend.complete
|
|
256
339
|
end
|
|
257
340
|
|
|
258
341
|
# @!visibility private
|
|
@@ -328,12 +411,13 @@ module Appsignal
|
|
|
328
411
|
end
|
|
329
412
|
|
|
330
413
|
# @!visibility private
|
|
414
|
+
#
|
|
415
|
+
# True when an outer integration (Active Job) is already recording this
|
|
416
|
+
# enqueue. Nested integrations use it to skip their own enqueue event, but
|
|
417
|
+
# they must still propagate trace context: the outer integration's producer
|
|
418
|
+
# span is what the performing job links back to, and only the nested
|
|
419
|
+
# integration owns the carrier that job travels on.
|
|
331
420
|
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
421
|
store("job_enqueue")[:suppressed] == true
|
|
338
422
|
end
|
|
339
423
|
|
|
@@ -356,17 +440,28 @@ module Appsignal
|
|
|
356
440
|
# @see https://docs.appsignal.com/guides/custom-data/sample-data.html
|
|
357
441
|
# Sample data guide
|
|
358
442
|
def add_params(given_params = nil, &block)
|
|
359
|
-
|
|
443
|
+
warn_params_deprecation
|
|
444
|
+
add_params_channel(:params, given_params, &block)
|
|
360
445
|
end
|
|
361
446
|
alias set_params add_params
|
|
362
447
|
|
|
448
|
+
# Marks every params channel as explicitly empty, so no parameters are
|
|
449
|
+
# reported for this transaction whatever their source.
|
|
450
|
+
#
|
|
451
|
+
# This is a deliberate choice for collector mode, where the request payload
|
|
452
|
+
# and the function parameters live in separate buckets: it empties both, not
|
|
453
|
+
# only the request payload the legacy `:params` channel maps to. Otherwise
|
|
454
|
+
# an integration's `_if_nil` setter could still add function parameters (a
|
|
455
|
+
# background job's arguments) after params were emptied, and the behavior
|
|
456
|
+
# would differ from agent mode, where all channels share one bucket.
|
|
457
|
+
#
|
|
363
458
|
# @since 4.0.0
|
|
364
459
|
# @return [void]
|
|
365
460
|
# @!visibility private
|
|
366
461
|
#
|
|
367
462
|
# @see Helpers::Instrumentation#set_empty_params!
|
|
368
463
|
def set_empty_params!
|
|
369
|
-
@
|
|
464
|
+
@params_buckets.each_value(&:set_empty_value!)
|
|
370
465
|
end
|
|
371
466
|
|
|
372
467
|
# Add parameters to the transaction if not already set.
|
|
@@ -382,10 +477,119 @@ module Appsignal
|
|
|
382
477
|
#
|
|
383
478
|
# @see #add_params
|
|
384
479
|
def add_params_if_nil(given_params = nil, &block)
|
|
385
|
-
add_params(given_params, &block) if
|
|
480
|
+
add_params(given_params, &block) if params_unset?(:params)
|
|
386
481
|
end
|
|
387
482
|
alias set_params_if_nil add_params_if_nil
|
|
388
483
|
|
|
484
|
+
# Add the request payload to the transaction.
|
|
485
|
+
#
|
|
486
|
+
# These are the parameters of an incoming request, such as the query string
|
|
487
|
+
# and the request body. In collector mode they map to the request payload
|
|
488
|
+
# attribute. In agent mode they are the transaction's params.
|
|
489
|
+
#
|
|
490
|
+
# Behaves like {#add_params}: merges when called multiple times, and a
|
|
491
|
+
# block takes precedence over the argument.
|
|
492
|
+
#
|
|
493
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
494
|
+
# transaction.
|
|
495
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
496
|
+
# return value will become the new parameters.
|
|
497
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
498
|
+
# @return [void]
|
|
499
|
+
#
|
|
500
|
+
# @see #add_function_parameters
|
|
501
|
+
def add_request_payload(given_params = nil, &block)
|
|
502
|
+
add_params_channel(:request_payload, given_params, &block)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Add the request payload to the transaction if not already set.
|
|
506
|
+
#
|
|
507
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
508
|
+
# transaction if none are already set.
|
|
509
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
510
|
+
# return value will become the new parameters.
|
|
511
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
512
|
+
# @return [void]
|
|
513
|
+
# @!visibility private
|
|
514
|
+
#
|
|
515
|
+
# @see #add_request_payload
|
|
516
|
+
def add_request_payload_if_nil(given_params = nil, &block)
|
|
517
|
+
add_request_payload(given_params, &block) if params_unset?(:request_payload)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Add the function parameters to the transaction.
|
|
521
|
+
#
|
|
522
|
+
# These are the arguments a background job or function was called with. In
|
|
523
|
+
# collector mode they map to the function parameters attribute. In agent
|
|
524
|
+
# mode they are the transaction's params.
|
|
525
|
+
#
|
|
526
|
+
# Behaves like {#add_params}: merges when called multiple times, and a
|
|
527
|
+
# block takes precedence over the argument.
|
|
528
|
+
#
|
|
529
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
530
|
+
# transaction.
|
|
531
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
532
|
+
# return value will become the new parameters.
|
|
533
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
534
|
+
# @return [void]
|
|
535
|
+
#
|
|
536
|
+
# @see #add_request_payload
|
|
537
|
+
def add_function_parameters(given_params = nil, &block)
|
|
538
|
+
add_params_channel(:function_parameters, given_params, &block)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Add the function parameters to the transaction if not already set.
|
|
542
|
+
#
|
|
543
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
544
|
+
# transaction if none are already set.
|
|
545
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
546
|
+
# return value will become the new parameters.
|
|
547
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
548
|
+
# @return [void]
|
|
549
|
+
# @!visibility private
|
|
550
|
+
#
|
|
551
|
+
# @see #add_function_parameters
|
|
552
|
+
def add_function_parameters_if_nil(given_params = nil, &block)
|
|
553
|
+
add_function_parameters(given_params, &block) if params_unset?(:function_parameters)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Add the query parameters to the transaction.
|
|
557
|
+
#
|
|
558
|
+
# These are the parameters parsed from an incoming request's query string.
|
|
559
|
+
# In collector mode they map to their own attribute, separate from the
|
|
560
|
+
# request payload and the function parameters. In agent mode they are the
|
|
561
|
+
# transaction's params.
|
|
562
|
+
#
|
|
563
|
+
# Behaves like {#add_params}: merges when called multiple times, and a
|
|
564
|
+
# block takes precedence over the argument.
|
|
565
|
+
#
|
|
566
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
567
|
+
# transaction.
|
|
568
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
569
|
+
# return value will become the new parameters.
|
|
570
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
571
|
+
# @return [void]
|
|
572
|
+
#
|
|
573
|
+
# @see #add_request_payload
|
|
574
|
+
def add_query_parameters(given_params = nil, &block)
|
|
575
|
+
add_params_channel(:query_parameters, given_params, &block)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Add the query parameters to the transaction if not already set.
|
|
579
|
+
#
|
|
580
|
+
# @param given_params [Hash<String, Object>, Array<Object>] The parameters to add to the
|
|
581
|
+
# transaction if none are already set.
|
|
582
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
583
|
+
# return value will become the new parameters.
|
|
584
|
+
# @yieldreturn [Hash<String, Object>, Array<Object>]
|
|
585
|
+
# @return [void]
|
|
586
|
+
# @!visibility private
|
|
587
|
+
#
|
|
588
|
+
# @see #add_query_parameters
|
|
589
|
+
def add_query_parameters_if_nil(given_params = nil, &block)
|
|
590
|
+
add_query_parameters(given_params, &block) if params_unset?(:query_parameters)
|
|
591
|
+
end
|
|
592
|
+
|
|
389
593
|
# Add tags to the transaction.
|
|
390
594
|
#
|
|
391
595
|
# When this method is called multiple times, it will merge the tags.
|
|
@@ -452,6 +656,10 @@ module Appsignal
|
|
|
452
656
|
|
|
453
657
|
# Add headers to the transaction.
|
|
454
658
|
#
|
|
659
|
+
# @deprecated Use {#add_request_headers} for request headers and
|
|
660
|
+
# {#add_request_environment} for the values a Rack environment holds that
|
|
661
|
+
# are not request headers. This method takes both kinds at once, so it
|
|
662
|
+
# has to work out which of them each value is.
|
|
455
663
|
# @since 4.0.0
|
|
456
664
|
# @param given_headers [Hash<String, Object>] A hash containing headers.
|
|
457
665
|
# @yield This block is called when the transaction is sampled. The block's
|
|
@@ -463,7 +671,19 @@ module Appsignal
|
|
|
463
671
|
# @see https://docs.appsignal.com/guides/custom-data/sample-data.html
|
|
464
672
|
# Sample data guide
|
|
465
673
|
def add_headers(given_headers = nil, &block)
|
|
466
|
-
|
|
674
|
+
if block
|
|
675
|
+
headers, environment = Appsignal::Utils::RequestHeaders.split_lazily(&block)
|
|
676
|
+
|
|
677
|
+
add_headers_channel(:request_headers, &headers)
|
|
678
|
+
add_headers_channel(:request_environment, &environment)
|
|
679
|
+
elsif given_headers.is_a?(Hash)
|
|
680
|
+
headers, environment = Appsignal::Utils::RequestHeaders.split(given_headers)
|
|
681
|
+
|
|
682
|
+
add_headers_channel(:request_headers, headers)
|
|
683
|
+
add_headers_channel(:request_environment, environment)
|
|
684
|
+
else
|
|
685
|
+
add_headers_channel(:request_environment, given_headers)
|
|
686
|
+
end
|
|
467
687
|
end
|
|
468
688
|
alias set_headers add_headers
|
|
469
689
|
|
|
@@ -472,6 +692,8 @@ module Appsignal
|
|
|
472
692
|
# When both the `given_headers` and a block is given to this method,
|
|
473
693
|
# the block is leading and the argument will _not_ be used.
|
|
474
694
|
#
|
|
695
|
+
# @deprecated Use {#add_request_headers_if_nil} or
|
|
696
|
+
# {#add_request_environment_if_nil}.
|
|
475
697
|
# @since 4.0.0
|
|
476
698
|
# @param given_headers [Hash<String, Object>] A hash containing headers.
|
|
477
699
|
# @yield This block is called when the transaction is sampled. The block's
|
|
@@ -484,10 +706,92 @@ module Appsignal
|
|
|
484
706
|
# @see https://docs.appsignal.com/guides/custom-data/sample-data.html
|
|
485
707
|
# Sample data guide
|
|
486
708
|
def add_headers_if_nil(given_headers = nil, &block)
|
|
487
|
-
|
|
709
|
+
return if channel_set?(:request_headers) || channel_set?(:request_environment)
|
|
710
|
+
|
|
711
|
+
add_headers(given_headers, &block)
|
|
488
712
|
end
|
|
489
713
|
alias set_headers_if_nil add_headers_if_nil
|
|
490
714
|
|
|
715
|
+
# Add request headers to the transaction.
|
|
716
|
+
#
|
|
717
|
+
# Name each header the way OpenTelemetry names it, in lowercase and with
|
|
718
|
+
# dashes, such as `accept` and `content-length`. In agent mode the names
|
|
719
|
+
# are converted to the Rack spellings the environment uses, such as
|
|
720
|
+
# `HTTP_ACCEPT`.
|
|
721
|
+
#
|
|
722
|
+
# Behaves like {#add_headers}: merges when called multiple times, and a
|
|
723
|
+
# block takes precedence over the argument.
|
|
724
|
+
#
|
|
725
|
+
# @param given_headers [Hash<String, Object>] A hash containing request
|
|
726
|
+
# headers.
|
|
727
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
728
|
+
# return value will become the new request headers.
|
|
729
|
+
# @yieldreturn [Hash<String, Object>]
|
|
730
|
+
# @return [void]
|
|
731
|
+
#
|
|
732
|
+
# @see #add_request_environment
|
|
733
|
+
# @see https://docs.appsignal.com/guides/custom-data/sample-data.html
|
|
734
|
+
# Sample data guide
|
|
735
|
+
def add_request_headers(given_headers = nil, &block)
|
|
736
|
+
add_headers_channel(:request_headers, given_headers, &block)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Add request headers to the transaction if none are already set.
|
|
740
|
+
#
|
|
741
|
+
# @param given_headers [Hash<String, Object>] A hash containing request
|
|
742
|
+
# headers to set if none are already set.
|
|
743
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
744
|
+
# return value will become the new request headers.
|
|
745
|
+
# @yieldreturn [Hash<String, Object>]
|
|
746
|
+
# @return [void]
|
|
747
|
+
# @!visibility private
|
|
748
|
+
#
|
|
749
|
+
# @see #add_request_headers
|
|
750
|
+
def add_request_headers_if_nil(given_headers = nil, &block)
|
|
751
|
+
add_request_headers(given_headers, &block) unless channel_set?(:request_headers)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Add values from the request environment to the transaction.
|
|
755
|
+
#
|
|
756
|
+
# These are the values a Rack environment holds that are not request
|
|
757
|
+
# headers, such as `REMOTE_ADDR` and `QUERY_STRING`. Name each one the way
|
|
758
|
+
# Rack names it. Use {#add_request_headers} for the request headers.
|
|
759
|
+
#
|
|
760
|
+
# Behaves like {#add_headers}: merges when called multiple times, and a
|
|
761
|
+
# block takes precedence over the argument.
|
|
762
|
+
#
|
|
763
|
+
# @param given_environment [Hash<String, Object>] A hash containing request
|
|
764
|
+
# environment values.
|
|
765
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
766
|
+
# return value will become the new request environment.
|
|
767
|
+
# @yieldreturn [Hash<String, Object>]
|
|
768
|
+
# @return [void]
|
|
769
|
+
#
|
|
770
|
+
# @see #add_request_headers
|
|
771
|
+
# @see https://docs.appsignal.com/guides/custom-data/sample-data.html
|
|
772
|
+
# Sample data guide
|
|
773
|
+
def add_request_environment(given_environment = nil, &block)
|
|
774
|
+
add_headers_channel(:request_environment, given_environment, &block)
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Add values from the request environment to the transaction if none are
|
|
778
|
+
# already set.
|
|
779
|
+
#
|
|
780
|
+
# @param given_environment [Hash<String, Object>] A hash containing request
|
|
781
|
+
# environment values to set if none are already set.
|
|
782
|
+
# @yield This block is called when the transaction is sampled. The block's
|
|
783
|
+
# return value will become the new request environment.
|
|
784
|
+
# @yieldreturn [Hash<String, Object>]
|
|
785
|
+
# @return [void]
|
|
786
|
+
# @!visibility private
|
|
787
|
+
#
|
|
788
|
+
# @see #add_request_environment
|
|
789
|
+
def add_request_environment_if_nil(given_environment = nil, &block)
|
|
790
|
+
return if channel_set?(:request_environment)
|
|
791
|
+
|
|
792
|
+
add_request_environment(given_environment, &block)
|
|
793
|
+
end
|
|
794
|
+
|
|
491
795
|
# Add custom data to the transaction.
|
|
492
796
|
#
|
|
493
797
|
# @since 4.0.0
|
|
@@ -524,14 +828,16 @@ module Appsignal
|
|
|
524
828
|
return
|
|
525
829
|
end
|
|
526
830
|
|
|
527
|
-
|
|
831
|
+
# The backend owns how breadcrumbs are stored: the agent backend buffers
|
|
832
|
+
# them and flushes at completion, the OpenTelemetry backend emits each as a
|
|
833
|
+
# span event right away (by completion its target span has finished).
|
|
834
|
+
@backend.add_breadcrumb(
|
|
528
835
|
:time => time.to_i,
|
|
529
836
|
:category => category,
|
|
530
837
|
:action => action,
|
|
531
838
|
:message => message,
|
|
532
839
|
:metadata => metadata
|
|
533
840
|
)
|
|
534
|
-
@breadcrumbs = @breadcrumbs.last(BREADCRUMB_LIMIT)
|
|
535
841
|
end
|
|
536
842
|
|
|
537
843
|
# Set an action name for the transaction.
|
|
@@ -548,7 +854,7 @@ module Appsignal
|
|
|
548
854
|
return unless action
|
|
549
855
|
|
|
550
856
|
@action = action
|
|
551
|
-
@
|
|
857
|
+
@backend.set_action(action)
|
|
552
858
|
end
|
|
553
859
|
|
|
554
860
|
# Set an action name only if there is no current action set.
|
|
@@ -596,7 +902,7 @@ module Appsignal
|
|
|
596
902
|
return unless namespace
|
|
597
903
|
|
|
598
904
|
@namespace = namespace
|
|
599
|
-
@
|
|
905
|
+
@backend.set_namespace(namespace)
|
|
600
906
|
end
|
|
601
907
|
|
|
602
908
|
# Set queue start time for transaction.
|
|
@@ -610,7 +916,7 @@ module Appsignal
|
|
|
610
916
|
def set_queue_start(start)
|
|
611
917
|
return unless start
|
|
612
918
|
|
|
613
|
-
@
|
|
919
|
+
@backend.set_queue_start(start)
|
|
614
920
|
rescue RangeError
|
|
615
921
|
Appsignal.internal_logger.warn("Queue start value #{start} is too big")
|
|
616
922
|
end
|
|
@@ -620,12 +926,47 @@ module Appsignal
|
|
|
620
926
|
return unless key && value
|
|
621
927
|
return if Appsignal.config[:filter_metadata].include?(key.to_s)
|
|
622
928
|
|
|
623
|
-
@
|
|
929
|
+
@backend.set_metadata(key, value)
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# Add OpenTelemetry attributes to the span AppSignal is currently
|
|
933
|
+
# recording.
|
|
934
|
+
#
|
|
935
|
+
# In collector mode, AppSignal records a transaction as an OpenTelemetry
|
|
936
|
+
# span, and every instrumented event as a child span. This adds attributes
|
|
937
|
+
# to whichever of those spans is open right now: the innermost event
|
|
938
|
+
# started by {Appsignal::Helpers::Instrumentation#instrument}, or the
|
|
939
|
+
# transaction's own span when no event is open.
|
|
940
|
+
#
|
|
941
|
+
# Use this to describe what is being instrumented in OpenTelemetry's own
|
|
942
|
+
# terms, following the OpenTelemetry semantic conventions where they apply.
|
|
943
|
+
# Attributes have no equivalent outside collector mode, so this does
|
|
944
|
+
# nothing when collector mode is not active.
|
|
945
|
+
#
|
|
946
|
+
# @example Describing a database query
|
|
947
|
+
# Appsignal.instrument("query.my_database") do
|
|
948
|
+
# Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
949
|
+
# "db.system.name" => "mysql"
|
|
950
|
+
# )
|
|
951
|
+
# run_the_query
|
|
952
|
+
# end
|
|
953
|
+
#
|
|
954
|
+
# @param attributes [Hash<String, Object>, nil] Attributes to add to the
|
|
955
|
+
# current span. Values that are not a String, Integer, Float or boolean
|
|
956
|
+
# are converted to a String. Nothing is added when this is nil or empty.
|
|
957
|
+
# @return [void]
|
|
958
|
+
#
|
|
959
|
+
# @see https://opentelemetry.io/docs/specs/semconv/
|
|
960
|
+
# OpenTelemetry semantic conventions
|
|
961
|
+
def add_opentelemetry_attributes(attributes = {})
|
|
962
|
+
return if attributes.nil? || attributes.empty?
|
|
963
|
+
|
|
964
|
+
@backend.set_attributes(attributes)
|
|
624
965
|
end
|
|
625
966
|
|
|
626
967
|
# @!visibility private
|
|
627
968
|
# @see Appsignal::Helpers::Instrumentation#report_error
|
|
628
|
-
def add_error(error, &block)
|
|
969
|
+
def add_error(error, source: nil, &block)
|
|
629
970
|
unless error.is_a?(Exception)
|
|
630
971
|
Appsignal.internal_logger.error "Appsignal::Transaction#add_error: Cannot add error. " \
|
|
631
972
|
"The given value is not an exception: #{error.inspect}"
|
|
@@ -635,10 +976,15 @@ module Appsignal
|
|
|
635
976
|
return unless error
|
|
636
977
|
return unless Appsignal.active?
|
|
637
978
|
|
|
638
|
-
if error.instance_variable_get(:@__appsignal_error_reported) && !@
|
|
979
|
+
if error.instance_variable_get(:@__appsignal_error_reported) && !@errors.include?(error)
|
|
639
980
|
return
|
|
640
981
|
end
|
|
641
982
|
|
|
983
|
+
# Wrap the block here, at the entry point, so it stays protected wherever
|
|
984
|
+
# it later runs: right away in collector mode, or at completion in agent
|
|
985
|
+
# mode. `source` names the helper the block was given to, when known, so
|
|
986
|
+
# the log points the customer at the right call.
|
|
987
|
+
block = protect(source ? "the block passed to #{source}" : "the error block", &block) if block
|
|
642
988
|
internal_set_error(error, &block)
|
|
643
989
|
|
|
644
990
|
# Mark errors and their causes as tracked so we don't report duplicates,
|
|
@@ -653,10 +999,13 @@ module Appsignal
|
|
|
653
999
|
|
|
654
1000
|
# @!visibility private
|
|
655
1001
|
# @see Helpers::Instrumentation#instrument
|
|
656
|
-
def start_event
|
|
1002
|
+
def start_event(opentelemetry_kind: nil, opentelemetry_scope: nil)
|
|
657
1003
|
return if paused?
|
|
658
1004
|
|
|
659
|
-
@
|
|
1005
|
+
@backend.start_event(
|
|
1006
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
1007
|
+
:opentelemetry_scope => opentelemetry_scope
|
|
1008
|
+
)
|
|
660
1009
|
end
|
|
661
1010
|
|
|
662
1011
|
# @!visibility private
|
|
@@ -664,98 +1013,290 @@ module Appsignal
|
|
|
664
1013
|
def finish_event(name, title, body, body_format = Appsignal::EventFormatter::DEFAULT)
|
|
665
1014
|
return if paused?
|
|
666
1015
|
|
|
667
|
-
@
|
|
1016
|
+
@backend.finish_event(
|
|
668
1017
|
name,
|
|
669
1018
|
title || BLANK,
|
|
670
1019
|
body || BLANK,
|
|
671
|
-
body_format || Appsignal::EventFormatter::DEFAULT
|
|
672
|
-
0
|
|
1020
|
+
body_format || Appsignal::EventFormatter::DEFAULT
|
|
673
1021
|
)
|
|
674
1022
|
end
|
|
675
1023
|
|
|
676
1024
|
# @!visibility private
|
|
677
1025
|
# @see Helpers::Instrumentation#instrument
|
|
678
|
-
def record_event(
|
|
1026
|
+
def record_event( # rubocop:disable Metrics/ParameterLists
|
|
1027
|
+
name,
|
|
1028
|
+
title,
|
|
1029
|
+
body,
|
|
1030
|
+
duration,
|
|
1031
|
+
body_format = Appsignal::EventFormatter::DEFAULT,
|
|
1032
|
+
opentelemetry_kind: nil,
|
|
1033
|
+
opentelemetry_scope: nil,
|
|
1034
|
+
opentelemetry_attributes: nil
|
|
1035
|
+
)
|
|
679
1036
|
return if paused?
|
|
680
1037
|
|
|
681
|
-
@
|
|
1038
|
+
@backend.record_event(
|
|
682
1039
|
name,
|
|
683
1040
|
title || BLANK,
|
|
684
1041
|
body || BLANK,
|
|
685
1042
|
body_format || Appsignal::EventFormatter::DEFAULT,
|
|
686
1043
|
duration,
|
|
687
|
-
|
|
1044
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
1045
|
+
:opentelemetry_scope => opentelemetry_scope,
|
|
1046
|
+
:opentelemetry_attributes => opentelemetry_attributes
|
|
688
1047
|
)
|
|
689
1048
|
end
|
|
690
1049
|
|
|
691
1050
|
# @!visibility private
|
|
692
1051
|
# @see Helpers::Instrumentation#instrument
|
|
693
|
-
def instrument(
|
|
694
|
-
|
|
1052
|
+
def instrument( # rubocop:disable Metrics/ParameterLists
|
|
1053
|
+
name,
|
|
1054
|
+
title = nil,
|
|
1055
|
+
body = nil,
|
|
1056
|
+
body_format = Appsignal::EventFormatter::DEFAULT,
|
|
1057
|
+
opentelemetry_kind: nil,
|
|
1058
|
+
opentelemetry_scope: nil
|
|
1059
|
+
)
|
|
1060
|
+
start_event(
|
|
1061
|
+
:opentelemetry_kind => opentelemetry_kind,
|
|
1062
|
+
:opentelemetry_scope => opentelemetry_scope
|
|
1063
|
+
)
|
|
695
1064
|
yield if block_given?
|
|
1065
|
+
rescue Exception => error
|
|
1066
|
+
# The block raised, so the operation this event describes failed. Say what
|
|
1067
|
+
# kind of failure it was, which the OpenTelemetry semantic conventions ask
|
|
1068
|
+
# for. This runs before the `ensure` below finishes the event, so the
|
|
1069
|
+
# attribute lands on the event's own span. The error itself is not reported
|
|
1070
|
+
# here; whatever catches it decides that.
|
|
1071
|
+
#
|
|
1072
|
+
# A paused transaction never started an event span, so there would be no
|
|
1073
|
+
# span of this event's to describe.
|
|
1074
|
+
unless paused?
|
|
1075
|
+
add_opentelemetry_attributes(
|
|
1076
|
+
Appsignal::OpenTelemetry::ErrorType.attributes_for(error.class.name)
|
|
1077
|
+
)
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
raise
|
|
696
1081
|
ensure
|
|
697
1082
|
finish_event(name, title, body, body_format)
|
|
698
1083
|
end
|
|
699
1084
|
|
|
700
1085
|
# @!visibility private
|
|
701
1086
|
def to_h
|
|
702
|
-
JSON.parse(@
|
|
1087
|
+
JSON.parse(@backend.to_json)
|
|
703
1088
|
end
|
|
704
1089
|
alias to_hash to_h
|
|
705
1090
|
|
|
706
1091
|
protected
|
|
707
1092
|
|
|
708
1093
|
# @!visibility private
|
|
709
|
-
attr_writer :is_duplicate, :tags, :custom_data, :
|
|
710
|
-
:session_data, :
|
|
1094
|
+
attr_writer :is_duplicate, :tags, :custom_data, :params_buckets,
|
|
1095
|
+
:session_data, :headers_buckets, :channels_set
|
|
711
1096
|
|
|
712
1097
|
# @!visibility private
|
|
713
1098
|
def internal_set_error(error, &block)
|
|
714
|
-
|
|
1099
|
+
is_new_error = !@errors.include?(error)
|
|
715
1100
|
|
|
716
|
-
if
|
|
1101
|
+
if is_new_error && @errors.length >= ERRORS_LIMIT
|
|
717
1102
|
Appsignal.internal_logger.warn "Appsignal::Transaction#add_error: Transaction has more " \
|
|
718
1103
|
"than #{ERRORS_LIMIT} distinct errors. Only the first " \
|
|
719
1104
|
"#{ERRORS_LIMIT} distinct errors will be reported."
|
|
720
1105
|
return
|
|
721
1106
|
end
|
|
722
|
-
|
|
723
|
-
@
|
|
1107
|
+
|
|
1108
|
+
if @errors.empty?
|
|
1109
|
+
_set_error(error)
|
|
1110
|
+
elsif is_new_error && @backend.supports_multiple_errors?
|
|
1111
|
+
# Record additional errors immediately so each exception event lands on
|
|
1112
|
+
# the span current now, not the root span at completion. The agent
|
|
1113
|
+
# backend instead reports extras as duplicate transactions.
|
|
1114
|
+
_send_error_to_backend(error)
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
@errors.add(error)
|
|
1118
|
+
|
|
1119
|
+
if @backend.supports_multiple_errors?
|
|
1120
|
+
# Collector mode: the error is already recorded, so run its block now
|
|
1121
|
+
# rather than at completion. Anything a block attaches to the current
|
|
1122
|
+
# span -- breadcrumbs, nested errors, custom instrumentation -- then
|
|
1123
|
+
# lands where the error was reported, not on the root span at
|
|
1124
|
+
# completion.
|
|
1125
|
+
if block
|
|
1126
|
+
self.class.with_transaction(self) do
|
|
1127
|
+
block.call(self)
|
|
1128
|
+
end
|
|
1129
|
+
end
|
|
1130
|
+
else
|
|
1131
|
+
@error_blocks[error] << block
|
|
1132
|
+
@error_blocks[error].compact!
|
|
1133
|
+
end
|
|
724
1134
|
end
|
|
725
1135
|
|
|
726
1136
|
private
|
|
727
1137
|
|
|
728
|
-
|
|
1138
|
+
# The `SampleData` bucket a logical params channel is stored in, per the
|
|
1139
|
+
# backend's `params_mapping`. In agent mode every channel resolves to the
|
|
1140
|
+
# same `:params` bucket (so they merge and share an `_if_nil` guard); in
|
|
1141
|
+
# collector mode the request payload, function parameters and query
|
|
1142
|
+
# parameters resolve to separate buckets. `fetch` raises if a backend's
|
|
1143
|
+
# mapping omits a channel.
|
|
1144
|
+
def params_data(channel)
|
|
1145
|
+
@params_buckets.fetch(@params_mapping.fetch(channel))
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
PARAMS_CHANNEL_ALIASES = { :params => :request_payload }.freeze
|
|
1149
|
+
private_constant :PARAMS_CHANNEL_ALIASES
|
|
1150
|
+
|
|
1151
|
+
def params_channel(channel)
|
|
1152
|
+
PARAMS_CHANNEL_ALIASES.fetch(channel, channel)
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def add_params_channel(channel, given_params = nil, &block)
|
|
1156
|
+
sample = params_data(channel)
|
|
1157
|
+
sample.add(given_params, &block)
|
|
1158
|
+
mark_channel_set(params_channel(channel)) if sample.value?
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
def params_unset?(channel)
|
|
1162
|
+
!channel_set?(params_channel(channel)) && !params_data(channel).empty?
|
|
1163
|
+
end
|
|
729
1164
|
|
|
1165
|
+
# `add_params`/`set_params` don't say whether the params are a request
|
|
1166
|
+
# payload or function parameters, so in collector mode they always map to
|
|
1167
|
+
# the request payload. Warn once per process to nudge callers toward the
|
|
1168
|
+
# explicit methods.
|
|
1169
|
+
def warn_params_deprecation
|
|
1170
|
+
return unless Appsignal.config&.collector_mode?
|
|
1171
|
+
|
|
1172
|
+
Appsignal::Transaction.warn_params_deprecation_once do
|
|
1173
|
+
Appsignal::Utils::StdoutAndLoggerMessage.warning(
|
|
1174
|
+
"`add_params`/`set_params` is deprecated in collector mode. Use " \
|
|
1175
|
+
"`add_request_payload` or `add_function_parameters` instead, or " \
|
|
1176
|
+
"`add_custom_data` for data that is neither."
|
|
1177
|
+
)
|
|
1178
|
+
end
|
|
1179
|
+
end
|
|
1180
|
+
|
|
1181
|
+
# Wrap a block handed to the transaction by user code so that, wherever it
|
|
1182
|
+
# later runs, a failure is logged and swallowed instead of breaking the
|
|
1183
|
+
# transaction lifecycle. A raise would otherwise skip the rest of creation or
|
|
1184
|
+
# completion, including the backend teardown that detaches the transaction's
|
|
1185
|
+
# OpenTelemetry context, which would then become the parent of the next
|
|
1186
|
+
# request's spans on that thread. Re-raising is wrong because the block runs
|
|
1187
|
+
# far from where its caller defined it.
|
|
1188
|
+
#
|
|
1189
|
+
# Returns `nil` when no block is given, so it can wrap an optional block.
|
|
1190
|
+
def protect(description, &block)
|
|
1191
|
+
return unless block
|
|
1192
|
+
|
|
1193
|
+
proc do |*args|
|
|
1194
|
+
block.call(*args)
|
|
1195
|
+
rescue => error
|
|
1196
|
+
location = block.source_location&.join(":") || "an unknown location"
|
|
1197
|
+
Appsignal.internal_logger.error(
|
|
1198
|
+
"Error in #{description}, defined at #{location}: " \
|
|
1199
|
+
"#{error.class}: #{error.message}\n#{error.backtrace&.join("\n")}"
|
|
1200
|
+
)
|
|
1201
|
+
end
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
# Hooks are registered both as blocks and as method objects pushed onto the
|
|
1205
|
+
# set directly, so there is no single entry point to wrap them at. They are
|
|
1206
|
+
# protected here instead, at the one place that runs all of them.
|
|
730
1207
|
def run_after_create_hooks
|
|
731
1208
|
self.class.after_create.each do |block|
|
|
732
|
-
block.call(self)
|
|
1209
|
+
protect("the after_create hook", &block).call(self)
|
|
733
1210
|
end
|
|
734
1211
|
end
|
|
735
1212
|
|
|
736
1213
|
def run_before_complete_hooks
|
|
737
1214
|
self.class.before_complete.each do |block|
|
|
738
|
-
block.call(self, @error_set)
|
|
1215
|
+
protect("the before_complete hook", &block).call(self, @error_set)
|
|
1216
|
+
end
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
# Reports the errors stored on the transaction at completion.
|
|
1220
|
+
#
|
|
1221
|
+
# In eager (collector) mode nothing is left to do: each error was recorded
|
|
1222
|
+
# and its block run when the error was added. In deferred (agent) mode the
|
|
1223
|
+
# extension holds a single error, so the primary error's blocks run on this
|
|
1224
|
+
# transaction and every additional error is reported as a duplicate
|
|
1225
|
+
# transaction.
|
|
1226
|
+
def report_errors
|
|
1227
|
+
return if @backend.supports_multiple_errors?
|
|
1228
|
+
|
|
1229
|
+
report_errors_as_duplicates
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
# Agent-only legacy path. The extension transaction holds a single error, so
|
|
1233
|
+
# extra errors are reported as duplicate transactions. This whole method
|
|
1234
|
+
# disappears once agent mode is dropped: collector mode records every error
|
|
1235
|
+
# eagerly and leaves nothing to do at completion.
|
|
1236
|
+
def report_errors_as_duplicates
|
|
1237
|
+
@errors.each do |error|
|
|
1238
|
+
# Ignore the error that is already set in this transaction.
|
|
1239
|
+
next if error == @error_set
|
|
1240
|
+
|
|
1241
|
+
duplicate.tap do |transaction|
|
|
1242
|
+
# In the duplicate transaction for each error, set an error
|
|
1243
|
+
# with a block that calls all the blocks set for that error
|
|
1244
|
+
# in the original transaction. Those blocks were already wrapped
|
|
1245
|
+
# when they were added, so they are called directly here.
|
|
1246
|
+
transaction.internal_set_error(error) do
|
|
1247
|
+
@error_blocks[error].each do |block|
|
|
1248
|
+
block.call(transaction)
|
|
1249
|
+
end
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
transaction.complete
|
|
1253
|
+
end
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
return unless @error_set && @error_blocks[@error_set].any?
|
|
1257
|
+
|
|
1258
|
+
self.class.with_transaction(self) do
|
|
1259
|
+
@error_blocks[@error_set].each do |block|
|
|
1260
|
+
block.call(self)
|
|
1261
|
+
end
|
|
739
1262
|
end
|
|
740
1263
|
end
|
|
741
1264
|
|
|
742
1265
|
def _set_error(error)
|
|
743
|
-
|
|
744
|
-
|
|
1266
|
+
@error_set = error
|
|
1267
|
+
_send_error_to_backend(error)
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
# Records an error on the backend. The cause chain is walked once into
|
|
1271
|
+
# neutral data ({name, message, backtrace}); each backend projects what it
|
|
1272
|
+
# needs -- the agent's first-line `error_causes` sample data, or the
|
|
1273
|
+
# OpenTelemetry `appsignal.error_causes` attribute. Called for the first
|
|
1274
|
+
# error and, in collector mode, for each additional error as it is added.
|
|
1275
|
+
def _send_error_to_backend(error)
|
|
1276
|
+
causes, root_cause_missing = _error_causes(error)
|
|
1277
|
+
@backend.set_error(
|
|
745
1278
|
error.class.name,
|
|
746
1279
|
cleaned_error_message(error),
|
|
747
|
-
|
|
1280
|
+
cleaned_backtrace(error.backtrace),
|
|
1281
|
+
causes.map do |cause|
|
|
1282
|
+
{
|
|
1283
|
+
:name => cause.class.name,
|
|
1284
|
+
:message => cleaned_error_message(cause),
|
|
1285
|
+
:backtrace => cleaned_backtrace(cause.backtrace)
|
|
1286
|
+
}
|
|
1287
|
+
end,
|
|
1288
|
+
root_cause_missing
|
|
748
1289
|
)
|
|
749
|
-
|
|
1290
|
+
end
|
|
750
1291
|
|
|
1292
|
+
# Walks the `error.cause` chain (without mutating `error`), collecting up to
|
|
1293
|
+
# `ERROR_CAUSES_LIMIT` causes. Returns the causes and whether the chain was
|
|
1294
|
+
# truncated (the root cause is missing).
|
|
1295
|
+
def _error_causes(error)
|
|
751
1296
|
root_cause_missing = false
|
|
752
|
-
|
|
753
1297
|
causes = []
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
break unless error
|
|
758
|
-
|
|
1298
|
+
cause = error
|
|
1299
|
+
while (cause = cause.cause)
|
|
759
1300
|
if causes.length >= ERROR_CAUSES_LIMIT
|
|
760
1301
|
Appsignal.internal_logger.debug "Appsignal::Transaction#add_error: Error has more " \
|
|
761
1302
|
"than #{ERROR_CAUSES_LIMIT} error causes. Only the first #{ERROR_CAUSES_LIMIT} " \
|
|
@@ -764,55 +1305,10 @@ module Appsignal
|
|
|
764
1305
|
break
|
|
765
1306
|
end
|
|
766
1307
|
|
|
767
|
-
causes <<
|
|
1308
|
+
causes << cause
|
|
768
1309
|
end
|
|
769
1310
|
|
|
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
|
|
1311
|
+
[causes, root_cause_missing]
|
|
816
1312
|
end
|
|
817
1313
|
|
|
818
1314
|
def set_sample_data(key, data)
|
|
@@ -825,10 +1321,11 @@ module Appsignal
|
|
|
825
1321
|
return
|
|
826
1322
|
end
|
|
827
1323
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1324
|
+
# Pass raw Ruby through to the backend. ExtensionBackend serializes to a
|
|
1325
|
+
# C-extension `Data` object; OpenTelemetryBackend reads the Hash/Array
|
|
1326
|
+
# directly. The `RuntimeError` rescue still covers ExtensionBackend's
|
|
1327
|
+
# `Data.generate`, which now runs inside the backend call.
|
|
1328
|
+
@backend.set_sample_data(key.to_s, data)
|
|
832
1329
|
rescue RuntimeError => e
|
|
833
1330
|
begin
|
|
834
1331
|
inspected_data = data.inspect
|
|
@@ -843,15 +1340,24 @@ module Appsignal
|
|
|
843
1340
|
end
|
|
844
1341
|
|
|
845
1342
|
def sample_data
|
|
846
|
-
{
|
|
847
|
-
|
|
848
|
-
|
|
1343
|
+
data = {}
|
|
1344
|
+
@headers_buckets.each do |bucket, sample|
|
|
1345
|
+
data[bucket] = sanitized_headers(bucket, sample)
|
|
1346
|
+
end
|
|
1347
|
+
data.merge!(
|
|
849
1348
|
:session_data => sanitized_session_data,
|
|
850
1349
|
:tags => sanitized_tags,
|
|
851
|
-
:breadcrumbs => breadcrumbs,
|
|
852
1350
|
:custom_data => custom_data
|
|
853
|
-
|
|
854
|
-
|
|
1351
|
+
)
|
|
1352
|
+
# Each params bucket is emitted under its own key. The extension backend
|
|
1353
|
+
# has a single `:params` bucket; the OpenTelemetry backend has separate
|
|
1354
|
+
# `:request_payload` and `:function_parameters` buckets. The backend maps
|
|
1355
|
+
# each key to its storage (C-extension slot or OpenTelemetry attribute).
|
|
1356
|
+
@params_buckets.each do |bucket, sample|
|
|
1357
|
+
data[bucket] = sanitized_params(bucket, sample)
|
|
1358
|
+
end
|
|
1359
|
+
data.each do |key, value|
|
|
1360
|
+
set_sample_data(key, value)
|
|
855
1361
|
end
|
|
856
1362
|
end
|
|
857
1363
|
|
|
@@ -860,30 +1366,40 @@ module Appsignal
|
|
|
860
1366
|
self.class.new(
|
|
861
1367
|
namespace,
|
|
862
1368
|
:id => new_transaction_id,
|
|
863
|
-
:
|
|
1369
|
+
:backend => @backend.duplicate(new_transaction_id)
|
|
864
1370
|
).tap do |transaction|
|
|
865
1371
|
transaction.is_duplicate = true
|
|
866
1372
|
transaction.tags = @tags.dup
|
|
867
1373
|
transaction.custom_data = @custom_data.dup
|
|
868
|
-
transaction.
|
|
869
|
-
transaction.params = @params.dup
|
|
1374
|
+
transaction.params_buckets = @params_buckets.transform_values(&:dup)
|
|
870
1375
|
transaction.session_data = @session_data.dup
|
|
871
|
-
transaction.
|
|
1376
|
+
transaction.headers_buckets = @headers_buckets.transform_values(&:dup)
|
|
1377
|
+
transaction.channels_set = @channels_set.dup
|
|
872
1378
|
end
|
|
873
1379
|
end
|
|
874
1380
|
|
|
875
1381
|
def params
|
|
876
|
-
|
|
877
|
-
rescue => e
|
|
878
|
-
Appsignal.internal_logger.error("Exception while fetching params: #{e.class}: #{e}")
|
|
879
|
-
nil
|
|
1382
|
+
params_value(params_data(:params))
|
|
880
1383
|
end
|
|
881
1384
|
|
|
882
|
-
def sanitized_params
|
|
883
|
-
|
|
1385
|
+
def sanitized_params(bucket, sample)
|
|
1386
|
+
options = @params_options.fetch(bucket)
|
|
1387
|
+
return if Appsignal.config[options.fetch(:send)] == false
|
|
1388
|
+
|
|
1389
|
+
filter_keys = Appsignal.config[options.fetch(:filter)] || []
|
|
1390
|
+
Appsignal::Utils::SampleDataSanitizer.sanitize(params_value(sample), filter_keys)
|
|
1391
|
+
end
|
|
884
1392
|
|
|
885
|
-
|
|
886
|
-
|
|
1393
|
+
# Reads a params bucket's value. Evaluating it runs any block the caller
|
|
1394
|
+
# passed to `add_params`/`add_request_payload`/`add_function_parameters`,
|
|
1395
|
+
# which is user code that can raise, so a failure is logged and swallowed.
|
|
1396
|
+
def params_value(sample)
|
|
1397
|
+
sample.value
|
|
1398
|
+
rescue => e
|
|
1399
|
+
Appsignal.internal_logger.error(
|
|
1400
|
+
"Exception while fetching params (#{sample.key}): #{e.class}: #{e}"
|
|
1401
|
+
)
|
|
1402
|
+
nil
|
|
887
1403
|
end
|
|
888
1404
|
|
|
889
1405
|
def session_data
|
|
@@ -912,32 +1428,66 @@ module Appsignal
|
|
|
912
1428
|
)
|
|
913
1429
|
end
|
|
914
1430
|
|
|
915
|
-
def
|
|
916
|
-
@
|
|
1431
|
+
def add_headers_channel(channel, given_headers = nil, &block)
|
|
1432
|
+
bucket, transform = @headers_mapping.fetch(channel)
|
|
1433
|
+
sample = @headers_buckets.fetch(bucket)
|
|
1434
|
+
|
|
1435
|
+
if transform.nil?
|
|
1436
|
+
sample.add(given_headers, &block)
|
|
1437
|
+
elsif block
|
|
1438
|
+
sample.add { transform_headers(transform, block.call) }
|
|
1439
|
+
else
|
|
1440
|
+
sample.add(transform_headers(transform, given_headers))
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
mark_channel_set(channel) if sample.value?
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
def transform_headers(transform, headers)
|
|
1447
|
+
return headers unless headers.is_a?(Hash)
|
|
1448
|
+
|
|
1449
|
+
headers.to_h(&transform)
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
def mark_channel_set(channel)
|
|
1453
|
+
@channels_set << channel unless @channels_set.include?(channel)
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
def channel_set?(channel)
|
|
1457
|
+
@channels_set.include?(channel)
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
def headers_value(sample)
|
|
1461
|
+
sample.value
|
|
917
1462
|
rescue => e
|
|
918
1463
|
Appsignal.internal_logger.error \
|
|
919
1464
|
"Exception while fetching headers: #{e.class}: #{e}"
|
|
920
1465
|
nil
|
|
921
1466
|
end
|
|
922
1467
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
# The environment of a transaction can contain a lot of information, not
|
|
926
|
-
# all of it useful for debugging.
|
|
927
|
-
#
|
|
928
|
-
# @return [nil] if no environment is present.
|
|
929
|
-
# @return [Hash<String, Object>]
|
|
930
|
-
def sanitized_request_headers
|
|
931
|
-
headers = request_headers
|
|
1468
|
+
def sanitized_headers(bucket, sample)
|
|
1469
|
+
headers = headers_value(sample)
|
|
932
1470
|
return unless headers
|
|
933
1471
|
|
|
1472
|
+
option, header_names = @headers_allowlist.fetch(bucket)
|
|
1473
|
+
allowlist = Appsignal.config[option]
|
|
1474
|
+
|
|
1475
|
+
headers = normalized_headers(headers) if header_names
|
|
1476
|
+
|
|
934
1477
|
{}.tap do |out|
|
|
935
|
-
|
|
1478
|
+
allowlist.each do |key|
|
|
1479
|
+
key = Appsignal::Utils::RequestHeaders.normalize(key) if header_names
|
|
936
1480
|
out[key] = headers[key] if headers[key]
|
|
937
1481
|
end
|
|
938
1482
|
end
|
|
939
1483
|
end
|
|
940
1484
|
|
|
1485
|
+
def normalized_headers(headers)
|
|
1486
|
+
headers.to_h do |key, value|
|
|
1487
|
+
[Appsignal::Utils::RequestHeaders.normalize(key), value]
|
|
1488
|
+
end
|
|
1489
|
+
end
|
|
1490
|
+
|
|
941
1491
|
# Only keep tags if they meet the following criteria:
|
|
942
1492
|
# * Key is a symbol or string with less then 100 chars
|
|
943
1493
|
# * Value is a symbol or string with less then 100 chars
|