appsignal 4.10.1-java → 5.0.0.rc.1-java

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