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
|
@@ -12,22 +12,34 @@ module Appsignal
|
|
|
12
12
|
].freeze
|
|
13
13
|
|
|
14
14
|
def log(message)
|
|
15
|
+
attributes = {}
|
|
16
|
+
|
|
15
17
|
# If scheme is SQL-like, try to sanitize it, otherwise clear the body
|
|
16
18
|
if SQL_CLASSES.include?(self.class.to_s)
|
|
17
19
|
body_content = message.query
|
|
18
20
|
body_format = Appsignal::EventFormatter::SQL_BODY_FORMAT
|
|
21
|
+
# The connection class names the engine it talks to, one of the four
|
|
22
|
+
# SQL_CLASSES above; a class this map does not recognise leaves the
|
|
23
|
+
# SQL sentinel to apply, same as it would for a fifth SQL_CLASSES
|
|
24
|
+
# entry this map has not been taught about.
|
|
25
|
+
db_system = Appsignal::OpenTelemetry::SqlDbSystem.name_for_data_mapper(self.class.to_s)
|
|
26
|
+
attributes["db.system.name"] = db_system if db_system
|
|
19
27
|
else
|
|
20
28
|
body_content = ""
|
|
21
29
|
body_format = Appsignal::EventFormatter::DEFAULT
|
|
22
30
|
end
|
|
23
31
|
|
|
24
|
-
# Record event
|
|
32
|
+
# Record event. The query is an outgoing call to the database, so tag it
|
|
33
|
+
# as a client span (collector mode); no-op in agent mode.
|
|
25
34
|
Appsignal::Transaction.current.record_event(
|
|
26
35
|
"query.data_mapper",
|
|
27
36
|
"DataMapper Query",
|
|
28
37
|
body_content,
|
|
29
38
|
message.duration,
|
|
30
|
-
body_format
|
|
39
|
+
body_format,
|
|
40
|
+
:opentelemetry_kind => :client,
|
|
41
|
+
:opentelemetry_scope => ["appsignal-ruby/data_mapper", Appsignal::VERSION],
|
|
42
|
+
:opentelemetry_attributes => attributes
|
|
31
43
|
)
|
|
32
44
|
super
|
|
33
45
|
end
|
|
@@ -11,18 +11,6 @@ module Appsignal
|
|
|
11
11
|
enqueue_with_instrumentation(job, block)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# Delayed Job asks a job for its own maximum run time before it invokes
|
|
15
|
-
# it, and answering that question reads the job's payload. A job whose
|
|
16
|
-
# payload will not deserialize therefore raises inside the worker and
|
|
17
|
-
# never reaches `invoke_job`, so the callback below does not run and the
|
|
18
|
-
# failure goes unreported. This callback sits one step further out, on
|
|
19
|
-
# the worker's whole run of the job, which is where such a job can still
|
|
20
|
-
# be seen.
|
|
21
|
-
lifecycle.around(:perform) do |worker, job, &block|
|
|
22
|
-
report_job_that_cannot_be_loaded(job)
|
|
23
|
-
block.call(worker, job)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
14
|
lifecycle.around(:invoke_job) do |job, &block|
|
|
27
15
|
invoke_with_instrumentation(job, block)
|
|
28
16
|
end
|
|
@@ -37,6 +25,11 @@ module Appsignal
|
|
|
37
25
|
# another job). An enqueue with no active transaction is a transparent
|
|
38
26
|
# pass-through.
|
|
39
27
|
def self.enqueue_with_instrumentation(job, block)
|
|
28
|
+
# Skip the enqueue event when enqueue instrumentation is disabled.
|
|
29
|
+
if Appsignal.config && !Appsignal.config[:enable_job_enqueue_instrumentation]
|
|
30
|
+
return block.call(job)
|
|
31
|
+
end
|
|
32
|
+
|
|
40
33
|
# Under Active Job the enqueue is already recorded as an
|
|
41
34
|
# `enqueue.active_job` event, so skip recording it again here.
|
|
42
35
|
if Appsignal::Transaction.current? &&
|
|
@@ -44,7 +37,20 @@ module Appsignal
|
|
|
44
37
|
return block.call(job)
|
|
45
38
|
end
|
|
46
39
|
|
|
47
|
-
Appsignal.instrument(
|
|
40
|
+
Appsignal.instrument(
|
|
41
|
+
"enqueue.delayed_job",
|
|
42
|
+
"enqueue #{enqueue_name(job)} job",
|
|
43
|
+
:opentelemetry_kind => :producer,
|
|
44
|
+
:opentelemetry_scope => ["appsignal-ruby/delayed_job", Appsignal::VERSION]
|
|
45
|
+
) do
|
|
46
|
+
# Describes this span as a job being enqueued. The messaging system
|
|
47
|
+
# is what the trace timeline reads to recognize background job work,
|
|
48
|
+
# and `delayed_job` is the value OpenTelemetry's own Delayed Job
|
|
49
|
+
# instrumentation uses.
|
|
50
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
51
|
+
Appsignal::OpenTelemetry::Messaging
|
|
52
|
+
.enqueue_attributes("delayed_job", :destination => queue_name(job))
|
|
53
|
+
)
|
|
48
54
|
block.call(job)
|
|
49
55
|
end
|
|
50
56
|
end
|
|
@@ -55,6 +61,12 @@ module Appsignal
|
|
|
55
61
|
# `enqueue Class#method job` rather than the bare `enqueue Class job`. We
|
|
56
62
|
# accept that inconsistency so the enqueue and perform events stay tied to
|
|
57
63
|
# the same name for the rare job that sets it.
|
|
64
|
+
# The queue a job is on. Not every Delayed Job backend has queues, so a
|
|
65
|
+
# job that does not know its queue is described without one.
|
|
66
|
+
def self.queue_name(job)
|
|
67
|
+
job.queue if job.respond_to?(:queue)
|
|
68
|
+
end
|
|
69
|
+
|
|
58
70
|
def self.enqueue_name(job)
|
|
59
71
|
payload = job.payload_object
|
|
60
72
|
appsignal_name = extract_value(payload, :appsignal_name, nil)
|
|
@@ -63,146 +75,67 @@ module Appsignal
|
|
|
63
75
|
job.name
|
|
64
76
|
end
|
|
65
77
|
|
|
66
|
-
# Reports a job whose payload cannot be read, and does nothing at all for
|
|
67
|
-
# a job that loads. That is what keeps every ordinary job instrumented by
|
|
68
|
-
# the `:invoke_job` callback alone: no transaction is opened here, so an
|
|
69
|
-
# ordinary job's reported duration and its reported error are exactly
|
|
70
|
-
# what they were before this callback existed.
|
|
71
|
-
#
|
|
72
|
-
# Reading the payload here reads it a moment before the worker would have
|
|
73
|
-
# read it anyway. Delayed Job memoizes a payload that loads, so a job
|
|
74
|
-
# that is fine is deserialized once either way.
|
|
75
|
-
#
|
|
76
|
-
# Nothing is re-raised. The worker reads the same payload immediately
|
|
77
|
-
# after this returns, raises the same error, and fails the job as it
|
|
78
|
-
# always has. Raising here instead would move that error out of the
|
|
79
|
-
# worker's own handling and into the loop that reserves jobs.
|
|
80
|
-
def self.report_job_that_cannot_be_loaded(job)
|
|
81
|
-
job.payload_object
|
|
82
|
-
nil
|
|
83
|
-
rescue Exception => error
|
|
84
|
-
warn_unreadable_payload_once(error)
|
|
85
|
-
|
|
86
|
-
transaction =
|
|
87
|
-
Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
|
|
88
|
-
transaction.set_action_if_nil(action_name_without_payload(job))
|
|
89
|
-
transaction.set_error(error)
|
|
90
|
-
add_job_metadata(transaction, job)
|
|
91
|
-
Appsignal::Transaction.complete_current!
|
|
92
|
-
end
|
|
93
|
-
|
|
94
78
|
def self.invoke_with_instrumentation(job, block)
|
|
95
79
|
transaction =
|
|
96
|
-
Appsignal::Transaction.create(
|
|
80
|
+
Appsignal::Transaction.create(
|
|
81
|
+
Appsignal::Transaction::BACKGROUND_JOB,
|
|
82
|
+
:opentelemetry_scope => ["appsignal-ruby/delayed_job", Appsignal::VERSION],
|
|
83
|
+
:opentelemetry_kind => :consumer,
|
|
84
|
+
:opentelemetry_relationship => :both
|
|
85
|
+
)
|
|
86
|
+
transaction.add_opentelemetry_attributes(
|
|
87
|
+
Appsignal::OpenTelemetry::Messaging
|
|
88
|
+
.perform_attributes("delayed_job", :destination => queue_name(job))
|
|
89
|
+
)
|
|
97
90
|
|
|
98
91
|
begin
|
|
99
|
-
Appsignal.instrument(
|
|
92
|
+
Appsignal.instrument(
|
|
93
|
+
"perform_job.delayed_job",
|
|
94
|
+
:opentelemetry_scope => ["appsignal-ruby/delayed_job", Appsignal::VERSION]
|
|
95
|
+
) do
|
|
96
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
97
|
+
Appsignal::OpenTelemetry::Messaging
|
|
98
|
+
.perform_attributes("delayed_job", :destination => queue_name(job))
|
|
99
|
+
)
|
|
100
100
|
block.call(job)
|
|
101
101
|
end
|
|
102
102
|
rescue Exception => error
|
|
103
103
|
transaction.set_error(error)
|
|
104
104
|
raise
|
|
105
105
|
ensure
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
transaction.set_action_if_nil("#{job_data["job_class"]}#perform")
|
|
117
|
-
transaction.add_params_if_nil(job_data.fetch("arguments", {}))
|
|
118
|
-
else
|
|
119
|
-
# Delayed Job
|
|
120
|
-
transaction.set_action_if_nil(action_name_from_payload(payload, job.name))
|
|
121
|
-
transaction.add_params_if_nil(extract_value(payload, :args, {}))
|
|
122
|
-
end
|
|
123
|
-
rescue => error
|
|
124
|
-
warn_unreadable_payload_once(error)
|
|
125
|
-
transaction.set_action_if_nil(action_name_without_payload(job))
|
|
106
|
+
payload = job.payload_object
|
|
107
|
+
if payload.respond_to? :job_data
|
|
108
|
+
# ActiveJob
|
|
109
|
+
job_data = payload.job_data
|
|
110
|
+
transaction.set_action_if_nil("#{job_data["job_class"]}#perform")
|
|
111
|
+
transaction.add_function_parameters_if_nil(job_data.fetch("arguments", {}))
|
|
112
|
+
else
|
|
113
|
+
# Delayed Job
|
|
114
|
+
transaction.set_action_if_nil(action_name_from_payload(payload, job.name))
|
|
115
|
+
transaction.add_function_parameters_if_nil(extract_value(payload, :args, {}))
|
|
126
116
|
end
|
|
127
117
|
|
|
128
|
-
|
|
118
|
+
transaction.add_tags(
|
|
119
|
+
:id => extract_value(job, :id, nil, true),
|
|
120
|
+
:queue => extract_value(job, :queue),
|
|
121
|
+
:priority => extract_value(job, :priority, 0),
|
|
122
|
+
:attempts => extract_value(job, :attempts, 0)
|
|
123
|
+
)
|
|
129
124
|
|
|
130
|
-
|
|
131
|
-
end
|
|
132
|
-
end
|
|
125
|
+
transaction.set_queue_start(extract_value(job, :run_at)&.to_i&.* 1_000)
|
|
133
126
|
|
|
134
|
-
|
|
135
|
-
transaction.add_tags(
|
|
136
|
-
:id => extract_value(job, :id, nil, true),
|
|
137
|
-
:queue => extract_value(job, :queue),
|
|
138
|
-
:priority => extract_value(job, :priority, 0),
|
|
139
|
-
:attempts => extract_value(job, :attempts, 0)
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
transaction.set_queue_start(extract_value(job, :run_at)&.to_i&.* 1_000)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# The name Delayed Job derives from the raw handler when the payload will
|
|
146
|
-
# not deserialize. It reads the class name out of the handler with a
|
|
147
|
-
# regular expression, which covers the job class a deploy removed. That
|
|
148
|
-
# expression raises for a handler it does not match, and it is skipped
|
|
149
|
-
# altogether when the payload raised something Delayed Job does not wrap,
|
|
150
|
-
# so fall back to a name that says the failure came from inside Delayed
|
|
151
|
-
# Job. The failure is then reported under a name that can be found, and
|
|
152
|
-
# every job this happens to is grouped together.
|
|
153
|
-
def self.action_name_without_payload(job)
|
|
154
|
-
with_perform_suffix(job.name)
|
|
155
|
-
rescue
|
|
156
|
-
"DelayedJobInternal"
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Guards the check-and-set below, so two threads that read an unreadable
|
|
160
|
-
# payload at the same time cannot both warn. A constant so it is created
|
|
161
|
-
# once at load time, because creating it lazily would race in turn.
|
|
162
|
-
WARN_ONCE_LOCK = Mutex.new
|
|
163
|
-
|
|
164
|
-
# A deploy that removes a job class leaves every job of that class unable
|
|
165
|
-
# to deserialize, so this can be reached once per job. Each of those jobs
|
|
166
|
-
# reports its own error to AppSignal, so the log only has to say once that
|
|
167
|
-
# it is happening.
|
|
168
|
-
def self.warn_unreadable_payload_once(error)
|
|
169
|
-
should_warn = WARN_ONCE_LOCK.synchronize do
|
|
170
|
-
next false if @warned_unreadable_payload
|
|
171
|
-
|
|
172
|
-
@warned_unreadable_payload = true
|
|
127
|
+
Appsignal::Transaction.complete_current!
|
|
173
128
|
end
|
|
174
|
-
return unless should_warn
|
|
175
|
-
|
|
176
|
-
Appsignal.internal_logger.warn(
|
|
177
|
-
"Unable to read a Delayed Job job's payload: #{error.class}: " \
|
|
178
|
-
"#{error.message}. Jobs whose payload cannot be read are reported " \
|
|
179
|
-
"without parameters. They are named after the class in their raw " \
|
|
180
|
-
"handler, or DelayedJobInternal when that cannot be read either."
|
|
181
|
-
)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# @!visibility private
|
|
185
|
-
#
|
|
186
|
-
# Resets the warn-once state. Only used to keep test runs isolated.
|
|
187
|
-
def self.reset_unreadable_payload_warning!
|
|
188
|
-
WARN_ONCE_LOCK.synchronize { @warned_unreadable_payload = false }
|
|
189
129
|
end
|
|
190
130
|
|
|
191
131
|
def self.action_name_from_payload(payload, default_name)
|
|
192
132
|
# Attempt to find appsignal_name override
|
|
193
133
|
class_and_method_name = extract_value(payload, :appsignal_name, nil)
|
|
194
134
|
return class_and_method_name if class_and_method_name.is_a?(String)
|
|
135
|
+
return default_name if default_name.split("#").length == 2
|
|
136
|
+
return default_name if default_name.split(".").length == 2
|
|
195
137
|
|
|
196
|
-
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# An action name is a class and a method. A name that already names both,
|
|
200
|
-
# separated either way, is left alone.
|
|
201
|
-
def self.with_perform_suffix(name)
|
|
202
|
-
return name if name.split("#").length == 2
|
|
203
|
-
return name if name.split(".").length == 2
|
|
204
|
-
|
|
205
|
-
"#{name}#perform"
|
|
138
|
+
"#{default_name}#perform"
|
|
206
139
|
end
|
|
207
140
|
|
|
208
141
|
# rubocop:disable Style/OptionalBooleanParameter
|
|
@@ -4,24 +4,48 @@ module Appsignal
|
|
|
4
4
|
module Integrations
|
|
5
5
|
# @!visibility private
|
|
6
6
|
module DryMonitorIntegration
|
|
7
|
+
# The event's formatter says what kind of work the event is, such as ROM
|
|
8
|
+
# reporting a SQL query as a dry-monitor `"sql"` event, and which library
|
|
9
|
+
# the instrumentation is for. Both are immutable once the span exists, so
|
|
10
|
+
# they have to be set here at event start.
|
|
11
|
+
#
|
|
12
|
+
# dry-monitor is a notification bus, so an event arriving over it is not
|
|
13
|
+
# necessarily dry-monitor's own work. A formatter that knows better says
|
|
14
|
+
# so; anything else is attributed to dry-monitor.
|
|
7
15
|
def instrument(event_id, payload = {}, &block)
|
|
8
|
-
Appsignal::Transaction.current.start_event
|
|
9
|
-
|
|
10
|
-
super
|
|
11
|
-
ensure
|
|
12
16
|
name = "#{event_id}.dry"
|
|
13
|
-
|
|
17
|
+
# An event a dedicated integration already records is not recorded a
|
|
18
|
+
# second time here.
|
|
19
|
+
return super unless Appsignal::EventFormatter.record?(name)
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
Appsignal::Transaction.current.start_event(
|
|
23
|
+
:opentelemetry_kind => Appsignal::EventFormatter.opentelemetry_kind(name),
|
|
24
|
+
:opentelemetry_scope =>
|
|
25
|
+
Appsignal::EventFormatter.opentelemetry_scope(name) ||
|
|
26
|
+
["appsignal-ruby/dry_monitor", Appsignal::VERSION]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
super
|
|
30
|
+
ensure
|
|
31
|
+
event_name, body, body_format = Appsignal::EventFormatter.format(name, payload)
|
|
32
|
+
# Set while the event's span is still open, so the attributes land
|
|
33
|
+
# on the event rather than on the transaction.
|
|
34
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
35
|
+
Appsignal::EventFormatter.opentelemetry_attributes(name, payload)
|
|
36
|
+
)
|
|
14
37
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
# dry-monitor reports an event under an id, such as `sql`, rather than
|
|
39
|
+
# a name. A formatter names the event it knows about, and an event
|
|
40
|
+
# without one is named after its id in the dry-monitor group. Either
|
|
41
|
+
# way the name has a group, which is what an event is listed under.
|
|
42
|
+
Appsignal::Transaction.current.finish_event(
|
|
43
|
+
event_name || name,
|
|
44
|
+
nil,
|
|
45
|
+
body,
|
|
46
|
+
body_format
|
|
47
|
+
)
|
|
48
|
+
end
|
|
25
49
|
end
|
|
26
50
|
end
|
|
27
51
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module Integrations
|
|
5
|
+
# Excon middleware that writes trace context onto the outgoing request, so
|
|
6
|
+
# the called service joins this trace. The integration on the connection
|
|
7
|
+
# records the span; this middleware only injects.
|
|
8
|
+
#
|
|
9
|
+
# @!visibility private
|
|
10
|
+
class ExconMiddleware < ::Excon::Middleware::Base
|
|
11
|
+
def request_call(datum)
|
|
12
|
+
datum[:headers] ||= {}
|
|
13
|
+
# Inject from whatever span is current. The connection's client span is
|
|
14
|
+
# open around the whole request, so the written `traceparent` reflects
|
|
15
|
+
# the Excon client event. No-op outside collector mode.
|
|
16
|
+
Appsignal::OpenTelemetry.inject_context(datum[:headers])
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -4,16 +4,20 @@ module Appsignal
|
|
|
4
4
|
module Integrations
|
|
5
5
|
# @!visibility private
|
|
6
6
|
module ExconIntegration
|
|
7
|
+
# The method of a request. Excon defaults it to GET further down, so the
|
|
8
|
+
# same default is applied here.
|
|
9
|
+
def self.method_for(datum)
|
|
10
|
+
datum[:method] || :get
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
# The title of the event, built the way the Net::HTTP integration builds
|
|
8
14
|
# its own: the request method and where the request went, without the
|
|
9
15
|
# path, so paths stay out of event titles.
|
|
10
16
|
#
|
|
11
17
|
# Excon splits a request's data between the connection it is made on and
|
|
12
|
-
# the call that makes it, so both are read to build this.
|
|
13
|
-
# the method to GET itself, so the same default is applied here.
|
|
18
|
+
# the call that makes it, so both are read to build this.
|
|
14
19
|
def self.title_for(datum)
|
|
15
|
-
|
|
16
|
-
"#{method} #{datum[:scheme]}://#{datum[:host]}"
|
|
20
|
+
"#{method_for(datum).to_s.upcase} #{datum[:scheme]}://#{datum[:host]}"
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def request(params = {})
|
|
@@ -38,18 +42,51 @@ module Appsignal
|
|
|
38
42
|
# A pipelined request is the exception to this method being the whole of
|
|
39
43
|
# a request. It returns before the response is read, so its event covers
|
|
40
44
|
# only the sending.
|
|
41
|
-
|
|
45
|
+
datum = data.merge(params)
|
|
46
|
+
|
|
47
|
+
Appsignal.instrument(
|
|
48
|
+
"request.excon",
|
|
49
|
+
ExconIntegration.title_for(datum),
|
|
50
|
+
:opentelemetry_kind => :client,
|
|
51
|
+
:opentelemetry_scope => ["appsignal-ruby/excon", Appsignal::VERSION]
|
|
52
|
+
) do
|
|
53
|
+
# Describes the span as an outgoing HTTP request. Together with the
|
|
54
|
+
# CLIENT kind, this is what the trace timeline reads to recognize it
|
|
55
|
+
# as one.
|
|
56
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
57
|
+
Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
|
|
58
|
+
:method => ExconIntegration.method_for(datum),
|
|
59
|
+
:scheme => datum[:scheme],
|
|
60
|
+
:host => datum[:host],
|
|
61
|
+
:port => datum[:port],
|
|
62
|
+
:path => datum[:path]
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
response =
|
|
67
|
+
if Appsignal::Transaction.current?
|
|
68
|
+
# Excon retries a request, and follows a redirect, by calling this
|
|
69
|
+
# method again from inside the request it is retrying or
|
|
70
|
+
# following. Suppressing those means they count towards this event
|
|
71
|
+
# rather than becoming events of their own, so one request stays
|
|
72
|
+
# one event.
|
|
73
|
+
Appsignal::Transaction.current.suppress_http_client_events { super }
|
|
74
|
+
else
|
|
75
|
+
super
|
|
76
|
+
end
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
78
|
+
# Describes the response on the same span as the request, which the
|
|
79
|
+
# semantic conventions ask for whenever one was received.
|
|
80
|
+
#
|
|
81
|
+
# A pipelined request returns the request data rather than a response,
|
|
82
|
+
# because its response has not been read yet, so there is no status to
|
|
83
|
+
# report for it.
|
|
84
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
85
|
+
Appsignal::OpenTelemetry::HttpResponse.attributes_for(
|
|
86
|
+
response.respond_to?(:status) ? response.status : nil
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
response
|
|
53
90
|
end
|
|
54
91
|
end
|
|
55
92
|
end
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Appsignal
|
|
4
4
|
module Integrations
|
|
5
|
-
# Faraday middleware that records each request as a `request.faraday`
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# Faraday middleware that records each request as a `request.faraday` client
|
|
6
|
+
# event, writes trace context onto the outgoing request so the called service
|
|
7
|
+
# joins this trace, and suppresses the downstream HTTP client's own
|
|
8
|
+
# instrumentation, so the request is recorded once rather than as nested
|
|
9
|
+
# Faraday + Net::HTTP (or Excon) client events.
|
|
9
10
|
#
|
|
10
11
|
# @!visibility private
|
|
11
12
|
class FaradayMiddleware < ::Faraday::Middleware
|
|
@@ -16,17 +17,50 @@ module Appsignal
|
|
|
16
17
|
# Net::HTTP's (scheme and host only), keeping paths out of event titles.
|
|
17
18
|
Appsignal.instrument(
|
|
18
19
|
"request.faraday",
|
|
19
|
-
"#{http_method} #{uri.scheme}://#{uri.host}"
|
|
20
|
+
"#{http_method} #{uri.scheme}://#{uri.host}",
|
|
21
|
+
:opentelemetry_kind => :client,
|
|
22
|
+
:opentelemetry_scope => ["appsignal-ruby/faraday", Appsignal::VERSION]
|
|
20
23
|
) do
|
|
24
|
+
# Describes the span as an outgoing HTTP request. Together with the
|
|
25
|
+
# CLIENT kind, this is what the trace timeline reads to recognize it
|
|
26
|
+
# as one.
|
|
27
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
28
|
+
Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
|
|
29
|
+
:method => env[:method],
|
|
30
|
+
:scheme => uri.scheme,
|
|
31
|
+
:host => uri.host,
|
|
32
|
+
:port => uri.port,
|
|
33
|
+
:path => uri.path
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
# Write trace context onto the outgoing request so the called service
|
|
37
|
+
# joins this trace. Injected inside the instrument block, so the written
|
|
38
|
+
# `traceparent` reflects the Faraday client event's span. No-op outside
|
|
39
|
+
# collector mode. `env.request_headers` is the live outgoing header set
|
|
40
|
+
# and a valid carrier (it responds to `[]=`).
|
|
41
|
+
Appsignal::OpenTelemetry.inject_context(env.request_headers)
|
|
42
|
+
|
|
21
43
|
# Faraday's default adapter is Net::HTTP, which AppSignal also
|
|
22
44
|
# instruments. Suppress the adapter's own instrumentation so the
|
|
23
45
|
# request appears once (as the Faraday event) rather than as nested
|
|
24
46
|
# Faraday + Net::HTTP client events.
|
|
25
|
-
|
|
26
|
-
Appsignal::Transaction.current
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
response =
|
|
48
|
+
if Appsignal::Transaction.current?
|
|
49
|
+
Appsignal::Transaction.current.suppress_http_client_events { @app.call(env) }
|
|
50
|
+
else
|
|
51
|
+
@app.call(env)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Describes the response on the same span, which the semantic
|
|
55
|
+
# conventions ask for whenever one was received. The event is still
|
|
56
|
+
# open here, so it lands on the request's own span. The status is read
|
|
57
|
+
# off the environment rather than the returned response, because an
|
|
58
|
+
# adapter fills the response in later than it fills in the environment.
|
|
59
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
60
|
+
Appsignal::OpenTelemetry::HttpResponse.attributes_for(env[:status])
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
response
|
|
30
64
|
end
|
|
31
65
|
end
|
|
32
66
|
end
|
|
@@ -37,8 +71,9 @@ module Appsignal
|
|
|
37
71
|
# the build path is the only way to instrument every connection automatically.
|
|
38
72
|
#
|
|
39
73
|
# Just before the adapter (the innermost handler, where the request is sent)
|
|
40
|
-
# it inserts `FaradayMiddleware`, which records the `request.faraday` event
|
|
41
|
-
# and suppresses the downstream client. Skipped if it's
|
|
74
|
+
# it inserts `FaradayMiddleware`, which records the `request.faraday` event,
|
|
75
|
+
# injects trace context, and suppresses the downstream client. Skipped if it's
|
|
76
|
+
# already present.
|
|
42
77
|
#
|
|
43
78
|
# @!visibility private
|
|
44
79
|
module FaradayRackBuilderPatch
|
|
@@ -9,7 +9,34 @@ module Appsignal
|
|
|
9
9
|
parsed_request_uri = uri.is_a?(URI) ? uri : uri_module.parse(uri.to_s)
|
|
10
10
|
request_uri = "#{parsed_request_uri.scheme}://#{parsed_request_uri.host}"
|
|
11
11
|
|
|
12
|
-
Appsignal.instrument(
|
|
12
|
+
Appsignal.instrument(
|
|
13
|
+
"request.http_rb",
|
|
14
|
+
"#{verb.to_s.upcase} #{request_uri}",
|
|
15
|
+
:opentelemetry_kind => :client,
|
|
16
|
+
:opentelemetry_scope => ["appsignal-ruby/http_rb", Appsignal::VERSION]
|
|
17
|
+
) do
|
|
18
|
+
# Describes the span as an outgoing HTTP request. Together with the
|
|
19
|
+
# CLIENT kind, this is what the trace timeline reads to recognize it
|
|
20
|
+
# as one.
|
|
21
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
22
|
+
Appsignal::OpenTelemetry::HttpClientRequest.attributes_for(
|
|
23
|
+
:method => verb,
|
|
24
|
+
:scheme => parsed_request_uri.scheme,
|
|
25
|
+
:host => parsed_request_uri.host,
|
|
26
|
+
:port => parsed_request_uri.port,
|
|
27
|
+
:path => parsed_request_uri.path
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
# Describes the response on the same span, which the semantic
|
|
31
|
+
# conventions ask for whenever one was received. The event is still
|
|
32
|
+
# open here, so it lands on the request's own span. A request that
|
|
33
|
+
# followed redirects reports the status of the response it ended on.
|
|
34
|
+
block.call.tap do |response|
|
|
35
|
+
Appsignal::Transaction.current.add_opentelemetry_attributes(
|
|
36
|
+
Appsignal::OpenTelemetry::HttpResponse.attributes_for(response&.code)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
13
40
|
end
|
|
14
41
|
|
|
15
42
|
# The event is recorded at the request boundary, so a redirected request
|
|
@@ -32,6 +59,21 @@ module Appsignal
|
|
|
32
59
|
HttpIntegration.instrument(verb, uri) { super }
|
|
33
60
|
end
|
|
34
61
|
end
|
|
62
|
+
|
|
63
|
+
# Trace context has to ride on each outgoing hop's headers, so it's
|
|
64
|
+
# injected at `HTTP::Client#perform` -- the single send chokepoint in both
|
|
65
|
+
# http5 and http6, called once per request and once per redirect hop --
|
|
66
|
+
# where the live request headers are reachable. The event stays at the
|
|
67
|
+
# request boundary above, so a redirected request is still a single event;
|
|
68
|
+
# this only propagates context, and every hop carries it. No-op outside
|
|
69
|
+
# collector mode. `req.headers` is the live outgoing header set and a valid
|
|
70
|
+
# carrier (it responds to `[]=`).
|
|
71
|
+
module ContextInjection
|
|
72
|
+
def perform(req, options)
|
|
73
|
+
Appsignal::OpenTelemetry.inject_context(req.headers)
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
35
77
|
end
|
|
36
78
|
end
|
|
37
79
|
end
|