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
@@ -0,0 +1,847 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "socket"
5
+
6
+ module Appsignal
7
+ class Transaction
8
+ # @!visibility private
9
+ #
10
+ # The transaction backend used in collector mode. Emits an OpenTelemetry
11
+ # root span for the transaction, a child span per instrumented event, and
12
+ # queue timing as a metric. Errors and breadcrumbs attach to whichever span
13
+ # is current when they happen.
14
+ class OpenTelemetryBackend < BaseBackend
15
+ TRACER_NAME = "appsignal-ruby"
16
+
17
+ # The action name given to a transaction that recorded an error without
18
+ # ever setting one. Bracketed so it reads as a marker rather than as a
19
+ # class the application defines, and so it sorts apart from real action
20
+ # names.
21
+ UNNAMED_ACTION = "[unnamed action]"
22
+
23
+ # Guards the process-wide warn-once state, which transactions touch
24
+ # concurrently on threaded servers. A constant so it is created once at
25
+ # load time rather than lazily (which would race).
26
+ WARN_ONCE_LOCK = Mutex.new
27
+
28
+ class << self
29
+ # Logs the block's message the first time it sees `key`, then stays quiet
30
+ # for that key for the rest of the process. Used for warnings that would
31
+ # otherwise repeat on every transaction. The message is built lazily, so
32
+ # a deduplicated call skips building it. The check-and-set is locked so
33
+ # concurrent transactions cannot both warn.
34
+ def warn_once(key)
35
+ first_time = WARN_ONCE_LOCK.synchronize do
36
+ next false if warned_keys.key?(key)
37
+
38
+ warned_keys[key] = true
39
+ end
40
+ Appsignal.internal_logger.warn(yield) if first_time
41
+ end
42
+
43
+ # @!visibility private
44
+ # Resets the warn-once state. Only used to keep test runs isolated.
45
+ def clear_warned!
46
+ WARN_ONCE_LOCK.synchronize { warned_keys.clear }
47
+ end
48
+
49
+ private
50
+
51
+ def warned_keys
52
+ @warned_keys ||= {}
53
+ end
54
+ end
55
+
56
+ # Collector treats SERVER/CONSUMER spans as subtrace roots; SERVER is the
57
+ # safe default when no kind is given (a transaction is almost always an
58
+ # external-triggered unit of work).
59
+ DEFAULT_SPAN_KIND = :server
60
+
61
+ # The span kinds a transaction may take. An unknown value would raise
62
+ # inside OpenTelemetry span creation, so it falls back to the default.
63
+ SPAN_KINDS = [:server, :consumer, :producer, :internal].freeze
64
+
65
+ # How the transaction's span relates to an incoming OpenTelemetry context
66
+ # when none is given. A web request continues the upstream trace, so
67
+ # parenting is the safe default.
68
+ DEFAULT_RELATIONSHIP = :parent
69
+
70
+ # How the transaction's span may relate to an incoming context. An unknown
71
+ # value would silently behave like `:none`, so it falls back to the
72
+ # default instead.
73
+ RELATIONSHIPS = [:parent, :link, :both, :none].freeze
74
+
75
+ # The collector expects "web"/"background"; the agent's processor converts
76
+ # these internal namespaces in agent mode, but nothing does in collector
77
+ # mode. Other namespaces pass through unchanged.
78
+ DISPLAY_NAMESPACE = {
79
+ "http_request" => "web",
80
+ "background_job" => "background"
81
+ }.freeze
82
+
83
+ # Placeholder name an event span carries between `start_event` and
84
+ # `finish_event`. `finish_event` overwrites it with the AS::N event name,
85
+ # so it only surfaces when `complete` has to drain a span that was started
86
+ # but never finished. It is deliberately an obvious placeholder rather
87
+ # than a plausible event name, so such a span reads as the unfinished
88
+ # event it is and is not mistaken for a real one.
89
+ EVENT_SPAN_PLACEHOLDER_NAME = "[unfinished transaction event]"
90
+
91
+ # Sentinel value the AppSignal collector recognizes as "a SQL system
92
+ # we don't know the specific dialect of" — sufficient to trigger SQL
93
+ # sanitization on `db.query.text`.
94
+ SQL_DB_SYSTEM = "other_sql"
95
+
96
+ # Epoch-ms floor (~year 2000) below which a queue start is ignored. Mirrors
97
+ # the agent's `set_queue_start` (ext `transaction.rs`), which only records a
98
+ # queue duration when `queue_start_ms > 946_681_200_000`.
99
+ QUEUE_START_MIN = 946_681_200_000
100
+
101
+ # One open event on the event stack. Holds the OpenTelemetry span and the
102
+ # context token attached for it, plus the allocation bookkeeping for the
103
+ # event. `allocation_start` is the allocation counter when the event began
104
+ # (nil when allocation tracking is off), and `child_allocation_count`
105
+ # accumulates the full allocation counts of the event's finished children,
106
+ # so the event's own allocations are `full - child_allocation_count`.
107
+ # `db_system_name_set` tracks whether the event itself already set
108
+ # `db.system.name` (through `set_attributes`) before it finishes, so the
109
+ # SQL sentinel written at finish only fills in a value that is missing.
110
+ EventFrame = Struct.new(
111
+ :span, :token, :allocation_start, :child_allocation_count,
112
+ :db_system_name_set
113
+ )
114
+
115
+ def initialize( # rubocop:disable Metrics/ParameterLists
116
+ transaction_id,
117
+ namespace,
118
+ opentelemetry_context: nil,
119
+ opentelemetry_scope: nil,
120
+ opentelemetry_kind: nil,
121
+ opentelemetry_relationship: nil
122
+ )
123
+ super()
124
+ @transaction_id = transaction_id
125
+ @namespace = namespace
126
+ @scope = opentelemetry_scope
127
+ @completed = false
128
+ @event_stack = []
129
+ @breadcrumb_count = 0
130
+ @queue_start = nil
131
+ @start_time = Time.now
132
+ @action_set = false
133
+ @action = nil
134
+ @error_set = false
135
+ @allocation_start = current_allocation_count
136
+ @root_child_allocation_count = 0
137
+
138
+ kind = validated_option(
139
+ opentelemetry_kind, SPAN_KINDS, DEFAULT_SPAN_KIND, "opentelemetry_kind"
140
+ )
141
+ relationship = validated_option(
142
+ opentelemetry_relationship, RELATIONSHIPS, DEFAULT_RELATIONSHIP,
143
+ "opentelemetry_relationship"
144
+ )
145
+ @span = start_transaction_span(namespace, kind, relationship, opentelemetry_context)
146
+ @context_token = ::OpenTelemetry::Context.attach(
147
+ ::OpenTelemetry::Trace.context_with_span(@span)
148
+ )
149
+
150
+ # Transaction#initialize sets the namespace directly without calling
151
+ # set_namespace, so emit the attribute from here.
152
+ @span.set_attribute("appsignal.namespace", display_namespace(namespace)) if namespace
153
+ end
154
+
155
+ # `opentelemetry_kind` (e.g. `:client` for an outgoing HTTP request) is set
156
+ # at span creation because OTel span kind is immutable afterwards. `nil`
157
+ # leaves the SDK default (INTERNAL).
158
+ def start_event(opentelemetry_kind: nil, opentelemetry_scope: nil)
159
+ span = tracer_for(opentelemetry_scope)
160
+ .start_span(EVENT_SPAN_PLACEHOLDER_NAME, :kind => opentelemetry_kind)
161
+ token = ::OpenTelemetry::Context.attach(
162
+ ::OpenTelemetry::Trace.context_with_span(span)
163
+ )
164
+ push_event(span, token)
165
+ end
166
+
167
+ def finish_event(name, title, body, body_format)
168
+ return if @event_stack.empty?
169
+
170
+ frame = @event_stack.pop
171
+ write_event_span_name(frame.span, name, title)
172
+ write_event_body_attributes(frame.span, body, body_format, frame.db_system_name_set)
173
+ write_event_allocation_count(frame)
174
+ ::OpenTelemetry::Context.detach(frame.token)
175
+ frame.span.finish
176
+ end
177
+
178
+ # `opentelemetry_kind` is set at span creation (kind is immutable in OTel),
179
+ # mirroring `start_event`. `nil` leaves the SDK default (INTERNAL).
180
+ def record_event( # rubocop:disable Metrics/ParameterLists
181
+ name, title, body, body_format, duration,
182
+ opentelemetry_kind: nil, opentelemetry_scope: nil, opentelemetry_attributes: nil
183
+ )
184
+ start_time = Time.now - (duration / 1_000_000_000.0)
185
+ span = tracer_for(opentelemetry_scope).start_span(
186
+ EVENT_SPAN_PLACEHOLDER_NAME,
187
+ :start_timestamp => start_time,
188
+ :kind => opentelemetry_kind
189
+ )
190
+ write_event_span_name(span, name, title)
191
+ # A recorded event has no window between start and finish, so this is
192
+ # its only chance to attach attributes -- there is no event frame for
193
+ # a later `set_attributes` call to note a `db.system.name` on.
194
+ formatted_attributes = Appsignal::OpenTelemetry::Attributes.format(
195
+ opentelemetry_attributes || {}
196
+ )
197
+ span.add_attributes(formatted_attributes) unless formatted_attributes.empty?
198
+ write_event_body_attributes(
199
+ span, body, body_format, named_db_system?(formatted_attributes)
200
+ )
201
+ # A recorded event has no start hook, so we never measured its
202
+ # allocations. We deliberately set no allocation attribute rather than a
203
+ # misleading zero. Its allocations instead fall into the enclosing
204
+ # event's own count, matching agent mode.
205
+ span.finish
206
+ end
207
+
208
+ def set_action(action)
209
+ # The collector reads the action from `appsignal.action_name`, not the
210
+ # span name. Set the name too so the OTel-native trace stays readable;
211
+ # the collector treats the span name as authoritative for display.
212
+ @span.name = action
213
+ @span.set_attribute("appsignal.action_name", action)
214
+ @action = action
215
+ @action_set = true
216
+ end
217
+
218
+ def set_namespace(namespace)
219
+ # Only the attribute can change here: SpanKind is fixed at span
220
+ # creation (immutable in OTel) from the initial namespace. A later
221
+ # namespace override updates `appsignal.namespace` but not the kind --
222
+ # the collector uses the attribute for the namespace and the kind only
223
+ # to pick the subtrace root, so this is fine for the rare late change.
224
+ @namespace = namespace
225
+ @span.set_attribute("appsignal.namespace", display_namespace(namespace))
226
+ end
227
+
228
+ # Queue start has no OTel-native home, so surface it two ways: an
229
+ # `appsignal.queue_start` event on the root span (per-trace timeline) and,
230
+ # at completion, a `transaction_queue_duration` metric (the aggregate
231
+ # graph). Like the agent, we record the delta and never shift span timing.
232
+ #
233
+ # The queue start time becomes the event's timestamp (events carry their
234
+ # own time), so it is not duplicated as an attribute.
235
+ def set_queue_start(start)
236
+ return unless start && start > QUEUE_START_MIN
237
+
238
+ @queue_start = start
239
+ @span.add_event(
240
+ "appsignal.queue_start",
241
+ :timestamp => Time.at(start / 1000.0)
242
+ )
243
+ end
244
+
245
+ # Transaction metadata (request path, method, ...) has no dedicated OTel
246
+ # attribute, but it is the same shape as tags and the collector/trace UI
247
+ # already surface `appsignal.tag.*`, so emit metadata as a tag.
248
+ def set_metadata(key, value)
249
+ @span.set_attribute("appsignal.tag.#{key}", value)
250
+ end
251
+
252
+ # Sets OpenTelemetry attributes on AppSignal's current span -- the open
253
+ # event span, or the root span when no event is open. This is how an
254
+ # integration describes what it instrumented in OpenTelemetry's own terms,
255
+ # such as `db.system.name` on a database query.
256
+ #
257
+ # Never the OTel current span, which may belong to another
258
+ # instrumentation. Values are coerced to the primitives OTLP accepts.
259
+ def set_attributes(attributes)
260
+ formatted = Appsignal::OpenTelemetry::Attributes.format(attributes)
261
+ # Note on the open event frame, if there is one, that this event
262
+ # already named a real `db.system.name`, so `write_event_body_attributes`
263
+ # knows not to overwrite it with the SQL sentinel when the event
264
+ # finishes.
265
+ if named_db_system?(formatted) && (frame = @event_stack.last)
266
+ frame.db_system_name_set = true
267
+ end
268
+ current_span.add_attributes(formatted)
269
+ end
270
+
271
+ # The collector keeps the request payload, the function parameters and the
272
+ # query parameters as separate attributes, so each gets its own bucket.
273
+ # Legacy `params` has no channel of its own, so it maps to the request
274
+ # payload (the web/server default), matching how the span kind defaults to
275
+ # `:server`.
276
+ PARAMS_MAPPING = {
277
+ :params => :request_payload,
278
+ :request_payload => :request_payload,
279
+ :function_parameters => :function_parameters,
280
+ :query_parameters => :query_parameters
281
+ }.freeze
282
+
283
+ def params_mapping
284
+ PARAMS_MAPPING
285
+ end
286
+
287
+ PARAMS_OPTIONS = {
288
+ :request_payload => {
289
+ :filter => :filter_request_payload,
290
+ :send => :send_request_payload
291
+ },
292
+ :function_parameters => {
293
+ :filter => :filter_function_parameters,
294
+ :send => :send_function_parameters
295
+ },
296
+ :query_parameters => {
297
+ :filter => :filter_request_query_parameters,
298
+ :send => :send_request_query_parameters
299
+ }
300
+ }.freeze
301
+
302
+ def params_options
303
+ PARAMS_OPTIONS
304
+ end
305
+
306
+ HEADERS_MAPPING = {
307
+ :request_headers => [:request_headers, nil],
308
+ :request_environment => [:environment, nil]
309
+ }.freeze
310
+
311
+ def headers_mapping
312
+ HEADERS_MAPPING
313
+ end
314
+
315
+ HEADERS_ALLOWLIST = {
316
+ :request_headers => [:keep_request_headers, true],
317
+ :environment => [:keep_request_environment, false]
318
+ }.freeze
319
+
320
+ def headers_allowlist
321
+ HEADERS_ALLOWLIST
322
+ end
323
+
324
+ # Routes each sample-data category to the attribute the collector reads.
325
+ # The params arrive on one of three channels: `request_payload` (web),
326
+ # `function_parameters` (jobs) and `query_parameters` (a request's query
327
+ # string), each its own attribute. The other JSON-blob
328
+ # categories (session, custom data) are serialized as JSON; `environment`
329
+ # becomes request-header attributes; tags fan out to `appsignal.tag.*`.
330
+ # Unknown keys pass through as `appsignal.<key>` JSON so nothing is lost.
331
+ # Breadcrumbs never reach here (the backend emits them as span events);
332
+ # causes ride on the exception event (see #set_error).
333
+ def set_sample_data(key, data)
334
+ case key
335
+ when "request_payload"
336
+ @span.set_attribute("appsignal.request.payload", JSON.generate(data))
337
+ when "function_parameters"
338
+ @span.set_attribute("appsignal.function.parameters", JSON.generate(data))
339
+ when "query_parameters"
340
+ @span.set_attribute("appsignal.request.query_parameters", JSON.generate(data))
341
+ when "session_data"
342
+ @span.set_attribute("appsignal.request.session_data", JSON.generate(data))
343
+ when "custom_data"
344
+ @span.set_attribute("appsignal.custom_data", JSON.generate(data))
345
+ when "request_headers"
346
+ write_request_headers(data)
347
+ when "environment"
348
+ write_request_environment(data)
349
+ when "tags"
350
+ write_tags(data)
351
+ else
352
+ @span.set_attribute("appsignal.#{key}", JSON.generate(data))
353
+ end
354
+ end
355
+
356
+ # Records the error as an `exception` event on AppSignal's current span --
357
+ # the open event span, or the root -- so it attaches to the operation that
358
+ # raised it. `appsignal.alert_this_error` tells the collector to report it
359
+ # even on a child span.
360
+ #
361
+ # Causes ride on one `appsignal.error_causes` JSON attribute, whose keys
362
+ # match the processor's `ErrorSubCause`. Separate cause events would each
363
+ # become their own incident. Each cause carries only the part of its
364
+ # backtrace that is not shared (see `trim_shared_tail`).
365
+ def set_error(class_name, message, backtrace, causes, _root_cause_missing)
366
+ @error_set = true
367
+ span = current_span
368
+ error_lines = Array(backtrace)
369
+
370
+ attributes = {
371
+ "exception.type" => class_name,
372
+ "exception.message" => message.to_s,
373
+ "exception.stacktrace" => error_lines.join("\n"),
374
+ "appsignal.alert_this_error" => true
375
+ }
376
+
377
+ unless causes.empty?
378
+ attributes["appsignal.error_causes"] = JSON.generate(
379
+ causes.map do |cause|
380
+ lines, lines_omitted = trim_shared_tail(Array(cause[:backtrace]), error_lines)
381
+
382
+ cause_attributes = {
383
+ "name" => cause[:name],
384
+ "message" => cause[:message],
385
+ "lines" => lines
386
+ }
387
+ cause_attributes["lines_omitted"] = lines_omitted if lines_omitted.positive?
388
+ cause_attributes
389
+ end
390
+ )
391
+ end
392
+
393
+ span.add_event("exception", :attributes => attributes)
394
+ # `error.type` is what the semantic conventions read to tell what kind of
395
+ # failure ended the operation. It is an attribute of the span, unlike the
396
+ # `exception.type` above, which is an attribute of the exception event.
397
+ # When a span collects more than one error the last one wins, because a
398
+ # span can only say one thing here.
399
+ span.add_attributes(Appsignal::OpenTelemetry::ErrorType.attributes_for(class_name))
400
+ span.status = ::OpenTelemetry::Trace::Status.error
401
+ end
402
+
403
+ # Emits a breadcrumb as an `appsignal.breadcrumb` span event on AppSignal's
404
+ # current span -- the open event span, falling back to the root -- rather
405
+ # than the OTel current span, which may belong to another instrumentation.
406
+ #
407
+ # Emitted immediately, because by completion the event span has finished
408
+ # and the SDK drops events added to an ended span. The breadcrumb's time
409
+ # becomes the event's timestamp, and the metadata Hash is a JSON string,
410
+ # because event attributes are flat.
411
+ #
412
+ # Capped at `BREADCRUMB_LIMIT` per transaction, keeping the first N where
413
+ # agent mode keeps the last N: a streamed event cannot be retracted.
414
+ def add_breadcrumb(breadcrumb)
415
+ return if @breadcrumb_count >= Appsignal::Transaction::BREADCRUMB_LIMIT
416
+
417
+ @breadcrumb_count += 1
418
+ current_span.add_event(
419
+ "appsignal.breadcrumb",
420
+ :timestamp => Time.at(breadcrumb[:time]),
421
+ :attributes => {
422
+ "category" => breadcrumb[:category],
423
+ "action" => breadcrumb[:action],
424
+ "message" => breadcrumb[:message],
425
+ "metadata" => JSON.generate(breadcrumb[:metadata] || {})
426
+ }
427
+ )
428
+ end
429
+
430
+ # Returns `true` so `Transaction#complete` runs `sample_data`, flushing the
431
+ # params/session/custom-data/tags/etc. onto the still-open root span before
432
+ # `complete` finishes it. The OTel SDK makes its own sampling decision; the
433
+ # gem always populates the span.
434
+ def finish
435
+ true
436
+ end
437
+
438
+ def complete
439
+ # `teardown` sets `@completed`, so this guard also makes the body
440
+ # idempotent across a double `complete`, and skips it on `discard`.
441
+ unless @completed
442
+ # Settle the action first, because the allocation count metric below
443
+ # tags by `@action`, and this is what fills it in for a transaction
444
+ # the application never named.
445
+ resolve_missing_action
446
+ emit_queue_duration_metric if should_report?
447
+ report_allocation_count
448
+ end
449
+ teardown
450
+ end
451
+
452
+ # Discarding does not mean "don't send" as it does in agent mode. The root
453
+ # span is still finished and exported, flagged with
454
+ # `appsignal.ignore_subtrace` so the collector drops the whole subtrace.
455
+ # The flag has to be written before the span finishes, because attributes
456
+ # set on an ended span are dropped. Tearing the span down here also
457
+ # detaches the context, so a discarded transaction cannot leave its root
458
+ # span current on the thread.
459
+ def discard
460
+ return if @completed
461
+
462
+ @span&.set_attribute("appsignal.ignore_subtrace", true)
463
+ teardown
464
+ end
465
+
466
+ # Each error is recorded eagerly as its own `exception` event on the span
467
+ # current when it was added, so a trace holds many errors and the
468
+ # Transaction never duplicates itself -- which is why `duplicate` is left
469
+ # unimplemented (see BaseBackend).
470
+ def supports_multiple_errors?
471
+ true
472
+ end
473
+
474
+ # Returned so `Transaction#to_h` (`JSON.parse(@backend.to_json)`) yields an
475
+ # empty Hash. Collector mode asserts on emitted spans, not `to_h`.
476
+ def to_json # rubocop:disable Lint/ToJSON
477
+ "{}"
478
+ end
479
+
480
+ private
481
+
482
+ # Detaches the OTel context and finishes the root span. Idempotent: the
483
+ # Transaction can complete directly and again via a cleanup path, and
484
+ # re-detaching/re-finishing an ended span would error.
485
+ def teardown
486
+ return if @completed
487
+
488
+ @completed = true
489
+ # Release any event span left unfinished by an aborted flow, so the
490
+ # root context can detach in LIFO order.
491
+ until @event_stack.empty?
492
+ frame = @event_stack.pop
493
+ ::OpenTelemetry::Context.detach(frame.token)
494
+ frame.span.finish
495
+ end
496
+ ::OpenTelemetry::Context.detach(@context_token) if @context_token
497
+ @span&.finish
498
+ end
499
+
500
+ # Whether this transaction is reported at all. One that set an action is
501
+ # reported so it can be grouped under it. One that recorded an error is
502
+ # reported even without an action, because the failure is worth reporting
503
+ # even when we cannot say what failed. The agent draws the line in the
504
+ # same place: it drops a transaction only when it has neither.
505
+ def should_report?
506
+ @action_set || @error_set
507
+ end
508
+
509
+ # Decides what to do about a transaction that never set an action, which
510
+ # has nothing to group by. Collector mode cannot represent "no action", so
511
+ # it needs a name for the ones it reports and a way to drop the rest.
512
+ #
513
+ # A reported one is named after the marker. Leaving the action unset would
514
+ # not report nothing: the collector fills an empty action in from the span
515
+ # name, which here is the placeholder this backend opened the span with,
516
+ # and that reads as though it were the application's own action.
517
+ #
518
+ # The rest are noise, such as serving assets in development, so their
519
+ # subtrace is flagged for the collector to drop, the same way `discard`
520
+ # does.
521
+ #
522
+ # Both branches have to run before `teardown` finishes the span, because
523
+ # attributes set on an ended span are dropped.
524
+ def resolve_missing_action
525
+ return if @action_set
526
+
527
+ if should_report?
528
+ set_action(UNNAMED_ACTION)
529
+ else
530
+ @span&.set_attribute("appsignal.ignore_subtrace", true)
531
+ end
532
+ end
533
+
534
+ # Emits the queue duration as a distribution metric in both the
535
+ # per-namespace and per-namespace-and-host series the queue-time graph
536
+ # reads. Nothing downstream fans these out, so emit both ourselves.
537
+ def emit_queue_duration_metric
538
+ return unless @queue_start
539
+
540
+ duration_ms = (@start_time.to_f * 1000) - @queue_start
541
+ return if duration_ms.negative?
542
+
543
+ namespace = display_namespace(@namespace)
544
+ Appsignal::Metrics::OpenTelemetryBackend.add_distribution_value(
545
+ "transaction_queue_duration", duration_ms, :namespace => namespace
546
+ )
547
+ Appsignal::Metrics::OpenTelemetryBackend.add_distribution_value(
548
+ "transaction_queue_duration", duration_ms,
549
+ :namespace => namespace, :hostname => hostname
550
+ )
551
+ end
552
+
553
+ # Sets the transaction's allocation counts on the root span, and, when the
554
+ # transaction has an action to group by, emits the total as a counter
555
+ # metric. The counter is read once so the attributes and the metric share
556
+ # the same value.
557
+ #
558
+ # The root's total is `appsignal.transaction_allocation_count`, named apart
559
+ # from an event's `appsignal.allocation_count` because it resets per
560
+ # transaction: it is the whole that a span's `self_allocation_count` is a
561
+ # part of, including across a distributed trace.
562
+ #
563
+ # The metric is emitted in both the per-namespace and
564
+ # per-namespace-and-action series the allocation graph reads, as a counter,
565
+ # never host-tagged. Nothing downstream fans these out.
566
+ def report_allocation_count
567
+ return unless @allocation_start
568
+
569
+ count = Appsignal::Extension.allocation_count - @allocation_start
570
+ return if allocation_count_reversed?(count)
571
+
572
+ @span&.set_attribute("appsignal.transaction_allocation_count", count)
573
+ @span&.set_attribute(
574
+ "appsignal.self_allocation_count",
575
+ count - @root_child_allocation_count
576
+ )
577
+
578
+ return unless should_report? && count.positive?
579
+
580
+ namespace = display_namespace(@namespace)
581
+ Appsignal::Metrics::OpenTelemetryBackend.increment_counter(
582
+ "transaction_allocation_count", count, :namespace => namespace
583
+ )
584
+ Appsignal::Metrics::OpenTelemetryBackend.increment_counter(
585
+ "transaction_allocation_count", count,
586
+ :namespace => namespace, :action => @action
587
+ )
588
+ end
589
+
590
+ # Sets a finished event's allocation counts and rolls its full count up to
591
+ # its parent. `appsignal.allocation_count` covers the event's whole
592
+ # subtree; `appsignal.self_allocation_count` excludes its children, so
593
+ # allocations can be attributed to a layer without walking the span tree.
594
+ #
595
+ # Only the immediate parent is updated, because each event's full count
596
+ # already includes its whole subtree. Nothing is set when allocation
597
+ # tracking is off.
598
+ def write_event_allocation_count(frame)
599
+ return unless frame.allocation_start
600
+
601
+ full = Appsignal::Extension.allocation_count - frame.allocation_start
602
+ return if allocation_count_reversed?(full)
603
+
604
+ self_count = full - frame.child_allocation_count
605
+ # Roll the full count up to the parent so it can compute its own self.
606
+ # A top-level event has no parent event; its full count belongs to the
607
+ # transaction, so credit the root accumulator instead.
608
+ if (parent = @event_stack.last)
609
+ parent.child_allocation_count += full
610
+ else
611
+ @root_child_allocation_count += full
612
+ end
613
+ frame.span.set_attribute("appsignal.allocation_count", full)
614
+ frame.span.set_attribute("appsignal.self_allocation_count", self_count)
615
+ end
616
+
617
+ # The allocation counter is thread-local and only ever increases, so a
618
+ # negative delta means the transaction or event finished on a different
619
+ # thread than it started on. The count is then meaningless, so warn and
620
+ # tell the caller to drop it rather than report a wrong value.
621
+ def allocation_count_reversed?(delta)
622
+ return false unless delta.negative?
623
+
624
+ Appsignal.internal_logger.warn(
625
+ "Not reporting an allocation count in transaction " \
626
+ "'#{@transaction_id}'. The thread-local allocation counter decreased " \
627
+ "between the start and finish, which happens when the work starts and " \
628
+ "finishes on different threads."
629
+ )
630
+ true
631
+ end
632
+
633
+ # The thread's cumulative object allocation count, or nil when allocation
634
+ # tracking is off. Callers snapshot this at a start boundary and subtract
635
+ # it from a later read to get the allocations made in between; a nil
636
+ # snapshot disables allocation reporting for that transaction or event.
637
+ def current_allocation_count
638
+ Appsignal::Extension.allocation_count if allocation_tracking?
639
+ end
640
+
641
+ # Allocation tracking runs only when enabled by config and not on JRuby,
642
+ # matching the condition under which `Appsignal.start` installs the
643
+ # allocation event hook that feeds the counter.
644
+ def allocation_tracking?
645
+ return false unless Appsignal.config&.[](:enable_allocation_tracking)
646
+
647
+ !Appsignal::System.jruby?
648
+ end
649
+
650
+ def hostname
651
+ Appsignal.config&.[](:hostname) || Socket.gethostname
652
+ end
653
+
654
+ # Resolve the tracer for an instrumentation scope. `scope` is a
655
+ # `[name, version]` pair supplied by the integration that created the
656
+ # span, or nil. A nil scope, a nil/blank name, or a nil version each fall
657
+ # back to the default AppSignal scope, so every span always carries a
658
+ # scope (the collector drops scope-less spans). The tracer provider caches
659
+ # tracers by `(name, version)`, so this resolves rather than rebuilds.
660
+ def tracer_for(scope)
661
+ name, version = scope
662
+ if name.nil? || name.to_s.empty?
663
+ # A nil scope or one with a blank name is unusable, so fall back to
664
+ # the default scope entirely rather than pairing the default name with
665
+ # a stray version.
666
+ name = TRACER_NAME
667
+ version = Appsignal::VERSION
668
+ else
669
+ version ||= Appsignal::VERSION
670
+ end
671
+ ::OpenTelemetry.tracer_provider.tracer(name, version)
672
+ end
673
+
674
+ # The open event span, or the root span when no event is open. Not the OTel
675
+ # current span, which may belong to another instrumentation.
676
+ def current_span
677
+ @event_stack.last&.span || @span
678
+ end
679
+
680
+ # Pushes an open event onto the stack, snapshotting the allocation counter
681
+ # so `finish_event` can measure the event's allocations as the delta since.
682
+ def push_event(span, token)
683
+ @event_stack.push(EventFrame.new(span, token, current_allocation_count, 0))
684
+ end
685
+
686
+ def placeholder_span_name(namespace)
687
+ "appsignal.transaction #{namespace}"
688
+ end
689
+
690
+ # Open the transaction's span, relating it to any incoming trace context
691
+ # by the requested relationship:
692
+ #
693
+ # - `:parent`: parent under the remote span so the transaction continues
694
+ # the upstream trace.
695
+ # - `:link`: start a fresh trace linked back to the remote span. The
696
+ # transaction is its own unit of work decoupled from the caller, so it
697
+ # gets its own trace, with a link recording the causal relationship.
698
+ # - `:both`: parent under the remote span and also link back to it, so the
699
+ # transaction continues the trace and keeps the explicit link.
700
+ # - `:none`: a plain root span that ignores any incoming context.
701
+ #
702
+ # With no context or an invalid remote span, every relationship falls back
703
+ # to a plain root span, since there is nothing to parent or link to.
704
+ def start_transaction_span(namespace, kind, relationship, opentelemetry_context)
705
+ name = placeholder_span_name(namespace)
706
+ remote = Appsignal::OpenTelemetry.remote_span_context(opentelemetry_context)
707
+ tracer = tracer_for(@scope)
708
+
709
+ # With no incoming context (or an invalid remote span) there is nothing
710
+ # to parent or link to, so any relationship is just a plain root span.
711
+ return tracer.start_root_span(name, :kind => kind) unless remote
712
+
713
+ # `:parent` and `:both` continue the trace under the remote span;
714
+ # `:link` and `:both` record a link back to it; `:none` does neither.
715
+ parent = opentelemetry_context if [:parent, :both].include?(relationship)
716
+ links = [::OpenTelemetry::Trace::Link.new(remote)] if [:link, :both].include?(relationship)
717
+
718
+ if parent
719
+ tracer.start_span(name, :with_parent => parent, :kind => kind, :links => links)
720
+ else
721
+ tracer.start_root_span(name, :kind => kind, :links => links)
722
+ end
723
+ end
724
+
725
+ # Returns the given option when it is one of the allowed values, the
726
+ # default when it is nil, or the default with a warning when it is an
727
+ # unknown value. Keeps an unexpected `opentelemetry_kind` from raising
728
+ # inside span creation, and an unexpected `opentelemetry_relationship`
729
+ # from silently dropping the incoming context.
730
+ def validated_option(value, allowed, default, name)
731
+ return default if value.nil?
732
+ return value if allowed.include?(value)
733
+
734
+ # A bad value is usually a static mistake passed on every transaction,
735
+ # so warn once per process to avoid flooding the log. Dedup on the
736
+ # option and value, and build the message -- including walking the
737
+ # stack for the caller location -- only when actually warning.
738
+ self.class.warn_once("#{name}: #{value.inspect}") do
739
+ "Unknown #{name} #{value.inspect} passed at #{option_caller_location}, " \
740
+ "falling back to #{default.inspect}. " \
741
+ "Expected one of: #{allowed.map(&:inspect).join(", ")}."
742
+ end
743
+ default
744
+ end
745
+
746
+ # The first caller frame outside the gem: where the invalid value was
747
+ # passed to `Transaction.create`, `Appsignal.monitor`, etc. Falls back to
748
+ # the immediate caller if every frame is inside the gem. Only walks the
749
+ # stack when a warning is actually emitted (see `validated_option`).
750
+ def option_caller_location
751
+ frames = caller
752
+ frames.find { |frame| !frame.include?("/lib/appsignal/") } || frames.first
753
+ end
754
+
755
+ def display_namespace(namespace)
756
+ DISPLAY_NAMESPACE.fetch(namespace, namespace)
757
+ end
758
+
759
+ def write_request_headers(headers)
760
+ headers.each do |name, value|
761
+ @span.set_attribute("http.request.header.#{name}", value.to_s)
762
+ end
763
+ end
764
+
765
+ def write_request_environment(environment)
766
+ environment.each do |key, value|
767
+ @span.set_attribute("appsignal.environment.#{key}", value.to_s)
768
+ end
769
+ end
770
+
771
+ # Each tag becomes its own `appsignal.tag.<key>` attribute, which the
772
+ # collector hoists and the trace UI lists under "Tags". `sanitized_tags`
773
+ # already restricts values to String/Symbol/Integer/boolean; OTel
774
+ # attribute values must be primitives, so coerce the Symbol case to a
775
+ # string (the only non-primitive that survives sanitization).
776
+ def write_tags(tags)
777
+ tags.each do |key, value|
778
+ value = value.to_s if value.is_a?(Symbol)
779
+ @span.set_attribute("appsignal.tag.#{key}", value)
780
+ end
781
+ end
782
+
783
+ # Returns the leading lines of a cause's backtrace that the reported
784
+ # error's backtrace does not already end with, and how many trailing lines
785
+ # were dropped to get there.
786
+ #
787
+ # A cause shares its trailing frames with the error it led to, and those
788
+ # are already sent in `exception.stacktrace`. Repeating them for every
789
+ # cause makes `appsignal.error_causes` too long for the collector to read.
790
+ #
791
+ # If every line is shared, one is kept, because a cause with no lines
792
+ # leaves the UI nothing to show. That kept line does not count as dropped.
793
+ def trim_shared_tail(cause_lines, error_lines)
794
+ shared = 0
795
+ while shared < cause_lines.length && shared < error_lines.length &&
796
+ cause_lines[-1 - shared] == error_lines[-1 - shared]
797
+ shared += 1
798
+ end
799
+
800
+ return [cause_lines, 0] if shared.zero?
801
+
802
+ kept = [cause_lines.length - shared, 1].max
803
+ [cause_lines.first(kept), cause_lines.length - kept]
804
+ end
805
+
806
+ # The OTel span name is what the collector surfaces as the event's
807
+ # label in the trace UI. The AS::N `name` (e.g. "sql.active_record")
808
+ # always leads the span name so it stays visible. When a formatter
809
+ # supplied a human-readable `title` (e.g. "User Load", "GET
810
+ # https://example.com"), it follows in parentheses, giving
811
+ # "sql.active_record (User Load)". Some integrations pass the event
812
+ # name as the title as well; in that case the name is not repeated.
813
+ def write_event_span_name(span, name, title)
814
+ has_title = title && !title.empty? && title != name
815
+ span.name = has_title ? "#{name} (#{title})" : name
816
+ end
817
+
818
+ def write_event_body_attributes(span, body, body_format, db_system_name_set)
819
+ has_body = !body.to_s.empty?
820
+
821
+ if body_format == Appsignal::EventFormatter::SQL_BODY_FORMAT
822
+ # Name the datastore whether or not there is a query to record with it.
823
+ # The semantic conventions require the attribute on every database
824
+ # span, and a SQL event with nothing in its body is still a SQL event.
825
+ # Only fall back to the sentinel when nothing set a real engine name
826
+ # earlier in the event, so an integration's own `db.system.name`
827
+ # always wins over it.
828
+ span.set_attribute("db.system.name", SQL_DB_SYSTEM) unless db_system_name_set
829
+ span.set_attribute("db.query.text", body) if has_body
830
+ elsif has_body
831
+ span.set_attribute("appsignal.body", body)
832
+ end
833
+ end
834
+
835
+ # Whether a formatted attributes hash names a real `db.system.name`,
836
+ # as opposed to merely having the key. `Attributes.format` coerces an
837
+ # explicit `nil` (or any other non-primitive) to `""`, so the key can
838
+ # be present with a blank value. A blank value must not count as set:
839
+ # it would block the SQL sentinel the same way a real value should,
840
+ # but leave the span with nothing the collector's sanitizer
841
+ # recognizes, instead of the sentinel that keeps sanitization on.
842
+ def named_db_system?(formatted_attributes)
843
+ !formatted_attributes["db.system.name"].to_s.empty?
844
+ end
845
+ end
846
+ end
847
+ end