appsignal 4.10.3-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 +9 -50
- data/README.md +1 -2
- data/Rakefile +87 -0
- data/appsignal.gemspec +8 -7
- data/build_matrix.yml +14 -7
- data/ext/agent.rb +27 -27
- data/ext/appsignal_extension.c +14 -0
- data/lib/appsignal/backends.rb +55 -0
- data/lib/appsignal/cli/demo.rb +0 -5
- data/lib/appsignal/cli/diagnose.rb +38 -6
- data/lib/appsignal/cli/helpers.rb +0 -45
- 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 -179
- 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 -19
- data/lib/appsignal/integrations/data_mapper.rb +14 -2
- data/lib/appsignal/integrations/delayed_job_plugin.rb +64 -131
- 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 +8 -37
- 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 +25 -6
|
@@ -6,6 +6,22 @@ module Appsignal
|
|
|
6
6
|
class ActiveJobHook < Appsignal::Hooks::Hook
|
|
7
7
|
register :active_job
|
|
8
8
|
|
|
9
|
+
# This integration records the enqueue itself, as a producer event that
|
|
10
|
+
# also injects trace context, and Active Job's own `enqueue.active_job`
|
|
11
|
+
# notification fires nested inside it. Claim the event so that the
|
|
12
|
+
# generic notification paths leave it alone.
|
|
13
|
+
#
|
|
14
|
+
# Claimed here, when this file is required, rather than in `install`.
|
|
15
|
+
# `install` only runs when Active Job instrumentation is turned on, but
|
|
16
|
+
# a customer who turns it off does not want to see the native
|
|
17
|
+
# notification reported instead. This call does not touch any
|
|
18
|
+
# `ActiveJob` constant, so it is safe to run even when the library is
|
|
19
|
+
# not present.
|
|
20
|
+
Appsignal::EventFormatter.register(
|
|
21
|
+
"enqueue.active_job",
|
|
22
|
+
Appsignal::EventFormatter::RecordedElsewhere
|
|
23
|
+
)
|
|
24
|
+
|
|
9
25
|
def self.version_7_1_or_higher?
|
|
10
26
|
@version_7_1_or_higher ||=
|
|
11
27
|
if dependencies_present?
|
|
@@ -21,59 +37,6 @@ module Appsignal
|
|
|
21
37
|
defined?(::ActiveJob)
|
|
22
38
|
end
|
|
23
39
|
|
|
24
|
-
# The parameters of the Active Job method this hook wraps to record a
|
|
25
|
-
# bulk enqueue. The wrapper reads the array of jobs out of the second
|
|
26
|
-
# one, so it is only safe to wrap a method that still takes these.
|
|
27
|
-
EXPECTED_INSTRUMENT_ENQUEUE_ALL_PARAMETERS = [
|
|
28
|
-
[:req, :queue_adapter],
|
|
29
|
-
[:req, :jobs]
|
|
30
|
-
].freeze
|
|
31
|
-
|
|
32
|
-
# Whether Active Job records a bulk enqueue through a method at all. It
|
|
33
|
-
# only does so from version 7.1 on. Checking for the method, rather than
|
|
34
|
-
# for the version, keeps us from wrapping one on a version that has no
|
|
35
|
-
# bulk enqueue path to instrument.
|
|
36
|
-
def self.instrument_enqueue_all_defined?
|
|
37
|
-
::ActiveJob.singleton_class.private_method_defined?(:instrument_enqueue_all)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Whether that method still takes the arguments the wrapper reads.
|
|
41
|
-
#
|
|
42
|
-
# The method is private, so a later version of Active Job can change
|
|
43
|
-
# what it takes. Wrapping one that takes something else raises an
|
|
44
|
-
# ArgumentError inside every `ActiveJob.perform_all_later` call an
|
|
45
|
-
# application makes, which breaks enqueuing and not just its
|
|
46
|
-
# instrumentation. Refusing to wrap it is therefore the safe direction,
|
|
47
|
-
# whatever the refusal costs us.
|
|
48
|
-
def self.instrument_enqueue_all_parameters_match?
|
|
49
|
-
instrument_enqueue_all_parameters == EXPECTED_INSTRUMENT_ENQUEUE_ALL_PARAMETERS
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Active Job's own definition of the method, looked up past the wrapper
|
|
53
|
-
# this hook prepends. Installing twice would otherwise find that wrapper
|
|
54
|
-
# rather than the method it wraps, and so check it against itself. Only
|
|
55
|
-
# the wrapper is skipped, so a version of Active Job that defines the
|
|
56
|
-
# method somewhere else is still found.
|
|
57
|
-
def self.instrument_enqueue_all_method
|
|
58
|
-
method = ::ActiveJob.singleton_class.instance_method(:instrument_enqueue_all)
|
|
59
|
-
method = method.super_method while method&.owner == ActiveJobBulkEnqueueInstrumentation
|
|
60
|
-
method
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def self.instrument_enqueue_all_parameters
|
|
64
|
-
instrument_enqueue_all_method&.parameters
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Names both sides of the mismatch, so a log line is enough to tell what
|
|
68
|
-
# Active Job changed and what the wrapper was written against.
|
|
69
|
-
def self.instrument_enqueue_all_mismatch_message
|
|
70
|
-
"Not instrumenting Active Job bulk enqueues: " \
|
|
71
|
-
"`ActiveJob.instrument_enqueue_all` takes " \
|
|
72
|
-
"#{instrument_enqueue_all_parameters.inspect} in this version of " \
|
|
73
|
-
"Active Job, where AppSignal expects " \
|
|
74
|
-
"#{EXPECTED_INSTRUMENT_ENQUEUE_ALL_PARAMETERS.inspect}."
|
|
75
|
-
end
|
|
76
|
-
|
|
77
40
|
def dependencies_present?
|
|
78
41
|
self.class.dependencies_present? && Appsignal.config &&
|
|
79
42
|
Appsignal.config[:instrument_active_job]
|
|
@@ -83,34 +46,11 @@ module Appsignal
|
|
|
83
46
|
ActiveSupport.on_load(:active_job) do
|
|
84
47
|
::ActiveJob::Base
|
|
85
48
|
.extend ::Appsignal::Hooks::ActiveJobHook::ActiveJobClassInstrumentation
|
|
49
|
+
# Carry W3C trace context across the enqueue/perform boundary in
|
|
50
|
+
# collector mode (no-ops otherwise). The patches are cheap and
|
|
51
|
+
# mode-gated inside their method bodies, so install them unconditionally.
|
|
86
52
|
::ActiveJob::Base
|
|
87
|
-
.prepend ::Appsignal::Hooks::ActiveJobHook::
|
|
88
|
-
|
|
89
|
-
# Wrap the method Active Job records a bulk enqueue through, but only
|
|
90
|
-
# when it is still the method the wrapper knows how to read. When it
|
|
91
|
-
# is not, the batch goes unrecorded: Rails' own notification stays
|
|
92
|
-
# suppressed, because a worse event is not worth reporting in place
|
|
93
|
-
# of the one we set out to report.
|
|
94
|
-
if !Appsignal::Hooks::ActiveJobHook.instrument_enqueue_all_defined?
|
|
95
|
-
# Nothing to instrument on a version with no bulk enqueue path, so
|
|
96
|
-
# there is nothing to report either.
|
|
97
|
-
Appsignal.internal_logger.debug(
|
|
98
|
-
"Not instrumenting Active Job bulk enqueues: this version of " \
|
|
99
|
-
"Active Job does not record them through " \
|
|
100
|
-
"`ActiveJob.instrument_enqueue_all`."
|
|
101
|
-
)
|
|
102
|
-
elsif !Appsignal::Hooks::ActiveJobHook.instrument_enqueue_all_parameters_match?
|
|
103
|
-
# A bulk enqueue path exists, but not one that can be wrapped
|
|
104
|
-
# without breaking `ActiveJob.perform_all_later` for the whole
|
|
105
|
-
# application. Report that at a level someone will see, because a
|
|
106
|
-
# batch that used to be recorded no longer is.
|
|
107
|
-
Appsignal.internal_logger.warn(
|
|
108
|
-
Appsignal::Hooks::ActiveJobHook.instrument_enqueue_all_mismatch_message
|
|
109
|
-
)
|
|
110
|
-
else
|
|
111
|
-
::ActiveJob.singleton_class
|
|
112
|
-
.prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobBulkEnqueueInstrumentation
|
|
113
|
-
end
|
|
53
|
+
.prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobTraceContext
|
|
114
54
|
|
|
115
55
|
next unless Appsignal::Hooks::ActiveJobHook.version_7_1_or_higher?
|
|
116
56
|
|
|
@@ -123,103 +63,6 @@ module Appsignal
|
|
|
123
63
|
end
|
|
124
64
|
end
|
|
125
65
|
|
|
126
|
-
# Records an `enqueue.active_job` event when a job is enqueued, so the
|
|
127
|
-
# enqueue shows up on the active transaction's timeline (e.g. when
|
|
128
|
-
# enqueuing from within a web request or another job).
|
|
129
|
-
#
|
|
130
|
-
# Wrapping `enqueue` ourselves -- rather than relying on Rails' native
|
|
131
|
-
# `enqueue.active_job` notification, which the AppSignal notifications
|
|
132
|
-
# path now suppresses -- gives us a single event we own. Like all
|
|
133
|
-
# AppSignal events, this only records when there's an active transaction;
|
|
134
|
-
# an enqueue with no transaction is a transparent pass-through.
|
|
135
|
-
#
|
|
136
|
-
# @!visibility private
|
|
137
|
-
module ActiveJobEnqueueInstrumentation
|
|
138
|
-
def enqueue(*, **)
|
|
139
|
-
# Skip recording the event when enqueue events are suppressed. That is
|
|
140
|
-
# the case when enqueue instrumentation is disabled, and it keeps this
|
|
141
|
-
# integration consistent with the standalone adapters (Sidekiq, ...),
|
|
142
|
-
# which already gate their own enqueue event on this check.
|
|
143
|
-
if Appsignal::Transaction.current? &&
|
|
144
|
-
Appsignal::Transaction.current.job_enqueue_events_suppressed?
|
|
145
|
-
return super
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
Appsignal.instrument("enqueue.active_job", "enqueue #{self.class.name} job") do
|
|
149
|
-
# Active Job enqueues through an adapter (Sidekiq, Resque, ...) that
|
|
150
|
-
# has its own enqueue instrumentation. Suppress it so the enqueue is
|
|
151
|
-
# recorded once, as this event, rather than as nested Active Job +
|
|
152
|
-
# adapter events.
|
|
153
|
-
if Appsignal::Transaction.current?
|
|
154
|
-
Appsignal::Transaction.current.suppress_job_enqueue_events { super }
|
|
155
|
-
else
|
|
156
|
-
super
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Records an `enqueue_all.active_job` event when a batch of jobs is
|
|
163
|
-
# enqueued with `ActiveJob.perform_all_later`, so the batch shows up on the
|
|
164
|
-
# active transaction's timeline as one event.
|
|
165
|
-
#
|
|
166
|
-
# This wraps `instrument_enqueue_all` rather than `perform_all_later`, for
|
|
167
|
-
# two reasons. It is the method that records the batch, so it is called
|
|
168
|
-
# once for each queue adapter the batch spans, which is the same event
|
|
169
|
-
# count as the native notification it replaces. And it runs inside
|
|
170
|
-
# `perform_all_later`, after Active Job has split off the jobs it defers
|
|
171
|
-
# until the database transaction commits, so each of those halves is
|
|
172
|
-
# recorded when it is really enqueued.
|
|
173
|
-
#
|
|
174
|
-
# @!visibility private
|
|
175
|
-
module ActiveJobBulkEnqueueInstrumentation
|
|
176
|
-
private
|
|
177
|
-
|
|
178
|
-
def instrument_enqueue_all(_queue_adapter, jobs)
|
|
179
|
-
# Skip recording the event when enqueue events are suppressed, which is
|
|
180
|
-
# also the case when enqueue instrumentation is disabled. Same check as
|
|
181
|
-
# the single-job path above.
|
|
182
|
-
if Appsignal::Transaction.current? &&
|
|
183
|
-
Appsignal::Transaction.current.job_enqueue_events_suppressed?
|
|
184
|
-
return super
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
Appsignal.instrument("enqueue_all.active_job", bulk_enqueue_title(jobs)) do
|
|
188
|
-
# A bulk enqueue does not go through `ActiveJob::Base#enqueue`, so
|
|
189
|
-
# nothing has suppressed the adapter (Sidekiq, Resque, ...) yet, and
|
|
190
|
-
# its own enqueue instrumentation would record an event for every job
|
|
191
|
-
# in the batch. Suppress it so the batch is recorded once, as this
|
|
192
|
-
# event.
|
|
193
|
-
if Appsignal::Transaction.current?
|
|
194
|
-
Appsignal::Transaction.current.suppress_job_enqueue_events { super }
|
|
195
|
-
else
|
|
196
|
-
super
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# The batch's job class, when every job in it has the same one. Active
|
|
202
|
-
# Job groups the jobs it enqueues by queue adapter rather than by class,
|
|
203
|
-
# so a batch can mix classes, and then there is no one class to name.
|
|
204
|
-
def bulk_enqueue_title(jobs)
|
|
205
|
-
job_class = shared_across(jobs) { |job| job.class.name }
|
|
206
|
-
return "bulk enqueue jobs" unless job_class
|
|
207
|
-
|
|
208
|
-
"bulk enqueue #{job_class} jobs"
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# The one value every job in the batch shares, or nil when they differ
|
|
212
|
-
# or the batch is empty. Stops at the first job that disagrees, because
|
|
213
|
-
# a batch is as large as the caller made it and a single mismatch is
|
|
214
|
-
# enough to know.
|
|
215
|
-
def shared_across(jobs)
|
|
216
|
-
return if jobs.empty?
|
|
217
|
-
|
|
218
|
-
first = yield(jobs.first)
|
|
219
|
-
jobs.all? { |job| yield(job) == first } ? first : nil
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
66
|
module ActiveJobClassInstrumentation
|
|
224
67
|
def execute(job)
|
|
225
68
|
enqueued_at = job["enqueued_at"]
|
|
@@ -241,12 +84,39 @@ module Appsignal
|
|
|
241
84
|
# We don't have a separate integration for this QueueAdapter like
|
|
242
85
|
# we do for Sidekiq.
|
|
243
86
|
#
|
|
87
|
+
# Read the trace context off the job so the transaction links back
|
|
88
|
+
# to the enqueuer (no-op outside collector mode). Only here, in the
|
|
89
|
+
# standalone branch: when a wrapper integration (e.g. Sidekiq)
|
|
90
|
+
# created the transaction, it already extracted, so we must not
|
|
91
|
+
# extract a second time.
|
|
92
|
+
#
|
|
244
93
|
# Prefer job_id from provider, instead of ActiveJob's internal ID.
|
|
245
|
-
Appsignal::Transaction.create(
|
|
94
|
+
Appsignal::Transaction.create(
|
|
95
|
+
Appsignal::Transaction::BACKGROUND_JOB,
|
|
96
|
+
:opentelemetry_context => Appsignal::OpenTelemetry.extract_job_context(job),
|
|
97
|
+
:opentelemetry_scope => ["appsignal-ruby/active_job", Appsignal::VERSION],
|
|
98
|
+
:opentelemetry_kind => :consumer,
|
|
99
|
+
:opentelemetry_relationship => :both
|
|
100
|
+
)
|
|
246
101
|
end
|
|
247
102
|
|
|
103
|
+
unless has_wrapper_transaction
|
|
104
|
+
# Describes this span as a job being performed. The messaging
|
|
105
|
+
# system is what the trace timeline reads to recognize background
|
|
106
|
+
# job work, and `active_job` is the value OpenTelemetry's own Active
|
|
107
|
+
# Job instrumentation uses.
|
|
108
|
+
#
|
|
109
|
+
# Only set when this hook created the transaction. When an adapter
|
|
110
|
+
# integration created it, that adapter already named itself, and its
|
|
111
|
+
# answer is the more specific one.
|
|
112
|
+
transaction.add_opentelemetry_attributes(
|
|
113
|
+
Appsignal::OpenTelemetry::Messaging
|
|
114
|
+
.perform_attributes("active_job", :destination => job["queue_name"])
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
248
118
|
begin
|
|
249
|
-
transaction.
|
|
119
|
+
transaction.add_function_parameters_if_nil(job["arguments"])
|
|
250
120
|
|
|
251
121
|
transaction_tags = ActiveJobHelpers.transaction_tags_for(job)
|
|
252
122
|
transaction.add_tags(transaction_tags)
|
|
@@ -302,6 +172,85 @@ module Appsignal
|
|
|
302
172
|
end
|
|
303
173
|
end
|
|
304
174
|
|
|
175
|
+
# Reads and writes W3C trace context on the ActiveJob enqueue/perform
|
|
176
|
+
# boundary, wire-compatible with OpenTelemetry's ActiveJob instrumentation.
|
|
177
|
+
# All of this no-ops outside collector mode.
|
|
178
|
+
#
|
|
179
|
+
# Context rides on the job under `__otel_headers`, the same carrier OTel
|
|
180
|
+
# uses. Stock `serialize`/`deserialize` only carry a fixed key set, so --
|
|
181
|
+
# like OTel -- we patch both plus an accessor to round-trip it. The on-wire
|
|
182
|
+
# value is run through ActiveJob's argument serializer (an array of
|
|
183
|
+
# `[key, value]` pairs), matching OTel byte-for-byte so an AppSignal- and an
|
|
184
|
+
# OTel-instrumented service read each other's jobs.
|
|
185
|
+
module ActiveJobTraceContext
|
|
186
|
+
# Inject on enqueue from inside a producer event, so the job carries this
|
|
187
|
+
# transaction's context and the perform later links back. Mirrors the
|
|
188
|
+
# Sidekiq client middleware: an AppSignal event (a producer span in
|
|
189
|
+
# collector mode), not a direct SDK span. `Appsignal.instrument` is a
|
|
190
|
+
# transparent pass-through when there's no active transaction, and
|
|
191
|
+
# `inject_context` no-ops outside collector mode.
|
|
192
|
+
def enqueue(*, **)
|
|
193
|
+
# When enqueue instrumentation is disabled, drop the trace context
|
|
194
|
+
# along with the event. Without an enqueue event there is no producer
|
|
195
|
+
# span, so the context we would write is that of whatever span is
|
|
196
|
+
# current, such as the surrounding web request. The job that performs
|
|
197
|
+
# later would then link back to a span that is not a producer.
|
|
198
|
+
return super if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
|
|
199
|
+
|
|
200
|
+
# Another enqueue integration is already recording this enqueue, so
|
|
201
|
+
# don't record it a second time.
|
|
202
|
+
if Appsignal::Transaction.current? &&
|
|
203
|
+
Appsignal::Transaction.current.job_enqueue_events_suppressed?
|
|
204
|
+
return super
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
Appsignal.instrument(
|
|
208
|
+
"enqueue.active_job",
|
|
209
|
+
"enqueue #{self.class.name} job",
|
|
210
|
+
:opentelemetry_kind => :producer,
|
|
211
|
+
:opentelemetry_scope => ["appsignal-ruby/active_job", Appsignal::VERSION]
|
|
212
|
+
) do
|
|
213
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
214
|
+
Appsignal::OpenTelemetry::Messaging
|
|
215
|
+
.enqueue_attributes("active_job", :destination => queue_name)
|
|
216
|
+
)
|
|
217
|
+
Appsignal::OpenTelemetry.inject_context(__otel_headers)
|
|
218
|
+
# Active Job enqueues through an adapter (Sidekiq, Resque, ...) that
|
|
219
|
+
# has its own enqueue instrumentation. Suppress it so the enqueue is
|
|
220
|
+
# recorded once, as this event, rather than as nested Active Job +
|
|
221
|
+
# adapter events.
|
|
222
|
+
if Appsignal::Transaction.current?
|
|
223
|
+
Appsignal::Transaction.current.suppress_job_enqueue_events { super }
|
|
224
|
+
else
|
|
225
|
+
super
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def serialize
|
|
231
|
+
super.tap do |data|
|
|
232
|
+
Appsignal::OpenTelemetry.if_started do
|
|
233
|
+
next if __otel_headers.empty?
|
|
234
|
+
|
|
235
|
+
data["__otel_headers"] = ::ActiveJob::Arguments.serialize(__otel_headers)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def deserialize(job_data)
|
|
241
|
+
super
|
|
242
|
+
serialized = job_data["__otel_headers"]
|
|
243
|
+
@__otel_headers =
|
|
244
|
+
serialized ? ::ActiveJob::Arguments.deserialize(serialized).to_h : {}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def __otel_headers
|
|
248
|
+
@__otel_headers ||= {}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
attr_writer :__otel_headers
|
|
252
|
+
end
|
|
253
|
+
|
|
305
254
|
module ActiveJobHelpers
|
|
306
255
|
ACTION_MAILER_CLASSES = [
|
|
307
256
|
"ActionMailer::DeliveryJob",
|
|
@@ -38,7 +38,10 @@ module Appsignal
|
|
|
38
38
|
|
|
39
39
|
report_error = true
|
|
40
40
|
|
|
41
|
-
Appsignal.report_error(
|
|
41
|
+
Appsignal.report_error(
|
|
42
|
+
error,
|
|
43
|
+
:opentelemetry_scope => ["appsignal-ruby/at_exit", Appsignal::VERSION]
|
|
44
|
+
) do |transaction|
|
|
42
45
|
transaction.set_namespace("unhandled")
|
|
43
46
|
end
|
|
44
47
|
ensure
|
|
@@ -12,15 +12,35 @@ module Appsignal
|
|
|
12
12
|
|
|
13
13
|
def install
|
|
14
14
|
require "appsignal/integrations/excon"
|
|
15
|
+
require "appsignal/integrations/excon/appsignal_middleware"
|
|
15
16
|
# Instrument the request at the connection, rather than by registering
|
|
16
17
|
# AppSignal as Excon's instrumentor. An instrumentor is told about a
|
|
17
18
|
# request in pieces, none of which covers the wait for the response, and
|
|
18
19
|
# there is only room for one of them, so registering ours would replace
|
|
19
20
|
# any the application set up itself.
|
|
20
21
|
::Excon::Connection.prepend Appsignal::Integrations::ExconIntegration
|
|
22
|
+
install_middleware
|
|
21
23
|
|
|
22
24
|
Appsignal::Environment.report_enabled("excon")
|
|
23
25
|
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Trace context is written onto the outgoing request by a middleware,
|
|
30
|
+
# because that is where Excon exposes the request's headers.
|
|
31
|
+
#
|
|
32
|
+
# Insert it just before the Mock middleware, the innermost one, where the
|
|
33
|
+
# response is produced. That way it runs before the request is sent.
|
|
34
|
+
# Appending to the end would place it after Mock, which short-circuits the
|
|
35
|
+
# chain before reaching it.
|
|
36
|
+
def install_middleware
|
|
37
|
+
middlewares = ::Excon.defaults[:middlewares].dup
|
|
38
|
+
return if middlewares.include?(Appsignal::Integrations::ExconMiddleware)
|
|
39
|
+
|
|
40
|
+
index = middlewares.index(::Excon::Middleware::Mock) || middlewares.length
|
|
41
|
+
middlewares.insert(index, Appsignal::Integrations::ExconMiddleware)
|
|
42
|
+
::Excon.defaults[:middlewares] = middlewares
|
|
43
|
+
end
|
|
24
44
|
end
|
|
25
45
|
end
|
|
26
46
|
end
|
|
@@ -6,6 +6,22 @@ module Appsignal
|
|
|
6
6
|
class FaradayHook < Appsignal::Hooks::Hook
|
|
7
7
|
register :faraday
|
|
8
8
|
|
|
9
|
+
# This integration records the request itself, so Faraday's own
|
|
10
|
+
# instrumentation middleware would report the same work again as a
|
|
11
|
+
# `request.faraday` notification. Claim it so the generic notification
|
|
12
|
+
# paths leave it alone.
|
|
13
|
+
#
|
|
14
|
+
# Claimed here, when this file is required, rather than in `install`.
|
|
15
|
+
# `install` only runs when Faraday instrumentation is turned on, but a
|
|
16
|
+
# customer who turns it off does not want to see the native
|
|
17
|
+
# notification reported instead. This call does not touch any
|
|
18
|
+
# `Faraday` constant, so it is safe to run even when the library is not
|
|
19
|
+
# present.
|
|
20
|
+
Appsignal::EventFormatter.register(
|
|
21
|
+
"request.faraday",
|
|
22
|
+
Appsignal::EventFormatter::RecordedElsewhere
|
|
23
|
+
)
|
|
24
|
+
|
|
9
25
|
def dependencies_present?
|
|
10
26
|
defined?(::Faraday) && Appsignal.config && Appsignal.config[:instrument_faraday]
|
|
11
27
|
end
|
data/lib/appsignal/hooks/http.rb
CHANGED
|
@@ -32,6 +32,11 @@ module Appsignal
|
|
|
32
32
|
if defined?(HTTP::Session)
|
|
33
33
|
HTTP::Session.prepend Appsignal::Integrations::HttpIntegration::KeywordOptions
|
|
34
34
|
end
|
|
35
|
+
# Propagate trace context onto every outgoing hop (redirects included) at
|
|
36
|
+
# `Client#perform`, where the live request headers are reachable. Kept
|
|
37
|
+
# separate from the request-boundary event above: it only injects context
|
|
38
|
+
# and no-ops outside collector mode.
|
|
39
|
+
HTTP::Client.prepend Appsignal::Integrations::HttpIntegration::ContextInjection
|
|
35
40
|
|
|
36
41
|
Appsignal::Environment.report_enabled("http_rb")
|
|
37
42
|
end
|
|
@@ -15,7 +15,7 @@ module Appsignal
|
|
|
15
15
|
Resque::Job.prepend Appsignal::Integrations::ResqueIntegration
|
|
16
16
|
|
|
17
17
|
# Resque enqueues through the `Resque.push` singleton method, so prepend
|
|
18
|
-
# onto its singleton class to
|
|
18
|
+
# onto its singleton class to write the trace context onto outgoing jobs.
|
|
19
19
|
Resque.singleton_class.prepend Appsignal::Integrations::ResquePushIntegration
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -10,8 +10,13 @@ module Appsignal
|
|
|
10
10
|
"sql.sequel",
|
|
11
11
|
nil,
|
|
12
12
|
sql,
|
|
13
|
-
Appsignal::EventFormatter::SQL_BODY_FORMAT
|
|
13
|
+
Appsignal::EventFormatter::SQL_BODY_FORMAT,
|
|
14
|
+
:opentelemetry_kind => :client,
|
|
15
|
+
:opentelemetry_scope => ["appsignal-ruby/sequel", Appsignal::VERSION]
|
|
14
16
|
) do
|
|
17
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
18
|
+
Appsignal::Hooks::SequelHook.sequel_db_attributes(self)
|
|
19
|
+
)
|
|
15
20
|
super
|
|
16
21
|
end
|
|
17
22
|
end
|
|
@@ -25,8 +30,13 @@ module Appsignal
|
|
|
25
30
|
"sql.sequel",
|
|
26
31
|
nil,
|
|
27
32
|
sql,
|
|
28
|
-
Appsignal::EventFormatter::SQL_BODY_FORMAT
|
|
33
|
+
Appsignal::EventFormatter::SQL_BODY_FORMAT,
|
|
34
|
+
:opentelemetry_kind => :client,
|
|
35
|
+
:opentelemetry_scope => ["appsignal-ruby/sequel", Appsignal::VERSION]
|
|
29
36
|
) do
|
|
37
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
38
|
+
Appsignal::Hooks::SequelHook.sequel_db_attributes(self)
|
|
39
|
+
)
|
|
30
40
|
super
|
|
31
41
|
end
|
|
32
42
|
end
|
|
@@ -35,6 +45,26 @@ module Appsignal
|
|
|
35
45
|
class SequelHook < Appsignal::Hooks::Hook
|
|
36
46
|
register :sequel
|
|
37
47
|
|
|
48
|
+
# The query's `Sequel::Database` names both the engine it talks to and
|
|
49
|
+
# the database it is connected to, neither of which the sql.sequel
|
|
50
|
+
# formatter can see -- it only gets the query text. Shared by both
|
|
51
|
+
# extensions above, whichever one a given Sequel version registers.
|
|
52
|
+
#
|
|
53
|
+
# @!visibility private
|
|
54
|
+
def self.sequel_db_attributes(database)
|
|
55
|
+
attributes = {}
|
|
56
|
+
|
|
57
|
+
name = Appsignal::OpenTelemetry::SqlDbSystem.name_for_sequel(database.database_type)
|
|
58
|
+
attributes["db.system.name"] = name if name
|
|
59
|
+
|
|
60
|
+
# `opts[:database]` is Sequel's own option key for the database to
|
|
61
|
+
# connect to, so it doubles as the database's name.
|
|
62
|
+
namespace = database.opts[:database].to_s
|
|
63
|
+
attributes["db.namespace"] = namespace unless namespace.empty?
|
|
64
|
+
|
|
65
|
+
attributes
|
|
66
|
+
end
|
|
67
|
+
|
|
38
68
|
def dependencies_present?
|
|
39
69
|
defined?(::Sequel::Database) &&
|
|
40
70
|
Appsignal.config &&
|
|
@@ -19,9 +19,9 @@ module Appsignal
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Servers enqueue jobs too, so they need the client middleware that
|
|
22
|
-
#
|
|
23
|
-
# outside the server, so register it here as
|
|
24
|
-
# within a worker.
|
|
22
|
+
# writes the trace context onto outgoing messages. Shoryuken only
|
|
23
|
+
# yields `configure_client` outside the server, so register it here as
|
|
24
|
+
# well for enqueues from within a worker.
|
|
25
25
|
config.client_middleware do |chain|
|
|
26
26
|
chain.add Appsignal::Integrations::ShoryukenClientMiddleware
|
|
27
27
|
end
|
|
@@ -45,7 +45,7 @@ module Appsignal
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Servers enqueue jobs too, so they need the client middleware that
|
|
48
|
-
#
|
|
48
|
+
# writes the trace context onto outgoing jobs.
|
|
49
49
|
config.client_middleware do |chain|
|
|
50
50
|
chain.add Appsignal::Integrations::SidekiqClientMiddleware
|
|
51
51
|
end
|
|
@@ -11,7 +11,10 @@ module Appsignal
|
|
|
11
11
|
request_id = request.request_id || SecureRandom.uuid
|
|
12
12
|
env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||= request_id
|
|
13
13
|
|
|
14
|
-
transaction = Appsignal::Transaction.create(
|
|
14
|
+
transaction = Appsignal::Transaction.create(
|
|
15
|
+
Appsignal::Transaction::ACTION_CABLE,
|
|
16
|
+
:opentelemetry_scope => ["appsignal-ruby/action_cable", Appsignal::VERSION]
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
begin
|
|
17
20
|
super
|
|
@@ -20,7 +23,7 @@ module Appsignal
|
|
|
20
23
|
raise exception
|
|
21
24
|
ensure
|
|
22
25
|
transaction.set_action_if_nil("#{self.class}##{args.first["action"]}")
|
|
23
|
-
transaction.
|
|
26
|
+
transaction.add_request_payload_if_nil(args.first)
|
|
24
27
|
transaction.add_session_data { request.session.to_h if request.respond_to? :session }
|
|
25
28
|
transaction.set_metadata("path", request.path)
|
|
26
29
|
transaction.set_metadata("method", "websocket")
|
|
@@ -7,29 +7,52 @@ module Appsignal
|
|
|
7
7
|
class << self
|
|
8
8
|
BANG = "!"
|
|
9
9
|
|
|
10
|
-
# Events a dedicated AppSignal integration already records, so the
|
|
11
|
-
# generic notifications path must not record them a second time. The
|
|
12
|
-
# ActiveJob hook owns `enqueue.active_job` and `enqueue_all.active_job`.
|
|
13
|
-
# It records its own event for a single enqueue, and one event for a
|
|
14
|
-
# whole batch, with Rails' native notification nested inside. The
|
|
15
|
-
# Faraday integration owns `request.faraday`.
|
|
16
|
-
SUPPRESSED_EVENT_NAMES = [
|
|
17
|
-
"enqueue.active_job",
|
|
18
|
-
"enqueue_all.active_job",
|
|
19
|
-
"request.faraday"
|
|
20
|
-
].freeze
|
|
21
|
-
|
|
22
10
|
def start_event(name)
|
|
23
11
|
return unless record_event?(name)
|
|
24
12
|
|
|
25
|
-
|
|
13
|
+
# The event's formatter says what kind of work the event is, such as
|
|
14
|
+
# a SQL query being an outgoing call to a database, and can name the
|
|
15
|
+
# library the instrumentation is for. Both are immutable once the
|
|
16
|
+
# span exists, so they have to be set here at event start.
|
|
17
|
+
#
|
|
18
|
+
# A formatter that names no library leaves the scope to be derived
|
|
19
|
+
# from the event name, which is right for everything Rails reports.
|
|
20
|
+
Appsignal::Transaction.current.start_event(
|
|
21
|
+
:opentelemetry_kind => Appsignal::EventFormatter.opentelemetry_kind(name),
|
|
22
|
+
:opentelemetry_scope =>
|
|
23
|
+
Appsignal::EventFormatter.opentelemetry_scope(name) || scope_for(name)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ActiveSupport::Notifications bridges many Rails components through this
|
|
28
|
+
# one path (`sql.active_record`, `render_template.action_view`, ...), so
|
|
29
|
+
# derive the instrumentation scope from the event name's group: the part
|
|
30
|
+
# after the last dot. That gives each Rails component its own scope
|
|
31
|
+
# (`appsignal-ruby/active_record`, `appsignal-ruby/action_view`, ...)
|
|
32
|
+
# rather than lumping them under one. A name without a group falls back
|
|
33
|
+
# to the default scope in the backend.
|
|
34
|
+
def scope_for(name)
|
|
35
|
+
# Only names with a group (a dot) map to a component scope. A name
|
|
36
|
+
# without one has no component to attribute it to, so it falls back to
|
|
37
|
+
# the default scope in the backend (returning nil here).
|
|
38
|
+
parts = name.to_s.split(".")
|
|
39
|
+
return if parts.length < 2 || parts.last.empty?
|
|
40
|
+
|
|
41
|
+
["appsignal-ruby/#{parts.last}", Appsignal::VERSION]
|
|
26
42
|
end
|
|
27
43
|
|
|
28
44
|
def finish_event(name, payload = {})
|
|
29
45
|
return unless record_event?(name)
|
|
30
46
|
|
|
31
47
|
title, body, body_format = Appsignal::EventFormatter.format(name, payload)
|
|
32
|
-
Appsignal::Transaction.current
|
|
48
|
+
transaction = Appsignal::Transaction.current
|
|
49
|
+
# Set while the event's span is still open, so the attributes land on
|
|
50
|
+
# the event rather than on the transaction.
|
|
51
|
+
transaction.add_opentelemetry_attributes(
|
|
52
|
+
Appsignal::EventFormatter.opentelemetry_attributes(name, payload)
|
|
53
|
+
)
|
|
54
|
+
record_error_type(transaction, payload)
|
|
55
|
+
transaction.finish_event(
|
|
33
56
|
name.to_s,
|
|
34
57
|
title,
|
|
35
58
|
body,
|
|
@@ -37,12 +60,29 @@ module Appsignal
|
|
|
37
60
|
)
|
|
38
61
|
end
|
|
39
62
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
63
|
+
# Says what kind of failure ended the event, which the OpenTelemetry
|
|
64
|
+
# semantic conventions ask for on a span whose operation failed.
|
|
65
|
+
#
|
|
66
|
+
# ActiveSupport puts the exception in the payload when the instrumented
|
|
67
|
+
# block raised, and it does so before it hands control to any of the
|
|
68
|
+
# paths this integration hooks into. So the failure is readable here and
|
|
69
|
+
# there is nothing to rescue, whether the event was reported through a
|
|
70
|
+
# block or through a `start` and `finish` pair.
|
|
71
|
+
def record_error_type(transaction, payload)
|
|
72
|
+
error = payload[:exception_object]
|
|
73
|
+
return unless error
|
|
74
|
+
|
|
75
|
+
transaction.add_opentelemetry_attributes(
|
|
76
|
+
Appsignal::OpenTelemetry::ErrorType.attributes_for(error.class.name)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Events starting with a bang are internal to Rails. An event that the
|
|
81
|
+
# registry says a dedicated integration records is not recorded again
|
|
82
|
+
# here. Both `start_event` and `finish_event` gate on this so the event
|
|
83
|
+
# stack stays balanced.
|
|
43
84
|
def record_event?(name)
|
|
44
|
-
name
|
|
45
|
-
name[0] != BANG && !SUPPRESSED_EVENT_NAMES.include?(name)
|
|
85
|
+
name.to_s[0] != BANG && Appsignal::EventFormatter.record?(name)
|
|
46
86
|
end
|
|
47
87
|
end
|
|
48
88
|
|