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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/Rakefile +195 -4
  4. data/appsignal.gemspec +8 -0
  5. data/build_matrix.yml +12 -0
  6. data/ext/appsignal_extension.c +14 -0
  7. data/lib/appsignal/backends.rb +55 -0
  8. data/lib/appsignal/cli/diagnose.rb +2 -10
  9. data/lib/appsignal/config.rb +377 -13
  10. data/lib/appsignal/demo.rb +12 -10
  11. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
  12. data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
  13. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
  14. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
  15. data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
  16. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
  17. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
  18. data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
  19. data/lib/appsignal/event_formatter.rb +78 -0
  20. data/lib/appsignal/extension.rb +4 -0
  21. data/lib/appsignal/helpers/instrumentation.rb +324 -20
  22. data/lib/appsignal/helpers/metrics.rb +3 -24
  23. data/lib/appsignal/hooks/action_cable.rb +26 -8
  24. data/lib/appsignal/hooks/active_job.rb +184 -47
  25. data/lib/appsignal/hooks/at_exit.rb +4 -1
  26. data/lib/appsignal/hooks/excon.rb +20 -0
  27. data/lib/appsignal/hooks/faraday.rb +16 -0
  28. data/lib/appsignal/hooks/http.rb +5 -0
  29. data/lib/appsignal/hooks/resque.rb +1 -1
  30. data/lib/appsignal/hooks/sequel.rb +32 -2
  31. data/lib/appsignal/hooks/shoryuken.rb +3 -3
  32. data/lib/appsignal/hooks/sidekiq.rb +1 -1
  33. data/lib/appsignal/integrations/action_cable.rb +5 -2
  34. data/lib/appsignal/integrations/active_support_notifications.rb +59 -19
  35. data/lib/appsignal/integrations/data_mapper.rb +14 -2
  36. data/lib/appsignal/integrations/delayed_job_plugin.rb +81 -8
  37. data/lib/appsignal/integrations/dry_monitor.rb +39 -15
  38. data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
  39. data/lib/appsignal/integrations/excon.rb +52 -15
  40. data/lib/appsignal/integrations/faraday.rb +47 -12
  41. data/lib/appsignal/integrations/http.rb +43 -1
  42. data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
  43. data/lib/appsignal/integrations/net_http.rb +31 -2
  44. data/lib/appsignal/integrations/puma.rb +4 -1
  45. data/lib/appsignal/integrations/que.rb +256 -37
  46. data/lib/appsignal/integrations/railtie.rb +4 -1
  47. data/lib/appsignal/integrations/rake.rb +9 -3
  48. data/lib/appsignal/integrations/redis.rb +22 -1
  49. data/lib/appsignal/integrations/redis_client.rb +22 -1
  50. data/lib/appsignal/integrations/resque.rb +81 -11
  51. data/lib/appsignal/integrations/shoryuken.rb +159 -12
  52. data/lib/appsignal/integrations/sidekiq.rb +94 -16
  53. data/lib/appsignal/integrations/webmachine.rb +56 -5
  54. data/lib/appsignal/loaders/padrino.rb +2 -1
  55. data/lib/appsignal/logger/extension_backend.rb +24 -0
  56. data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
  57. data/lib/appsignal/logger.rb +13 -9
  58. data/lib/appsignal/metrics/extension_backend.rb +47 -0
  59. data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
  60. data/lib/appsignal/opentelemetry/attributes.rb +31 -0
  61. data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
  62. data/lib/appsignal/opentelemetry/error_type.rb +37 -0
  63. data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
  64. data/lib/appsignal/opentelemetry/http_method.rb +59 -0
  65. data/lib/appsignal/opentelemetry/http_response.rb +30 -0
  66. data/lib/appsignal/opentelemetry/http_server_request.rb +79 -0
  67. data/lib/appsignal/opentelemetry/messaging.rb +82 -0
  68. data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
  69. data/lib/appsignal/opentelemetry/rendering.rb +29 -0
  70. data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
  71. data/lib/appsignal/opentelemetry.rb +495 -0
  72. data/lib/appsignal/rack/abstract_middleware.rb +66 -4
  73. data/lib/appsignal/rack/body_wrapper.rb +18 -5
  74. data/lib/appsignal/rack/event_handler.rb +44 -4
  75. data/lib/appsignal/rack/grape_middleware.rb +1 -0
  76. data/lib/appsignal/rack/hanami_middleware.rb +2 -1
  77. data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
  78. data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
  79. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
  80. data/lib/appsignal/rack.rb +68 -12
  81. data/lib/appsignal/sample_data.rb +4 -0
  82. data/lib/appsignal/transaction/base_backend.rb +128 -0
  83. data/lib/appsignal/transaction/extension_backend.rb +229 -0
  84. data/lib/appsignal/transaction/opentelemetry_backend.rb +847 -0
  85. data/lib/appsignal/transaction.rb +714 -164
  86. data/lib/appsignal/utils/request_headers.rb +78 -0
  87. data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
  88. data/lib/appsignal/utils.rb +1 -0
  89. data/lib/appsignal/version.rb +1 -1
  90. data/lib/appsignal.rb +10 -0
  91. data/sig/appsignal.rbi +630 -37
  92. data/sig/appsignal.rbs +582 -27
  93. 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(namespace)
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(Appsignal::Transaction.new(namespace))
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(namespace, id: SecureRandom.uuid, ext: nil)
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
- @ext = ext || Appsignal::Extension.start_transaction(
248
+ @backend = backend || Appsignal::Backends.transaction.new(
183
249
  @transaction_id,
184
250
  @namespace,
185
- 0
186
- ) || Appsignal::Extension::MockTransaction.new
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 = @error_blocks.keys
223
- should_sample = @ext.finish(0)
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
- 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
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
- @ext.complete
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
- @params.add(given_params, &block)
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
- @params.set_empty_value!
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 !@params.value? && !@params.empty?
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
- @headers.add(given_headers, &block)
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
- add_headers(given_headers, &block) unless @headers.value?
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
- @breadcrumbs.push(
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
- @ext.set_action(action)
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
- @ext.set_namespace(namespace)
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
- @ext.set_queue_start(start)
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
- @ext.set_metadata(key, value)
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) && !@error_blocks.include?(error)
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
- @ext.start_event(0)
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
- @ext.finish_event(
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(name, title, body, duration, body_format = Appsignal::EventFormatter::DEFAULT)
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
- @ext.record_event(
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
- 0
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(name, title = nil, body = nil, body_format = Appsignal::EventFormatter::DEFAULT)
694
- start_event
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(@ext.to_json)
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, :breadcrumbs, :params,
710
- :session_data, :headers
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
- _set_error(error) if @error_blocks.empty?
1099
+ is_new_error = !@errors.include?(error)
715
1100
 
716
- if !@error_blocks.include?(error) && @error_blocks.length >= ERRORS_LIMIT
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
- @error_blocks[error] << block
723
- @error_blocks[error].compact!
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
- attr_reader :breadcrumbs
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
- backtrace = cleaned_backtrace(error.backtrace)
744
- @ext.set_error(
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
- backtrace ? Appsignal::Utils::Data.generate(backtrace) : Appsignal::Extension.data_array_new
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
- @error_set = error
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
- while error
755
- error = error.cause
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 << error
1308
+ causes << cause
768
1309
  end
769
1310
 
770
- causes_sample_data = causes.map do |e|
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
- @ext.set_sample_data(
829
- key.to_s,
830
- Appsignal::Utils::Data.generate(data)
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
- :params => sanitized_params,
848
- :environment => sanitized_request_headers,
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
- }.each do |key, data|
854
- set_sample_data(key, data)
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
- :ext => @ext.duplicate(new_transaction_id)
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.breadcrumbs = @breadcrumbs.dup
869
- transaction.params = @params.dup
1374
+ transaction.params_buckets = @params_buckets.transform_values(&:dup)
870
1375
  transaction.session_data = @session_data.dup
871
- transaction.headers = @headers.dup
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
- @params.value
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
- return unless Appsignal.config[:send_params]
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
- filter_keys = Appsignal.config[:filter_parameters] || []
886
- Appsignal::Utils::SampleDataSanitizer.sanitize(params, filter_keys)
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 request_headers
916
- @headers.value
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
- # Returns sanitized environment for a transaction.
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
- Appsignal.config[:request_headers].each do |key|
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