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,20 +2,122 @@
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 aws-sdk
8
+ # instrumentation does: as SQS message attributes, using the global
9
+ # propagator. Staying wire-equivalent means that if both AppSignal and
10
+ # OpenTelemetry's aws-sdk instrumentation are active, one simply shadows the
11
+ # other rather than corrupting the carrier. Collector mode only.
12
+ module ShoryukenTraceContext
13
+ module_function
14
+
15
+ # SQS allows at most 10 message attributes per message. Mirror
16
+ # OpenTelemetry and skip propagation rather than risk the enqueue failing
17
+ # when the user already fills the slots.
18
+ MAX_MESSAGE_ATTRIBUTES = 10
19
+
20
+ # Writes each trace header as an SQS message attribute, matching the shape
21
+ # OpenTelemetry's aws-sdk instrumentation injects on send.
22
+ module MessageAttributeSetter
23
+ def self.set(carrier, key, value)
24
+ return if carrier.length >= MAX_MESSAGE_ATTRIBUTES
25
+
26
+ carrier[key] = { :string_value => value, :data_type => "String" }
27
+ end
28
+ end
29
+
30
+ # Reads a trace header back out of a message attribute. Works both for the
31
+ # plain hash we inject and for the `Aws::SQS::Types::MessageAttributeValue`
32
+ # struct delivered on receive, since both respond to `[:string_value]` /
33
+ # `[:data_type]`.
34
+ module MessageAttributeGetter
35
+ def self.get(carrier, key)
36
+ attribute = carrier[key]
37
+ attribute[:string_value] if attribute && attribute[:data_type] == "String"
38
+ end
39
+ end
40
+
41
+ # Read the incoming context off a message's SQS message attributes so the
42
+ # transaction links back to the enqueuer. Returns an
43
+ # `OpenTelemetry::Context`, or `nil` outside collector mode.
44
+ def extract(message_attributes)
45
+ Appsignal::OpenTelemetry.if_started do
46
+ ::OpenTelemetry.propagation.extract(
47
+ message_attributes || {},
48
+ :getter => MessageAttributeGetter
49
+ )
50
+ end
51
+ end
52
+
53
+ # Write the current trace context into the outgoing send `options`.
54
+ # Injects into a scratch carrier first and merges it into the message
55
+ # attributes only when something was written, so an enqueue with no active
56
+ # span (no transaction, or outside collector mode) leaves the options
57
+ # untouched -- a transparent pass-through.
58
+ def inject(options)
59
+ Appsignal::OpenTelemetry.if_started do
60
+ carrier = {}
61
+ ::OpenTelemetry.propagation.inject(carrier, :setter => MessageAttributeSetter)
62
+ next if carrier.empty?
63
+
64
+ options[:message_attributes] = (options[:message_attributes] || {}).merge(carrier)
65
+ end
66
+ end
67
+ end
68
+
5
69
  # @!visibility private
6
70
  class ShoryukenMiddleware
7
71
  def call(worker_instance, queue, sqs_msg, body, &block)
8
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
72
+ batch = sqs_msg.is_a?(Array)
73
+ # How many messages this call covers, which is only reported for a batch.
74
+ batch_size = sqs_msg.size if batch
9
75
 
10
- Appsignal.instrument("perform_job.shoryuken", &block)
76
+ # Read the incoming trace context off the message so the transaction
77
+ # links back to the enqueuer. A batch carries messages from multiple
78
+ # traces with no single parent, so only single messages link back.
79
+ # No-op outside collector mode.
80
+ context = extract_context(sqs_msg, body) unless batch
81
+
82
+ transaction = Appsignal::Transaction.create(
83
+ Appsignal::Transaction::BACKGROUND_JOB,
84
+ :opentelemetry_context => context,
85
+ :opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION],
86
+ :opentelemetry_kind => :consumer,
87
+ # A message batch is a batch on the receiving side, and carries no
88
+ # context to relate to at all, so the body it reads here is the single
89
+ # message's job data or nothing.
90
+ :opentelemetry_relationship =>
91
+ Appsignal::OpenTelemetry.active_job_relationship(batch ? nil : body)
92
+ )
93
+ # Describes this span as a job being performed. The messaging system is
94
+ # what the trace timeline reads to recognize background job work.
95
+ # Shoryuken runs on Amazon SQS, so it takes the `aws_sqs` value the
96
+ # OpenTelemetry semantic conventions define for it.
97
+ transaction.add_opentelemetry_attributes(
98
+ Appsignal::OpenTelemetry::Messaging.perform_attributes(
99
+ "aws_sqs", :destination => queue, :batch_size => batch_size
100
+ )
101
+ )
102
+
103
+ Appsignal.instrument(
104
+ "perform_job.shoryuken",
105
+ :opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION]
106
+ ) do
107
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
108
+ Appsignal::OpenTelemetry::Messaging.perform_attributes(
109
+ "aws_sqs", :destination => queue, :batch_size => batch_size
110
+ )
111
+ )
112
+ block.call
113
+ end
11
114
  rescue Exception => error
12
115
  transaction.set_error(error)
13
116
  raise
14
117
  ensure
15
- batch = sqs_msg.is_a?(Array)
16
118
  attributes = fetch_attributes(batch, sqs_msg)
17
119
  transaction.set_action_if_nil("#{worker_instance.class.name}#perform")
18
- transaction.add_params_if_nil { fetch_args(batch, sqs_msg, body) }
120
+ transaction.add_function_parameters_if_nil { fetch_args(batch, sqs_msg, body) }
19
121
  transaction.add_tags(attributes)
20
122
  transaction.add_tags("queue" => queue)
21
123
  transaction.add_tags("batch" => true) if batch
@@ -29,6 +131,21 @@ module Appsignal
29
131
 
30
132
  private
31
133
 
134
+ # The trace context to continue: the Active Job layer when this is an
135
+ # Active Job job, the message's own attributes otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
136
+ # for why that layer wins.
137
+ # SQS allows ten message attributes per message, shared with whatever the
138
+ # user puts there.
139
+ #
140
+ # The Active Job adapter registers a worker that parses the message body as
141
+ # JSON, so an Active Job job's body arrives here as its serialized job
142
+ # data. Nothing checks that it is one: a body with no readable trace
143
+ # context in it reads as "nothing here" on its own.
144
+ def extract_context(sqs_msg, body)
145
+ Appsignal::OpenTelemetry.extract_active_job_context(body) ||
146
+ ShoryukenTraceContext.extract(sqs_msg.message_attributes)
147
+ end
148
+
32
149
  def fetch_attributes(batch, sqs_msg)
33
150
  if batch
34
151
  # We can't instrument batched message separately, the `yield` will
@@ -73,7 +190,9 @@ module Appsignal
73
190
  end
74
191
 
75
192
  # Shoryuken client middleware that records an `enqueue.shoryuken` event so
76
- # the enqueue shows up under the active transaction.
193
+ # the enqueue shows up under the active transaction (both modes), and in
194
+ # collector mode writes the current trace context onto the outgoing message
195
+ # so the job that later performs links back to it.
77
196
  #
78
197
  # Like all AppSignal events, this only records when there's an active
79
198
  # transaction (e.g. enqueuing from within a web request or another job). An
@@ -81,13 +200,36 @@ module Appsignal
81
200
  #
82
201
  # @!visibility private
83
202
  class ShoryukenClientMiddleware
84
- def call(options, &block)
203
+ def call(options)
204
+ # When enqueue instrumentation is disabled, drop the trace context along
205
+ # with the event. Without an enqueue event there is no producer span, so
206
+ # the context we would write is that of whatever span is current, such as
207
+ # the surrounding web request. The job that performs later would then
208
+ # link back to a span that is not a producer.
209
+ return yield if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
210
+
85
211
  # Under Active Job the enqueue is already recorded as an
86
- # `enqueue.active_job` event, so skip recording it again here.
87
- return yield if Appsignal::Transaction.current? &&
88
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
212
+ # `enqueue.active_job` event, so skip recording it again here. The trace
213
+ # context is still injected so the performed job links back.
214
+ if Appsignal::Transaction.current? &&
215
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
216
+ ShoryukenTraceContext.inject(options)
217
+ return yield
218
+ end
89
219
 
90
- Appsignal.instrument("enqueue.shoryuken", enqueue_title(options), &block)
220
+ Appsignal.instrument(
221
+ "enqueue.shoryuken",
222
+ enqueue_title(options),
223
+ :opentelemetry_kind => :producer,
224
+ :opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION]
225
+ ) do
226
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
227
+ Appsignal::OpenTelemetry::Messaging
228
+ .enqueue_attributes("aws_sqs", :destination => queue_name(options))
229
+ )
230
+ ShoryukenTraceContext.inject(options)
231
+ yield
232
+ end
91
233
  end
92
234
 
93
235
  private
@@ -99,8 +241,13 @@ module Appsignal
99
241
  worker_class = options.dig(:message_attributes, "shoryuken_class", :string_value)
100
242
  return "enqueue #{worker_class} job" if worker_class
101
243
 
102
- queue = options[:queue_url].to_s.split("/").last
103
- "enqueue on #{queue}"
244
+ "enqueue on #{queue_name(options)}"
245
+ end
246
+
247
+ # The queue a message is being sent to, which SQS identifies by a URL whose
248
+ # last segment is the queue's name.
249
+ def queue_name(options)
250
+ options[:queue_url].to_s.split("/").last
104
251
  end
105
252
  end
106
253
  end
@@ -38,10 +38,18 @@ module Appsignal
38
38
  # Sidekiq error outside of the middleware scope.
39
39
  # Can be a job JSON parse error or some other error happening in
40
40
  # Sidekiq.
41
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
41
+ transaction = Appsignal::Transaction.create(
42
+ Appsignal::Transaction::BACKGROUND_JOB,
43
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION],
44
+ :opentelemetry_kind => :consumer,
45
+ :opentelemetry_relationship => :both
46
+ )
47
+ transaction.add_opentelemetry_attributes(
48
+ Appsignal::OpenTelemetry::Messaging.perform_attributes("sidekiq")
49
+ )
42
50
  transaction.set_action_if_nil("SidekiqInternal")
43
51
  transaction.set_metadata("sidekiq_error", sidekiq_context[:context])
44
- transaction.add_params_if_nil(:jobstr => sidekiq_context[:jobstr])
52
+ transaction.add_function_parameters_if_nil(:jobstr => sidekiq_context[:jobstr])
45
53
  transaction.set_error(exception)
46
54
  end
47
55
 
@@ -98,24 +106,52 @@ module Appsignal
98
106
  end
99
107
  end
100
108
 
101
- # Sidekiq client middleware that runs on enqueue. Records an
102
- # `enqueue.sidekiq` event so the enqueue shows up under the active
103
- # transaction.
109
+ # Client middleware that runs on enqueue. Records an `enqueue.sidekiq`
110
+ # event so the enqueue shows up under the active transaction (both modes),
111
+ # and in collector mode writes the trace context onto the job hash so the
112
+ # job that later performs links back to it.
104
113
  #
105
114
  # Like all AppSignal events, this only records when there's an active
106
- # transaction (e.g. enqueuing from within a web request or another job). An
107
- # enqueue with no transaction is a transparent pass-through.
115
+ # transaction (e.g. enqueuing from within a web request or another job).
116
+ # An enqueue with no transaction is a transparent pass-through.
108
117
  #
109
118
  # @!visibility private
110
119
  class SidekiqClientMiddleware
111
- def call(_worker_class, job, _queue, _redis_pool, &block)
120
+ def call(_worker_class, job, _queue, _redis_pool)
121
+ # When enqueue instrumentation is disabled, drop the trace context along
122
+ # with the event. Without an enqueue event there is no producer span, so
123
+ # the context we would write is that of whatever span is current, such as
124
+ # the surrounding web request. The job that performs later would then
125
+ # link back to a span that is not a producer.
126
+ return yield if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
127
+
112
128
  # Under Active Job the enqueue is already recorded as an
113
- # `enqueue.active_job` event, so skip recording it again here.
114
- return yield if Appsignal::Transaction.current? &&
115
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
129
+ # `enqueue.active_job` event, so skip recording it again here. The trace
130
+ # context is still injected so the performed job links back.
131
+ if Appsignal::Transaction.current? &&
132
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
133
+ Appsignal::OpenTelemetry.inject_context(job)
134
+ return yield
135
+ end
116
136
 
117
137
  title = "enqueue #{SidekiqActionName.parse_action_name(job)} job"
118
- Appsignal.instrument("enqueue.sidekiq", title, &block)
138
+ Appsignal.instrument(
139
+ "enqueue.sidekiq",
140
+ title,
141
+ :opentelemetry_kind => :producer,
142
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION]
143
+ ) do
144
+ # Describes this span as a job being enqueued. The messaging system
145
+ # is what the trace timeline reads to recognize background job work,
146
+ # and `sidekiq` is the value OpenTelemetry's own Sidekiq
147
+ # instrumentation uses.
148
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
149
+ Appsignal::OpenTelemetry::Messaging
150
+ .enqueue_attributes("sidekiq", :destination => job["queue"])
151
+ )
152
+ Appsignal::OpenTelemetry.inject_context(job)
153
+ yield
154
+ end
119
155
  end
120
156
  end
121
157
 
@@ -126,7 +162,7 @@ module Appsignal
126
162
  EXCLUDED_JOB_KEYS = %w[
127
163
  args backtrace class created_at enqueued_at error_backtrace error_class
128
164
  error_message failed_at jid retried_at retry wrapped cattr tags retry_for
129
- unique_for
165
+ unique_for traceparent tracestate __otel_headers
130
166
  ].freeze
131
167
 
132
168
  def self.sidekiq8?
@@ -138,7 +174,21 @@ module Appsignal
138
174
  def call(_worker, item, _queue, &block)
139
175
  job_status = nil
140
176
  action_name = formatted_action_name(item)
141
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
177
+ # Read trace context off the job so the transaction links back to the
178
+ # enqueuer. No-op outside collector mode.
179
+ job_data = active_job_data(item)
180
+ transaction = Appsignal::Transaction.create(
181
+ Appsignal::Transaction::BACKGROUND_JOB,
182
+ :opentelemetry_context => extract_context(item, job_data),
183
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION],
184
+ :opentelemetry_kind => :consumer,
185
+ :opentelemetry_relationship =>
186
+ Appsignal::OpenTelemetry.active_job_relationship(job_data)
187
+ )
188
+ transaction.add_opentelemetry_attributes(
189
+ Appsignal::OpenTelemetry::Messaging
190
+ .perform_attributes("sidekiq", :destination => item["queue"])
191
+ )
142
192
  transaction.set_action_if_nil(action_name)
143
193
 
144
194
  formatted_metadata(item).each do |key, value|
@@ -146,13 +196,22 @@ module Appsignal
146
196
  end
147
197
 
148
198
  begin
149
- Appsignal.instrument "perform_job.sidekiq", &block
199
+ Appsignal.instrument(
200
+ "perform_job.sidekiq",
201
+ :opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION]
202
+ ) do
203
+ Appsignal::Transaction.current.add_opentelemetry_attributes(
204
+ Appsignal::OpenTelemetry::Messaging
205
+ .perform_attributes("sidekiq", :destination => item["queue"])
206
+ )
207
+ block.call
208
+ end
150
209
  rescue Exception => exception
151
210
  job_status = :failed
152
211
  raise exception
153
212
  ensure
154
213
  if transaction
155
- transaction.add_params_if_nil { parse_arguments(item) }
214
+ transaction.add_function_parameters_if_nil { parse_arguments(item) }
156
215
  enqueued_at = item["enqueued_at"]
157
216
  queue_start =
158
217
  if self.class.sidekiq8?
@@ -189,6 +248,25 @@ module Appsignal
189
248
 
190
249
  private
191
250
 
251
+ # The trace context to continue: the Active Job layer when this is an
252
+ # Active Job job, the Sidekiq job itself otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
253
+ # for why that layer wins.
254
+ def extract_context(item, job_data)
255
+ Appsignal::OpenTelemetry.extract_active_job_context(job_data) ||
256
+ Appsignal::OpenTelemetry.extract_job_context(item)
257
+ end
258
+
259
+ # The serialized Active Job job data inside a Sidekiq job, or nil when this
260
+ # is not an Active Job job. Both Active Job adapters for Sidekiq, the one in
261
+ # Rails and the one in the Sidekiq gem, enqueue a wrapper class with the
262
+ # job data as its only argument and name the real job class in `wrapped`.
263
+ def active_job_data(item)
264
+ return unless item["wrapped"]
265
+
266
+ job_data = item["args"]&.first
267
+ job_data if job_data.is_a?(Hash)
268
+ end
269
+
192
270
  def increment_counter(key, value, tags = {})
193
271
  Appsignal.increment_counter "sidekiq_#{key}", value, tags
194
272
  end
@@ -10,20 +10,71 @@ module Appsignal
10
10
  if has_parent_transaction
11
11
  Appsignal::Transaction.current
12
12
  else
13
- Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
13
+ # Read the incoming trace context off the request headers so the
14
+ # transaction continues the upstream trace. No-op outside collector
15
+ # mode. Webmachine isn't Rack: `request.headers` is a case-insensitive
16
+ # `Webmachine::Headers`, so the default getter reads it directly.
17
+ Appsignal::Transaction.create(
18
+ Appsignal::Transaction::HTTP_REQUEST,
19
+ :opentelemetry_context => Appsignal::OpenTelemetry.if_started do
20
+ ::OpenTelemetry.propagation.extract(request.headers)
21
+ end,
22
+ :opentelemetry_scope => ["appsignal-ruby/webmachine", Appsignal::VERSION]
23
+ )
14
24
  end
15
25
 
26
+ unless has_parent_transaction
27
+ # Describes the transaction's span as an incoming HTTP request.
28
+ # Together with the SERVER span kind the transaction already carries,
29
+ # this is what the trace timeline reads to recognize a web request.
30
+ # Set here, where the transaction is created, so they land on the
31
+ # transaction's own span rather than on the event started below.
32
+ #
33
+ # Webmachine isn't Rack: the path, scheme, query string, host and port
34
+ # come off the request's `URI` rather than from Rack's readers. The
35
+ # request's own `query` is the parsed form, so the URI is where the
36
+ # string itself is. Webmachine has no environment to read the protocol
37
+ # version from, so that one is left undescribed.
38
+ transaction.add_opentelemetry_attributes(
39
+ Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
40
+ :method => request.method,
41
+ :path => request.uri&.path,
42
+ :scheme => request.uri&.scheme,
43
+ :query => request.uri&.query,
44
+ :host => request.uri&.host,
45
+ :port => request.uri&.port
46
+ )
47
+ )
48
+ end
49
+
16
50
  begin
17
- transaction.add_params_if_nil { request.query }
18
- transaction.add_headers_if_nil { request.headers if request.respond_to?(:headers) }
51
+ transaction.add_query_parameters_if_nil { request.query }
52
+ # `Webmachine::Headers` names a header the way OpenTelemetry does, in
53
+ # lowercase and with dashes, so these are headers and nothing else.
54
+ transaction.add_request_headers_if_nil do
55
+ request.headers if request.respond_to?(:headers)
56
+ end
19
57
 
20
- Appsignal.instrument("process_action.webmachine") do
58
+ Appsignal.instrument(
59
+ "process_action.webmachine",
60
+ :opentelemetry_scope => ["appsignal-ruby/webmachine", Appsignal::VERSION]
61
+ ) do
21
62
  super
22
63
  end
23
64
  ensure
24
65
  transaction.set_action_if_nil("#{resource.class.name}##{request.method}")
25
66
 
26
- Appsignal::Transaction.complete_current! unless has_parent_transaction
67
+ unless has_parent_transaction
68
+ # Describes the response on the transaction's span, which the
69
+ # semantic conventions ask for whenever a response was sent. The
70
+ # event above has closed by now, so this lands on the transaction's
71
+ # own span.
72
+ transaction.add_opentelemetry_attributes(
73
+ Appsignal::OpenTelemetry::HttpResponse.attributes_for(response.code)
74
+ )
75
+
76
+ Appsignal::Transaction.complete_current!
77
+ end
27
78
  end
28
79
  end
29
80
 
@@ -20,7 +20,8 @@ module Appsignal
20
20
  Padrino.before_load do
21
21
  Padrino.use Appsignal::Rack::EventMiddleware
22
22
  Padrino.use Appsignal::Rack::SinatraBaseInstrumentation,
23
- :instrument_event_name => "process_action.padrino"
23
+ :instrument_event_name => "process_action.padrino",
24
+ :opentelemetry_scope => ["appsignal-ruby/padrino", Appsignal::VERSION]
24
25
  end
25
26
  end
26
27
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Logger < ::Logger
5
+ # @!visibility private
6
+ #
7
+ # Routes Appsignal::Logger emits through the AppSignal C-extension,
8
+ # which forwards them to the agent. This is the default backend used
9
+ # when collector mode is not active.
10
+ module ExtensionBackend
11
+ class << self
12
+ def emit(group, severity, format, message, attributes)
13
+ Appsignal::Extension.log(
14
+ group,
15
+ SEVERITY_MAP.fetch(severity, 0),
16
+ format,
17
+ message,
18
+ Appsignal::Utils::Data.generate(attributes)
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appsignal/opentelemetry/attributes"
4
+
5
+ module Appsignal
6
+ class Logger < ::Logger
7
+ # @!visibility private
8
+ #
9
+ # Routes Appsignal::Logger emits through the OpenTelemetry logs SDK
10
+ # using the logger provider configured at `Appsignal.start` time when
11
+ # collector mode is active.
12
+ #
13
+ # Each emit attaches two well-known attributes that the AppSignal
14
+ # collector consumes:
15
+ #
16
+ # - `appsignal.group` — overrides the collector's default
17
+ # `service.name`-based grouping with the logger's `group` argument.
18
+ # - `appsignal.format` — the lowercase parse-format name
19
+ # (`plaintext`/`logfmt`/`json`/`autodetect`) the processor uses to
20
+ # extract structured attributes from the message body.
21
+ module OpenTelemetryBackend
22
+ # Maps Ruby `::Logger` severities to OTel SeverityNumber + the
23
+ # human-readable severity text.
24
+ OTEL_SEVERITY_MAP = {
25
+ ::Logger::DEBUG => [5, "DEBUG"],
26
+ ::Logger::INFO => [9, "INFO"],
27
+ ::Logger::WARN => [13, "WARN"],
28
+ ::Logger::ERROR => [17, "ERROR"],
29
+ ::Logger::FATAL => [21, "FATAL"]
30
+ }.freeze
31
+
32
+ # Maps the integer parse-format flag on `Appsignal::Logger` to the
33
+ # lowercase string the AppSignal collector and processor share.
34
+ FORMAT_NAMES = {
35
+ Appsignal::Logger::PLAINTEXT => "plaintext",
36
+ Appsignal::Logger::LOGFMT => "logfmt",
37
+ Appsignal::Logger::JSON => "json",
38
+ Appsignal::Logger::AUTODETECT => "autodetect"
39
+ }.freeze
40
+
41
+ class << self
42
+ def emit(group, severity, format, message, attributes)
43
+ number, text = OTEL_SEVERITY_MAP.fetch(severity, [0, nil])
44
+ otel_attributes = Appsignal::OpenTelemetry::Attributes.format(attributes)
45
+ otel_attributes["appsignal.group"] = group.to_s
46
+ otel_attributes["appsignal.format"] = FORMAT_NAMES.fetch(format, "autodetect")
47
+ logger.on_emit(
48
+ :severity_number => number,
49
+ :severity_text => text,
50
+ :body => message,
51
+ :attributes => otel_attributes
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ # Resolve the OTel logger on each emit. The logger provider caches it
58
+ # by name, so this is a registry lookup rather than a rebuild, and it
59
+ # always reflects the currently configured provider.
60
+ def logger
61
+ ::OpenTelemetry.logger_provider.logger(:name => "appsignal-logger")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logger"
4
- require "set"
5
4
 
6
5
  module Appsignal
7
6
  # Logger that flushes logs to the AppSignal logging service.
@@ -55,6 +54,8 @@ module Appsignal
55
54
  # @!visibility private
56
55
  AUTODETECT = 3
57
56
  # @!visibility private
57
+ FORMATS = [PLAINTEXT, LOGFMT, JSON, AUTODETECT].freeze
58
+ # @!visibility private
58
59
  SEVERITY_MAP = {
59
60
  DEBUG => 2,
60
61
  INFO => 3,
@@ -83,7 +84,7 @@ module Appsignal
83
84
  @group = group
84
85
  @level = level
85
86
  @silenced = false
86
- @format = format
87
+ @format = validated_format(format)
87
88
  @mutex = Mutex.new
88
89
  @default_attributes = attributes
89
90
  @appsignal_attributes = attributes
@@ -145,13 +146,7 @@ module Appsignal
145
146
 
146
147
  message = formatter.call(severity, Time.now, group, message) if formatter
147
148
 
148
- Appsignal::Extension.log(
149
- group,
150
- SEVERITY_MAP.fetch(severity, 0),
151
- @format,
152
- message.to_s,
153
- Appsignal::Utils::Data.generate(appsignal_attributes)
154
- )
149
+ Appsignal::Backends.logger.emit(group, severity, @format, message.to_s, appsignal_attributes)
155
150
 
156
151
  false
157
152
  end
@@ -299,5 +294,14 @@ module Appsignal
299
294
  ensure
300
295
  @appsignal_attributes = default_attributes
301
296
  end
297
+
298
+ def validated_format(format)
299
+ return format if FORMATS.include?(format)
300
+
301
+ Appsignal.internal_logger.warn(
302
+ "Unknown Appsignal::Logger format #{format.inspect}; falling back to AUTODETECT"
303
+ )
304
+ AUTODETECT
305
+ end
302
306
  end
303
307
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Metrics
5
+ # @!visibility private
6
+ #
7
+ # Routes custom metric helper calls through the AppSignal C-extension,
8
+ # which forwards them to the agent. This is the default backend used
9
+ # when collector mode is not active.
10
+ module ExtensionBackend
11
+ class << self
12
+ def set_gauge(name, value, tags)
13
+ Appsignal::Extension.set_gauge(
14
+ name.to_s,
15
+ value.to_f,
16
+ Appsignal::Utils::Data.generate(tags)
17
+ )
18
+ rescue RangeError
19
+ Appsignal.internal_logger
20
+ .warn("The gauge value '#{value}' for metric '#{name}' is too big")
21
+ end
22
+
23
+ def increment_counter(name, value, tags)
24
+ Appsignal::Extension.increment_counter(
25
+ name.to_s,
26
+ value.to_f,
27
+ Appsignal::Utils::Data.generate(tags)
28
+ )
29
+ rescue RangeError
30
+ Appsignal.internal_logger
31
+ .warn("The counter value '#{value}' for metric '#{name}' is too big")
32
+ end
33
+
34
+ def add_distribution_value(name, value, tags)
35
+ Appsignal::Extension.add_distribution_value(
36
+ name.to_s,
37
+ value.to_f,
38
+ Appsignal::Utils::Data.generate(tags)
39
+ )
40
+ rescue RangeError
41
+ Appsignal.internal_logger
42
+ .warn("The distribution value '#{value}' for metric '#{name}' is too big")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end