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