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
@@ -2,21 +2,184 @@
2
2
 
3
3
  module Appsignal
4
4
  module Integrations
5
+ # @!visibility private
6
+ #
7
+ # Reads and writes W3C trace context the way OpenTelemetry's Que
8
+ # instrumentation does: as `"key:value"` strings in the job's tags array
9
+ # (the only carrier Que's enqueue API exposes). Collector mode only.
10
+ module QueTraceContext
11
+ module_function
12
+
13
+ # Que has no header map, so context rides in the tags array. OTel writes
14
+ # each header as a `"key:value"` tag; mirror that exact format.
15
+ module TagSetter
16
+ def self.set(carrier, key, value)
17
+ carrier << "#{key}:#{value}"
18
+ end
19
+ end
20
+
21
+ # Que rejects jobs with too many or too-long tags, so injected context
22
+ # must stay within these or the enqueue would raise. Read the limits from
23
+ # Que when available, with the documented defaults as a fallback.
24
+ MAX_TAGS_COUNT =
25
+ defined?(::Que::Job::MAXIMUM_TAGS_COUNT) ? ::Que::Job::MAXIMUM_TAGS_COUNT : 5
26
+ MAX_TAG_LENGTH =
27
+ defined?(::Que::Job::MAXIMUM_TAG_LENGTH) ? ::Que::Job::MAXIMUM_TAG_LENGTH : 100
28
+
29
+ # Marks a job as one of a batch enqueued by `bulk_enqueue`, so the worker
30
+ # can tell the two enqueue paths apart. Que itself records nothing that
31
+ # distinguishes them: both insert paths write the same columns, and the
32
+ # job's `data` only ever holds its tags. So the enqueue side has to say so.
33
+ #
34
+ # This deliberately contains no colon. The trace context rides in the same
35
+ # tags array as `"key:value"` strings, so a reader that splits tags on the
36
+ # colon to rebuild the carrier drops this tag on its own. That keeps it out
37
+ # of the carrier for us and for OpenTelemetry's own Que instrumentation.
38
+ BULK_TAG = "appsignal.bulk_enqueue"
39
+
40
+ # Que only has tags from version 1.0 on, and they are the only carrier its
41
+ # enqueue API exposes. On Que 0.x there is nowhere to put the trace context
42
+ # that survives to the worker, so propagation is skipped there. Writing the
43
+ # context into the job's arguments instead would change the arguments the
44
+ # job is called with, which breaks the job.
45
+ TAGS_SUPPORTED = defined?(::Que::Job::MAXIMUM_TAGS_COUNT)
46
+
47
+ # Read the incoming context off the job's tags. Splits each `"key:value"`
48
+ # tag on the first colon back into a carrier hash, then extracts. Returns
49
+ # an `OpenTelemetry::Context`, or `nil` outside collector mode and on Que
50
+ # versions without tags.
51
+ def extract(tags)
52
+ return unless TAGS_SUPPORTED
53
+
54
+ Appsignal::OpenTelemetry.if_started do
55
+ carrier = Array(tags)
56
+ .map { |tag| tag.split(":", 2) }
57
+ .select { |pair| pair.size == 2 }
58
+ .to_h
59
+ ::OpenTelemetry.propagation.extract(carrier)
60
+ end
61
+ end
62
+
63
+ # Returns the tags array to enqueue the job with. In collector mode that is
64
+ # a copy with the current context injected, kept only if it still fits
65
+ # Que's limits: propagation is skipped rather than break the enqueue.
66
+ # Outside collector mode, and on Que versions without tags, the tags are
67
+ # returned unchanged.
68
+ #
69
+ # Pass `bulk` for a `bulk_enqueue` batch, which adds the bulk marker too.
70
+ # Both are kept or dropped together, because a batch missing its marker
71
+ # would parent every job in it to the one producer span.
72
+ def inject(tags, bulk: false)
73
+ original = Array(tags)
74
+ return original unless TAGS_SUPPORTED
75
+
76
+ injected = Appsignal::OpenTelemetry.if_started do
77
+ copy = original.dup
78
+ ::OpenTelemetry.propagation.inject(copy, :setter => TagSetter)
79
+ # The marker has nothing to link back to without a trace context, so
80
+ # only add it when the context was actually injected. The propagator
81
+ # writes nothing when there is no valid span to propagate.
82
+ copy << BULK_TAG if bulk && copy.length > original.length
83
+ copy
84
+ end
85
+ return original if injected.nil? || !within_limits?(injected)
86
+
87
+ injected
88
+ end
89
+
90
+ # Whether the job being performed was enqueued as part of a batch, which
91
+ # the enqueue side records by adding `BULK_TAG` to the job's tags.
92
+ def bulk?(tags)
93
+ Array(tags).include?(BULK_TAG)
94
+ end
95
+
96
+ # The class the Active Job adapter enqueues, with the serialized job data
97
+ # as its only argument. Que records the name in the job's `job_class`, and
98
+ # the name is fixed, because it is stored in every job record the adapter
99
+ # has ever written.
100
+ ACTIVE_JOB_WRAPPER = "ActiveJob::QueueAdapters::QueAdapter::JobWrapper"
101
+
102
+ # The trace context to continue: the Active Job layer when this is an
103
+ # Active Job job, the job's own tags otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
104
+ # for why that layer wins.
105
+ # Que's tags are a carrier with a hard limit: five per job, shared with
106
+ # whatever the user puts there.
107
+ def extract_context(job_data, tags)
108
+ Appsignal::OpenTelemetry.extract_active_job_context(job_data) || extract(tags)
109
+ end
110
+
111
+ # The serialized Active Job job data inside a Que job, or nil when this is
112
+ # not an Active Job job.
113
+ #
114
+ # Que reads a job's arguments out of JSONB with symbol keys and only turns
115
+ # them back into strings on the way into Active Job, so they are
116
+ # stringified here too. Only the outer keys need it: the headers below them
117
+ # are an array of pairs of strings, which Que leaves alone.
118
+ def active_job_data(attrs)
119
+ return unless attrs[:job_class] == ACTIVE_JOB_WRAPPER
120
+
121
+ job_data = Array(attrs[:args]).first
122
+ job_data.transform_keys(&:to_s) if job_data.is_a?(Hash)
123
+ end
124
+
125
+ def within_limits?(tags)
126
+ tags.length <= MAX_TAGS_COUNT && tags.all? { |tag| tag.length <= MAX_TAG_LENGTH }
127
+ end
128
+ end
129
+
5
130
  # @!visibility private
6
131
  module QuePlugin
7
132
  def _run(*args)
133
+ local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
134
+ tags = local_attrs.dig(:data, :tags)
135
+
136
+ job_data = QueTraceContext.active_job_data(local_attrs)
137
+ # A Que batch says so with a tag, and an Active Job batch says so in the
138
+ # job data, so both have to be asked. See
139
+ # `Appsignal::OpenTelemetry.active_job_relationship` for why a batch
140
+ # links back rather than parenting under the span that enqueued it.
141
+ relationship =
142
+ if QueTraceContext.bulk?(tags)
143
+ :link
144
+ else
145
+ Appsignal::OpenTelemetry.active_job_relationship(job_data)
146
+ end
147
+
148
+ # Read the incoming trace context off the job's tags so the transaction
149
+ # links back to the enqueuer. No-op outside collector mode.
8
150
  transaction =
9
- Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
151
+ Appsignal::Transaction.create(
152
+ Appsignal::Transaction::BACKGROUND_JOB,
153
+ :opentelemetry_context => QueTraceContext.extract_context(job_data, tags),
154
+ :opentelemetry_scope => ["appsignal-ruby/que", Appsignal::VERSION],
155
+ :opentelemetry_kind => :consumer,
156
+ :opentelemetry_relationship => relationship
157
+ )
158
+ # Describes this span as a job being performed. The messaging system is
159
+ # what the trace timeline reads to recognize background job work, and
160
+ # `que` is the value OpenTelemetry's own Que instrumentation uses.
161
+ transaction.add_opentelemetry_attributes(
162
+ Appsignal::OpenTelemetry::Messaging
163
+ .perform_attributes("que", :destination => local_attrs[:queue])
164
+ )
10
165
 
11
166
  begin
12
- Appsignal.instrument("perform_job.que") { super }
167
+ Appsignal.instrument(
168
+ "perform_job.que",
169
+ :opentelemetry_scope => ["appsignal-ruby/que", Appsignal::VERSION]
170
+ ) do
171
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
172
+ Appsignal::OpenTelemetry::Messaging
173
+ .perform_attributes("que", :destination => local_attrs[:queue])
174
+ )
175
+ super
176
+ end
13
177
  rescue Exception => error
14
178
  transaction.set_error(error)
15
179
  raise error
16
180
  ensure
17
- local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
18
181
  transaction.set_action_if_nil("#{local_attrs[:job_class]}#run")
19
- transaction.add_params_if_nil do
182
+ transaction.add_function_parameters_if_nil do
20
183
  {
21
184
  :arguments => local_attrs[:args]
22
185
  }.tap do |hash|
@@ -37,36 +200,91 @@ module Appsignal
37
200
 
38
201
  # @!visibility private
39
202
  #
40
- # Prepended to `Que::Job`'s singleton so it records each enqueue as an
41
- # `enqueue.que` event under the active transaction. Like all AppSignal
42
- # events, it only records when there's an active transaction (e.g. enqueuing
43
- # from within a web request or another job); otherwise it's a transparent
44
- # pass-through.
203
+ # Prepended to `Que::Job`'s singleton so it wraps enqueues. Records the
204
+ # enqueue as an AppSignal event (a producer span in collector mode), and in
205
+ # collector mode writes the current trace context onto the job's tags so the
206
+ # job that later performs links back to it. Like all AppSignal events, the
207
+ # enqueue only records when there's an active transaction; otherwise it's a
208
+ # transparent pass-through.
45
209
  module QueClientPlugin
46
- # The keyword arguments are captured into a single `**kwargs` hash, rather
47
- # than declaring a `job_options:` keyword with a default, so that an
48
- # implicit `super` forwards the original call unchanged. Declaring
49
- # `job_options: {}` would bind that default even when the caller did not
50
- # pass it, and `super` would then forward it to Que. On Que 0.14, whose
51
- # `enqueue` takes only positional arguments, that extra keyword ends up
52
- # persisted as an additional job argument.
53
- def enqueue(*_args, **kwargs)
54
- job_options = kwargs[:job_options] || {}
55
-
56
- # Inside a `bulk_enqueue` block the batch is recorded once by the
57
- # `bulk_enqueue` wrapper, so each inner enqueue is a pass-through to
58
- # avoid recording an event per job.
210
+ # The keyword arguments are captured into a single `kwargs` hash, rather
211
+ # than declaring a `job_options:` keyword with a default, so that a
212
+ # keyword the caller did not pass is never forwarded to Que. See
213
+ # `forward_job_options` for why that matters.
214
+ def enqueue(*args, **kwargs)
215
+ # Inside a `bulk_enqueue` block the per-job enqueue must stay a
216
+ # pass-through: tags come from `bulk_enqueue`'s own `job_options` (Que
217
+ # raises if an inner enqueue passes them), and the batch's event and
218
+ # propagation are recorded once by the `bulk_enqueue` wrapper.
59
219
  return super if Thread.current[:appsignal_que_bulk_enqueue]
60
220
 
61
- # Under Active Job the enqueue is already recorded as an
62
- # `enqueue.active_job` event, so skip recording it again here.
63
- return super if Appsignal::Transaction.current? &&
64
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
221
+ job_options = kwargs[:job_options] || {}
65
222
 
66
223
  # Resolve the job class the way Que does: an explicit `:job_class`, else
67
224
  # the class `enqueue` was called on.
68
225
  title = "enqueue #{job_options[:job_class] || name} job"
69
- Appsignal.instrument("enqueue.que", title) { super }
226
+ record_enqueue(job_options, "enqueue.que", title) do |merged|
227
+ super(*args, **forward_job_options(kwargs, merged))
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ # Builds the keyword arguments to enqueue the job with. Passes
234
+ # `job_options` on to Que only when the caller passed it, or when the
235
+ # trace context was injected into it. Adding a `job_options` keyword to a
236
+ # call that did not have one breaks Que 0.x: it does not recognise the
237
+ # keyword, so it stores it as an extra job argument, and the job then
238
+ # fails because it is called with one argument too many.
239
+ def forward_job_options(kwargs, job_options)
240
+ return kwargs unless kwargs.key?(:job_options) || job_options.any?
241
+
242
+ kwargs.merge(:job_options => job_options)
243
+ end
244
+
245
+ # Records the enqueue as a producer event and, in collector mode, injects
246
+ # the current trace context into the job's tags so the job that later
247
+ # performs links back. Yields the (possibly tag-augmented) `job_options` to
248
+ # do the actual enqueue.
249
+ def record_enqueue(job_options, event_name, title, bulk: false)
250
+ # When enqueue instrumentation is disabled, drop the trace context along
251
+ # with the event, so yield the job options untouched. Without an enqueue
252
+ # event there is no producer span, so the context we would write is that
253
+ # of whatever span is current, such as the surrounding web request. The
254
+ # job that performs later would then link back to a span that is not a
255
+ # producer.
256
+ if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
257
+ return yield job_options
258
+ end
259
+
260
+ # Under Active Job the enqueue is already recorded as an
261
+ # `enqueue.active_job` event, so skip recording it again here. The trace
262
+ # context is still injected so the performed job links back.
263
+ if Appsignal::Transaction.current? &&
264
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
265
+ return yield job_options_with_context(job_options, :bulk => bulk)
266
+ end
267
+
268
+ Appsignal.instrument(
269
+ event_name,
270
+ title,
271
+ :opentelemetry_kind => :producer,
272
+ :opentelemetry_scope => ["appsignal-ruby/que", Appsignal::VERSION]
273
+ ) do
274
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
275
+ Appsignal::OpenTelemetry::Messaging
276
+ .enqueue_attributes("que", :destination => job_options[:queue])
277
+ )
278
+ yield job_options_with_context(job_options, :bulk => bulk)
279
+ end
280
+ end
281
+
282
+ # In collector mode, injects the current trace context into a copy of the
283
+ # job's tags and returns the tag-augmented `job_options`; a no-op that
284
+ # returns `job_options` unchanged outside collector mode.
285
+ def job_options_with_context(job_options, bulk: false)
286
+ tags = QueTraceContext.inject(job_options[:tags], :bulk => bulk)
287
+ tags.empty? ? job_options : job_options.merge(:tags => tags)
70
288
  end
71
289
  end
72
290
 
@@ -74,22 +292,23 @@ module Appsignal
74
292
  #
75
293
  # `bulk_enqueue` exists only on Que 2+, so this lives in its own module that
76
294
  # the hook prepends only when Que has the method -- otherwise we'd define a
77
- # `bulk_enqueue` on Que versions that have none. The whole batch records a
78
- # single `bulk_enqueue.que` event; the inner enqueues are pass-throughs.
295
+ # `bulk_enqueue` on Que versions that have none. The whole batch shares one
296
+ # `job_options`, so it records a single `bulk_enqueue.que` producer event and
297
+ # the inner enqueues are pass-throughs.
79
298
  module QueBulkClientPlugin
80
- def bulk_enqueue(*_args, job_options: {}, **_rest)
81
- # Under Active Job the enqueue is already recorded as an
82
- # `enqueue.active_job` event, so skip recording it again here.
83
- return super if Appsignal::Transaction.current? &&
84
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
85
-
86
- Appsignal.instrument("bulk_enqueue.que", bulk_enqueue_title(job_options)) do
299
+ def bulk_enqueue(job_options: {}, **rest, &block)
300
+ record_enqueue(
301
+ job_options,
302
+ "bulk_enqueue.que",
303
+ bulk_enqueue_title(job_options),
304
+ :bulk => true
305
+ ) do |merged|
87
306
  # Flag the batch so the enqueues this block triggers pass through
88
307
  # without recording, without reading Que's internal bulk state.
89
308
  was_bulk = Thread.current[:appsignal_que_bulk_enqueue]
90
309
  Thread.current[:appsignal_que_bulk_enqueue] = true
91
310
  begin
92
- super
311
+ super(:job_options => merged, **rest, &block)
93
312
  ensure
94
313
  Thread.current[:appsignal_que_bulk_enqueue] = was_bulk
95
314
  end
@@ -98,7 +98,10 @@ module Appsignal
98
98
  is_rails_runner = source == "application.runner.railties"
99
99
  namespace, action_name, tags, custom_data = context_for(context.dup)
100
100
 
101
- Appsignal.report_error(error) do |transaction|
101
+ Appsignal.report_error(
102
+ error,
103
+ :opentelemetry_scope => ["appsignal-ruby/rails", Appsignal::VERSION]
104
+ ) do |transaction|
102
105
  if namespace
103
106
  transaction.set_namespace(namespace)
104
107
  elsif is_rails_runner
@@ -22,7 +22,10 @@ module Appsignal
22
22
  end
23
23
 
24
24
  begin
25
- Appsignal.instrument "task.rake" do
25
+ Appsignal.instrument(
26
+ "task.rake",
27
+ :opentelemetry_scope => ["appsignal-ruby/rake", Appsignal::VERSION]
28
+ ) do
26
29
  super
27
30
  end
28
31
  rescue Exception => error
@@ -38,7 +41,7 @@ module Appsignal
38
41
  params, _ = args
39
42
  params = params.to_hash if params.respond_to?(:to_hash)
40
43
  transaction.set_action(name)
41
- transaction.add_params_if_nil(params)
44
+ transaction.add_request_payload_if_nil(params)
42
45
  transaction.complete
43
46
  end
44
47
  end
@@ -47,7 +50,10 @@ module Appsignal
47
50
  private
48
51
 
49
52
  def _appsignal_create_transaction
50
- Appsignal::Transaction.create("rake")
53
+ Appsignal::Transaction.create(
54
+ "rake",
55
+ :opentelemetry_scope => ["appsignal-ruby/rake", Appsignal::VERSION]
56
+ )
51
57
  end
52
58
  end
53
59
 
@@ -11,8 +11,29 @@ module Appsignal
11
11
  else
12
12
  "#{command[0]}#{" ?" * (command.size - 1)}"
13
13
  end
14
+ operation_name = command[0].to_s
14
15
 
15
- Appsignal.instrument "query.redis", id, sanitized_command do
16
+ Appsignal.instrument(
17
+ "query.redis",
18
+ id,
19
+ sanitized_command,
20
+ :opentelemetry_kind => :client,
21
+ :opentelemetry_scope => ["appsignal-ruby/redis", Appsignal::VERSION]
22
+ ) do
23
+ # Names the datastore this span talks to, which is what the trace
24
+ # timeline reads to recognize a cache call, along with the command and
25
+ # the database it ran against.
26
+ #
27
+ # The command stays in the event body rather than moving to
28
+ # `db.query.text`: it is sanitized here already, and the collector
29
+ # sanitizes `db.query.text` again for Redis, which would mangle it.
30
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
31
+ {
32
+ "db.system.name" => "redis",
33
+ "db.operation.name" => (operation_name unless operation_name.empty?),
34
+ "db.namespace" => (db.to_s if respond_to?(:db) && db)
35
+ }.compact
36
+ )
16
37
  super
17
38
  end
18
39
  end
@@ -11,8 +11,29 @@ module Appsignal
11
11
  else
12
12
  "#{command[0]}#{" ?" * (command.size - 1)}"
13
13
  end
14
+ operation_name = command[0].to_s
14
15
 
15
- Appsignal.instrument "query.redis", @config.id, sanitized_command do
16
+ Appsignal.instrument(
17
+ "query.redis",
18
+ @config.id,
19
+ sanitized_command,
20
+ :opentelemetry_kind => :client,
21
+ :opentelemetry_scope => ["appsignal-ruby/redis_client", Appsignal::VERSION]
22
+ ) do
23
+ # Names the datastore this span talks to, which is what the trace
24
+ # timeline reads to recognize a cache call, along with the command and
25
+ # the database it ran against.
26
+ #
27
+ # The command stays in the event body rather than moving to
28
+ # `db.query.text`: it is sanitized here already, and the collector
29
+ # sanitizes `db.query.text` again for Redis, which would mangle it.
30
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
31
+ {
32
+ "db.system.name" => "redis",
33
+ "db.operation.name" => (operation_name unless operation_name.empty?),
34
+ "db.namespace" => (@config.db.to_s if @config.respond_to?(:db) && @config.db)
35
+ }.compact
36
+ )
16
37
  super
17
38
  end
18
39
  end
@@ -5,9 +5,33 @@ module Appsignal
5
5
  # @!visibility private
6
6
  module ResqueIntegration
7
7
  def perform
8
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
8
+ # Read trace context off the job so the transaction links back to the
9
+ # enqueuer. No-op outside collector mode.
10
+ job_data = ResqueHelpers.active_job_data(payload)
11
+ transaction = Appsignal::Transaction.create(
12
+ Appsignal::Transaction::BACKGROUND_JOB,
13
+ :opentelemetry_context => ResqueHelpers.extract_context(payload, job_data),
14
+ :opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION],
15
+ :opentelemetry_kind => :consumer,
16
+ :opentelemetry_relationship =>
17
+ Appsignal::OpenTelemetry.active_job_relationship(job_data)
18
+ )
19
+ # Describes this span as a job being performed. The messaging system is
20
+ # what the trace timeline reads to recognize background job work, and
21
+ # `resque` is the value OpenTelemetry's own Resque instrumentation uses.
22
+ transaction.add_opentelemetry_attributes(
23
+ Appsignal::OpenTelemetry::Messaging
24
+ .perform_attributes("resque", :destination => queue)
25
+ )
9
26
 
10
- Appsignal.instrument "perform.resque" do
27
+ Appsignal.instrument(
28
+ "perform.resque",
29
+ :opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION]
30
+ ) do
31
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
32
+ Appsignal::OpenTelemetry::Messaging
33
+ .perform_attributes("resque", :destination => queue)
34
+ )
11
35
  super
12
36
  end
13
37
  rescue Exception => exception
@@ -16,7 +40,7 @@ module Appsignal
16
40
  ensure
17
41
  if transaction
18
42
  transaction.set_action_if_nil("#{payload["class"]}#perform")
19
- transaction.add_params_if_nil { ResqueHelpers.arguments(payload) }
43
+ transaction.add_function_parameters_if_nil { ResqueHelpers.arguments(payload) }
20
44
  transaction.add_tags("queue" => queue)
21
45
 
22
46
  Appsignal::Transaction.complete_current!
@@ -25,8 +49,10 @@ module Appsignal
25
49
  end
26
50
  end
27
51
 
28
- # Wraps `Resque.push` to record an `enqueue.resque` event so the enqueue
29
- # shows up under the active transaction.
52
+ # Wraps `Resque.push` to record an `enqueue.resque` event so the
53
+ # enqueue shows up under the active transaction (both modes), and in
54
+ # collector mode writes the trace context onto the job hash so the job that
55
+ # later performs links back to it.
30
56
  #
31
57
  # Like all AppSignal events, this only records when there's an active
32
58
  # transaction (e.g. enqueuing from within a web request or another job).
@@ -34,26 +60,70 @@ module Appsignal
34
60
  #
35
61
  # @!visibility private
36
62
  module ResquePushIntegration
37
- def push(_queue, item)
63
+ def push(queue, item)
64
+ # When enqueue instrumentation is disabled, drop the trace context along
65
+ # with the event. Without an enqueue event there is no producer span, so
66
+ # the context we would write is that of whatever span is current, such as
67
+ # the surrounding web request. The job that performs later would then
68
+ # link back to a span that is not a producer.
69
+ return super if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
70
+
38
71
  # Under Active Job the enqueue is already recorded as an
39
- # `enqueue.active_job` event, so skip recording it again here.
40
- return super if Appsignal::Transaction.current? &&
41
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
72
+ # `enqueue.active_job` event, so skip recording it again here. The trace
73
+ # context is still injected so the performed job links back.
74
+ if Appsignal::Transaction.current? &&
75
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
76
+ Appsignal::OpenTelemetry.inject_context(item)
77
+ return super
78
+ end
42
79
 
43
- Appsignal.instrument("enqueue.resque", "enqueue #{item["class"]} job") { super }
80
+ Appsignal.instrument(
81
+ "enqueue.resque",
82
+ "enqueue #{item["class"]} job",
83
+ :opentelemetry_kind => :producer,
84
+ :opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION]
85
+ ) do
86
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
87
+ Appsignal::OpenTelemetry::Messaging
88
+ .enqueue_attributes("resque", :destination => queue)
89
+ )
90
+ Appsignal::OpenTelemetry.inject_context(item)
91
+ super
92
+ end
44
93
  end
45
94
  end
46
95
 
47
96
  # @!visibility private
48
97
  class ResqueHelpers
98
+ # The class the Active Job adapter enqueues, with the serialized job data
99
+ # as its only argument.
100
+ ACTIVE_JOB_WRAPPER = "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper"
101
+
49
102
  def self.arguments(payload)
50
103
  case payload["class"]
51
- when "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper"
104
+ when ACTIVE_JOB_WRAPPER
52
105
  nil # Set in the ActiveJob integration
53
106
  else
54
107
  payload["args"]
55
108
  end
56
109
  end
110
+
111
+ # The serialized Active Job job data inside a Resque job, or nil when this
112
+ # is not an Active Job job.
113
+ def self.active_job_data(payload)
114
+ return unless payload["class"] == ACTIVE_JOB_WRAPPER
115
+
116
+ job_data = payload["args"]&.first
117
+ job_data if job_data.is_a?(Hash)
118
+ end
119
+
120
+ # The trace context to continue: the Active Job layer when this is an
121
+ # Active Job job, the Resque job itself otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
122
+ # for why that layer wins.
123
+ def self.extract_context(payload, job_data)
124
+ Appsignal::OpenTelemetry.extract_active_job_context(job_data) ||
125
+ Appsignal::OpenTelemetry.extract_job_context(payload)
126
+ end
57
127
  end
58
128
  end
59
129
  end