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
@@ -2,21 +2,152 @@
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
+ def within_limits?(tags)
97
+ tags.length <= MAX_TAGS_COUNT && tags.all? { |tag| tag.length <= MAX_TAG_LENGTH }
98
+ end
99
+ end
100
+
5
101
  # @!visibility private
6
102
  module QuePlugin
7
103
  def _run(*args)
104
+ local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
105
+ tags = local_attrs.dig(:data, :tags)
106
+
107
+ # A job enqueued on its own is the only job its producer span produced, so
108
+ # it can be a child of that span as well as link to it. Every job in a
109
+ # batch shares one producer span, and a span can only have one parent, so
110
+ # parenting a batch would hang the whole batch off that single span. Only
111
+ # link those, which is what the OpenTelemetry messaging conventions ask
112
+ # for: they use links as the default, and allow the producer to be the
113
+ # parent only when it produced a single message.
114
+ relationship = QueTraceContext.bulk?(tags) ? :link : :both
115
+
116
+ # Read the incoming trace context off the job's tags so the transaction
117
+ # links back to the enqueuer. No-op outside collector mode.
8
118
  transaction =
9
- Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
119
+ Appsignal::Transaction.create(
120
+ Appsignal::Transaction::BACKGROUND_JOB,
121
+ :opentelemetry_context => QueTraceContext.extract(tags),
122
+ :opentelemetry_scope => ["appsignal-ruby/que", Appsignal::VERSION],
123
+ :opentelemetry_kind => :consumer,
124
+ :opentelemetry_relationship => relationship
125
+ )
126
+ # Describes this span as a job being performed. The messaging system is
127
+ # what the trace timeline reads to recognize background job work, and
128
+ # `que` is the value OpenTelemetry's own Que instrumentation uses.
129
+ transaction.add_opentelemetry_attributes(
130
+ Appsignal::OpenTelemetry::Messaging
131
+ .perform_attributes("que", :destination => local_attrs[:queue])
132
+ )
10
133
 
11
134
  begin
12
- Appsignal.instrument("perform_job.que") { super }
135
+ Appsignal.instrument(
136
+ "perform_job.que",
137
+ :opentelemetry_scope => ["appsignal-ruby/que", Appsignal::VERSION]
138
+ ) do
139
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
140
+ Appsignal::OpenTelemetry::Messaging
141
+ .perform_attributes("que", :destination => local_attrs[:queue])
142
+ )
143
+ super
144
+ end
13
145
  rescue Exception => error
14
146
  transaction.set_error(error)
15
147
  raise error
16
148
  ensure
17
- local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
18
149
  transaction.set_action_if_nil("#{local_attrs[:job_class]}#run")
19
- transaction.add_params_if_nil do
150
+ transaction.add_function_parameters_if_nil do
20
151
  {
21
152
  :arguments => local_attrs[:args]
22
153
  }.tap do |hash|
@@ -37,36 +168,91 @@ module Appsignal
37
168
 
38
169
  # @!visibility private
39
170
  #
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.
171
+ # Prepended to `Que::Job`'s singleton so it wraps enqueues. Records the
172
+ # enqueue as an AppSignal event (a producer span in collector mode), and in
173
+ # collector mode writes the current trace context onto the job's tags so the
174
+ # job that later performs links back to it. Like all AppSignal events, the
175
+ # enqueue only records when there's an active transaction; otherwise it's a
176
+ # transparent pass-through.
45
177
  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.
178
+ # The keyword arguments are captured into a single `kwargs` hash, rather
179
+ # than declaring a `job_options:` keyword with a default, so that a
180
+ # keyword the caller did not pass is never forwarded to Que. See
181
+ # `forward_job_options` for why that matters.
182
+ def enqueue(*args, **kwargs)
183
+ # Inside a `bulk_enqueue` block the per-job enqueue must stay a
184
+ # pass-through: tags come from `bulk_enqueue`'s own `job_options` (Que
185
+ # raises if an inner enqueue passes them), and the batch's event and
186
+ # propagation are recorded once by the `bulk_enqueue` wrapper.
59
187
  return super if Thread.current[:appsignal_que_bulk_enqueue]
60
188
 
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?
189
+ job_options = kwargs[:job_options] || {}
65
190
 
66
191
  # Resolve the job class the way Que does: an explicit `:job_class`, else
67
192
  # the class `enqueue` was called on.
68
193
  title = "enqueue #{job_options[:job_class] || name} job"
69
- Appsignal.instrument("enqueue.que", title) { super }
194
+ record_enqueue(job_options, "enqueue.que", title) do |merged|
195
+ super(*args, **forward_job_options(kwargs, merged))
196
+ end
197
+ end
198
+
199
+ private
200
+
201
+ # Builds the keyword arguments to enqueue the job with. Passes
202
+ # `job_options` on to Que only when the caller passed it, or when the
203
+ # trace context was injected into it. Adding a `job_options` keyword to a
204
+ # call that did not have one breaks Que 0.x: it does not recognise the
205
+ # keyword, so it stores it as an extra job argument, and the job then
206
+ # fails because it is called with one argument too many.
207
+ def forward_job_options(kwargs, job_options)
208
+ return kwargs unless kwargs.key?(:job_options) || job_options.any?
209
+
210
+ kwargs.merge(:job_options => job_options)
211
+ end
212
+
213
+ # Records the enqueue as a producer event and, in collector mode, injects
214
+ # the current trace context into the job's tags so the job that later
215
+ # performs links back. Yields the (possibly tag-augmented) `job_options` to
216
+ # do the actual enqueue.
217
+ def record_enqueue(job_options, event_name, title, bulk: false)
218
+ # When enqueue instrumentation is disabled, drop the trace context along
219
+ # with the event, so yield the job options untouched. Without an enqueue
220
+ # event there is no producer span, so the context we would write is that
221
+ # of whatever span is current, such as the surrounding web request. The
222
+ # job that performs later would then link back to a span that is not a
223
+ # producer.
224
+ if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
225
+ return yield job_options
226
+ end
227
+
228
+ # Under Active Job the enqueue is already recorded as an
229
+ # `enqueue.active_job` event, so skip recording it again here. The trace
230
+ # context is still injected so the performed job links back.
231
+ if Appsignal::Transaction.current? &&
232
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
233
+ return yield job_options_with_context(job_options, :bulk => bulk)
234
+ end
235
+
236
+ Appsignal.instrument(
237
+ event_name,
238
+ title,
239
+ :opentelemetry_kind => :producer,
240
+ :opentelemetry_scope => ["appsignal-ruby/que", Appsignal::VERSION]
241
+ ) do
242
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
243
+ Appsignal::OpenTelemetry::Messaging
244
+ .enqueue_attributes("que", :destination => job_options[:queue])
245
+ )
246
+ yield job_options_with_context(job_options, :bulk => bulk)
247
+ end
248
+ end
249
+
250
+ # In collector mode, injects the current trace context into a copy of the
251
+ # job's tags and returns the tag-augmented `job_options`; a no-op that
252
+ # returns `job_options` unchanged outside collector mode.
253
+ def job_options_with_context(job_options, bulk: false)
254
+ tags = QueTraceContext.inject(job_options[:tags], :bulk => bulk)
255
+ tags.empty? ? job_options : job_options.merge(:tags => tags)
70
256
  end
71
257
  end
72
258
 
@@ -74,22 +260,23 @@ module Appsignal
74
260
  #
75
261
  # `bulk_enqueue` exists only on Que 2+, so this lives in its own module that
76
262
  # 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.
263
+ # `bulk_enqueue` on Que versions that have none. The whole batch shares one
264
+ # `job_options`, so it records a single `bulk_enqueue.que` producer event and
265
+ # the inner enqueues are pass-throughs.
79
266
  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
267
+ def bulk_enqueue(job_options: {}, **rest, &block)
268
+ record_enqueue(
269
+ job_options,
270
+ "bulk_enqueue.que",
271
+ bulk_enqueue_title(job_options),
272
+ :bulk => true
273
+ ) do |merged|
87
274
  # Flag the batch so the enqueues this block triggers pass through
88
275
  # without recording, without reading Que's internal bulk state.
89
276
  was_bulk = Thread.current[:appsignal_que_bulk_enqueue]
90
277
  Thread.current[:appsignal_que_bulk_enqueue] = true
91
278
  begin
92
- super
279
+ super(:job_options => merged, **rest, &block)
93
280
  ensure
94
281
  Thread.current[:appsignal_que_bulk_enqueue] = was_bulk
95
282
  end
@@ -96,7 +96,10 @@ module Appsignal
96
96
  is_rails_runner = source == "application.runner.railties"
97
97
  namespace, action_name, tags, custom_data = context_for(context.dup)
98
98
 
99
- Appsignal.report_error(error) do |transaction|
99
+ Appsignal.report_error(
100
+ error,
101
+ :opentelemetry_scope => ["appsignal-ruby/rails", Appsignal::VERSION]
102
+ ) do |transaction|
100
103
  if namespace
101
104
  transaction.set_namespace(namespace)
102
105
  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,31 @@ 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
+ transaction = Appsignal::Transaction.create(
11
+ Appsignal::Transaction::BACKGROUND_JOB,
12
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_job_context(payload),
13
+ :opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION],
14
+ :opentelemetry_kind => :consumer,
15
+ :opentelemetry_relationship => :both
16
+ )
17
+ # Describes this span as a job being performed. The messaging system is
18
+ # what the trace timeline reads to recognize background job work, and
19
+ # `resque` is the value OpenTelemetry's own Resque instrumentation uses.
20
+ transaction.add_opentelemetry_attributes(
21
+ Appsignal::OpenTelemetry::Messaging
22
+ .perform_attributes("resque", :destination => queue)
23
+ )
9
24
 
10
- Appsignal.instrument "perform.resque" do
25
+ Appsignal.instrument(
26
+ "perform.resque",
27
+ :opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION]
28
+ ) do
29
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
30
+ Appsignal::OpenTelemetry::Messaging
31
+ .perform_attributes("resque", :destination => queue)
32
+ )
11
33
  super
12
34
  end
13
35
  rescue Exception => exception
@@ -16,7 +38,7 @@ module Appsignal
16
38
  ensure
17
39
  if transaction
18
40
  transaction.set_action_if_nil("#{payload["class"]}#perform")
19
- transaction.add_params_if_nil { ResqueHelpers.arguments(payload) }
41
+ transaction.add_function_parameters_if_nil { ResqueHelpers.arguments(payload) }
20
42
  transaction.add_tags("queue" => queue)
21
43
 
22
44
  Appsignal::Transaction.complete_current!
@@ -25,8 +47,10 @@ module Appsignal
25
47
  end
26
48
  end
27
49
 
28
- # Wraps `Resque.push` to record an `enqueue.resque` event so the enqueue
29
- # shows up under the active transaction.
50
+ # Wraps `Resque.push` to record an `enqueue.resque` event so the
51
+ # enqueue shows up under the active transaction (both modes), and in
52
+ # collector mode writes the trace context onto the job hash so the job that
53
+ # later performs links back to it.
30
54
  #
31
55
  # Like all AppSignal events, this only records when there's an active
32
56
  # transaction (e.g. enqueuing from within a web request or another job).
@@ -34,13 +58,36 @@ module Appsignal
34
58
  #
35
59
  # @!visibility private
36
60
  module ResquePushIntegration
37
- def push(_queue, item)
61
+ def push(queue, item)
62
+ # When enqueue instrumentation is disabled, drop the trace context along
63
+ # with the event. Without an enqueue event there is no producer span, so
64
+ # the context we would write is that of whatever span is current, such as
65
+ # the surrounding web request. The job that performs later would then
66
+ # link back to a span that is not a producer.
67
+ return super if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
68
+
38
69
  # 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?
70
+ # `enqueue.active_job` event, so skip recording it again here. The trace
71
+ # context is still injected so the performed job links back.
72
+ if Appsignal::Transaction.current? &&
73
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
74
+ Appsignal::OpenTelemetry.inject_context(item)
75
+ return super
76
+ end
42
77
 
43
- Appsignal.instrument("enqueue.resque", "enqueue #{item["class"]} job") { super }
78
+ Appsignal.instrument(
79
+ "enqueue.resque",
80
+ "enqueue #{item["class"]} job",
81
+ :opentelemetry_kind => :producer,
82
+ :opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION]
83
+ ) do
84
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
85
+ Appsignal::OpenTelemetry::Messaging
86
+ .enqueue_attributes("resque", :destination => queue)
87
+ )
88
+ Appsignal::OpenTelemetry.inject_context(item)
89
+ super
90
+ end
44
91
  end
45
92
  end
46
93