appsignal 5.0.0.rc.1-java → 5.0.0.rc.2-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 +140 -0
- data/README.md +2 -1
- data/Rakefile +110 -6
- data/appsignal.gemspec +7 -0
- data/build_matrix.yml +7 -2
- data/ext/agent.rb +27 -27
- data/lib/appsignal/cli/demo.rb +5 -0
- data/lib/appsignal/cli/diagnose.rb +8 -48
- data/lib/appsignal/cli/helpers.rb +45 -0
- data/lib/appsignal/config.rb +262 -37
- data/lib/appsignal/demo.rb +11 -9
- data/lib/appsignal/helpers/instrumentation.rb +88 -0
- data/lib/appsignal/hooks/action_cable.rb +8 -2
- data/lib/appsignal/hooks/active_job.rb +189 -1
- data/lib/appsignal/integrations/delayed_job_plugin.rb +172 -32
- data/lib/appsignal/integrations/que.rb +41 -9
- data/lib/appsignal/integrations/railtie.rb +4 -2
- data/lib/appsignal/integrations/resque.rb +26 -3
- data/lib/appsignal/integrations/shoryuken.rb +21 -2
- data/lib/appsignal/integrations/sidekiq.rb +23 -2
- data/lib/appsignal/integrations/webmachine.rb +13 -5
- data/lib/appsignal/opentelemetry/http_server_request.rb +35 -1
- data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
- data/lib/appsignal/opentelemetry.rb +182 -24
- data/lib/appsignal/rack/abstract_middleware.rb +4 -1
- data/lib/appsignal/rack/event_handler.rb +9 -2
- data/lib/appsignal/rack/grape_middleware.rb +37 -7
- data/lib/appsignal/rack.rb +29 -1
- data/lib/appsignal/transaction/base_backend.rb +21 -0
- data/lib/appsignal/transaction/extension_backend.rb +26 -0
- data/lib/appsignal/transaction/opentelemetry_backend.rb +90 -39
- data/lib/appsignal/transaction.rb +189 -32
- data/lib/appsignal/utils/request_headers.rb +78 -0
- data/lib/appsignal/utils.rb +1 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +1 -0
- data/sig/appsignal.rbi +205 -1
- data/sig/appsignal.rbs +196 -0
- metadata +3 -1
|
@@ -13,8 +13,10 @@ module Appsignal
|
|
|
13
13
|
config.appsignal = ActiveSupport::OrderedOptions.new
|
|
14
14
|
config.appsignal.start_at = :on_load
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
16
|
+
# Runs before `load_config_initializers` so that AppSignal has started
|
|
17
|
+
# when the app's initializers run, and errors they raise are reported.
|
|
18
|
+
initializer "appsignal.configure_rails_initialization",
|
|
19
|
+
:before => :load_config_initializers do |app|
|
|
18
20
|
Appsignal::Integrations::Railtie.on_load(app)
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -7,12 +7,14 @@ module Appsignal
|
|
|
7
7
|
def perform
|
|
8
8
|
# Read trace context off the job so the transaction links back to the
|
|
9
9
|
# enqueuer. No-op outside collector mode.
|
|
10
|
+
job_data = ResqueHelpers.active_job_data(payload)
|
|
10
11
|
transaction = Appsignal::Transaction.create(
|
|
11
12
|
Appsignal::Transaction::BACKGROUND_JOB,
|
|
12
|
-
:opentelemetry_context =>
|
|
13
|
+
:opentelemetry_context => ResqueHelpers.extract_context(payload, job_data),
|
|
13
14
|
:opentelemetry_scope => ["appsignal-ruby/resque", Appsignal::VERSION],
|
|
14
15
|
:opentelemetry_kind => :consumer,
|
|
15
|
-
:opentelemetry_relationship =>
|
|
16
|
+
:opentelemetry_relationship =>
|
|
17
|
+
Appsignal::OpenTelemetry.active_job_relationship(job_data)
|
|
16
18
|
)
|
|
17
19
|
# Describes this span as a job being performed. The messaging system is
|
|
18
20
|
# what the trace timeline reads to recognize background job work, and
|
|
@@ -93,14 +95,35 @@ module Appsignal
|
|
|
93
95
|
|
|
94
96
|
# @!visibility private
|
|
95
97
|
class ResqueHelpers
|
|
98
|
+
# The class the Active Job adapter enqueues, with the serialized job data
|
|
99
|
+
# as its only argument.
|
|
100
|
+
ACTIVE_JOB_WRAPPER = "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper"
|
|
101
|
+
|
|
96
102
|
def self.arguments(payload)
|
|
97
103
|
case payload["class"]
|
|
98
|
-
when
|
|
104
|
+
when ACTIVE_JOB_WRAPPER
|
|
99
105
|
nil # Set in the ActiveJob integration
|
|
100
106
|
else
|
|
101
107
|
payload["args"]
|
|
102
108
|
end
|
|
103
109
|
end
|
|
110
|
+
|
|
111
|
+
# The serialized Active Job job data inside a Resque job, or nil when this
|
|
112
|
+
# is not an Active Job job.
|
|
113
|
+
def self.active_job_data(payload)
|
|
114
|
+
return unless payload["class"] == ACTIVE_JOB_WRAPPER
|
|
115
|
+
|
|
116
|
+
job_data = payload["args"]&.first
|
|
117
|
+
job_data if job_data.is_a?(Hash)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The trace context to continue: the Active Job layer when this is an
|
|
121
|
+
# Active Job job, the Resque job itself otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
|
|
122
|
+
# for why that layer wins.
|
|
123
|
+
def self.extract_context(payload, job_data)
|
|
124
|
+
Appsignal::OpenTelemetry.extract_active_job_context(job_data) ||
|
|
125
|
+
Appsignal::OpenTelemetry.extract_job_context(payload)
|
|
126
|
+
end
|
|
104
127
|
end
|
|
105
128
|
end
|
|
106
129
|
end
|
|
@@ -77,14 +77,18 @@ module Appsignal
|
|
|
77
77
|
# links back to the enqueuer. A batch carries messages from multiple
|
|
78
78
|
# traces with no single parent, so only single messages link back.
|
|
79
79
|
# No-op outside collector mode.
|
|
80
|
-
context =
|
|
80
|
+
context = extract_context(sqs_msg, body) unless batch
|
|
81
81
|
|
|
82
82
|
transaction = Appsignal::Transaction.create(
|
|
83
83
|
Appsignal::Transaction::BACKGROUND_JOB,
|
|
84
84
|
:opentelemetry_context => context,
|
|
85
85
|
:opentelemetry_scope => ["appsignal-ruby/shoryuken", Appsignal::VERSION],
|
|
86
86
|
:opentelemetry_kind => :consumer,
|
|
87
|
-
|
|
87
|
+
# A message batch is a batch on the receiving side, and carries no
|
|
88
|
+
# context to relate to at all, so the body it reads here is the single
|
|
89
|
+
# message's job data or nothing.
|
|
90
|
+
:opentelemetry_relationship =>
|
|
91
|
+
Appsignal::OpenTelemetry.active_job_relationship(batch ? nil : body)
|
|
88
92
|
)
|
|
89
93
|
# Describes this span as a job being performed. The messaging system is
|
|
90
94
|
# what the trace timeline reads to recognize background job work.
|
|
@@ -127,6 +131,21 @@ module Appsignal
|
|
|
127
131
|
|
|
128
132
|
private
|
|
129
133
|
|
|
134
|
+
# The trace context to continue: the Active Job layer when this is an
|
|
135
|
+
# Active Job job, the message's own attributes otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
|
|
136
|
+
# for why that layer wins.
|
|
137
|
+
# SQS allows ten message attributes per message, shared with whatever the
|
|
138
|
+
# user puts there.
|
|
139
|
+
#
|
|
140
|
+
# The Active Job adapter registers a worker that parses the message body as
|
|
141
|
+
# JSON, so an Active Job job's body arrives here as its serialized job
|
|
142
|
+
# data. Nothing checks that it is one: a body with no readable trace
|
|
143
|
+
# context in it reads as "nothing here" on its own.
|
|
144
|
+
def extract_context(sqs_msg, body)
|
|
145
|
+
Appsignal::OpenTelemetry.extract_active_job_context(body) ||
|
|
146
|
+
ShoryukenTraceContext.extract(sqs_msg.message_attributes)
|
|
147
|
+
end
|
|
148
|
+
|
|
130
149
|
def fetch_attributes(batch, sqs_msg)
|
|
131
150
|
if batch
|
|
132
151
|
# We can't instrument batched message separately, the `yield` will
|
|
@@ -176,12 +176,14 @@ module Appsignal
|
|
|
176
176
|
action_name = formatted_action_name(item)
|
|
177
177
|
# Read trace context off the job so the transaction links back to the
|
|
178
178
|
# enqueuer. No-op outside collector mode.
|
|
179
|
+
job_data = active_job_data(item)
|
|
179
180
|
transaction = Appsignal::Transaction.create(
|
|
180
181
|
Appsignal::Transaction::BACKGROUND_JOB,
|
|
181
|
-
:opentelemetry_context =>
|
|
182
|
+
:opentelemetry_context => extract_context(item, job_data),
|
|
182
183
|
:opentelemetry_scope => ["appsignal-ruby/sidekiq", Appsignal::VERSION],
|
|
183
184
|
:opentelemetry_kind => :consumer,
|
|
184
|
-
:opentelemetry_relationship =>
|
|
185
|
+
:opentelemetry_relationship =>
|
|
186
|
+
Appsignal::OpenTelemetry.active_job_relationship(job_data)
|
|
185
187
|
)
|
|
186
188
|
transaction.add_opentelemetry_attributes(
|
|
187
189
|
Appsignal::OpenTelemetry::Messaging
|
|
@@ -246,6 +248,25 @@ module Appsignal
|
|
|
246
248
|
|
|
247
249
|
private
|
|
248
250
|
|
|
251
|
+
# The trace context to continue: the Active Job layer when this is an
|
|
252
|
+
# Active Job job, the Sidekiq job itself otherwise. See `Appsignal::OpenTelemetry.extract_active_job_context`
|
|
253
|
+
# for why that layer wins.
|
|
254
|
+
def extract_context(item, job_data)
|
|
255
|
+
Appsignal::OpenTelemetry.extract_active_job_context(job_data) ||
|
|
256
|
+
Appsignal::OpenTelemetry.extract_job_context(item)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The serialized Active Job job data inside a Sidekiq job, or nil when this
|
|
260
|
+
# is not an Active Job job. Both Active Job adapters for Sidekiq, the one in
|
|
261
|
+
# Rails and the one in the Sidekiq gem, enqueue a wrapper class with the
|
|
262
|
+
# job data as its only argument and name the real job class in `wrapped`.
|
|
263
|
+
def active_job_data(item)
|
|
264
|
+
return unless item["wrapped"]
|
|
265
|
+
|
|
266
|
+
job_data = item["args"]&.first
|
|
267
|
+
job_data if job_data.is_a?(Hash)
|
|
268
|
+
end
|
|
269
|
+
|
|
249
270
|
def increment_counter(key, value, tags = {})
|
|
250
271
|
Appsignal.increment_counter "sidekiq_#{key}", value, tags
|
|
251
272
|
end
|
|
@@ -30,22 +30,30 @@ module Appsignal
|
|
|
30
30
|
# Set here, where the transaction is created, so they land on the
|
|
31
31
|
# transaction's own span rather than on the event started below.
|
|
32
32
|
#
|
|
33
|
-
# Webmachine isn't Rack: the path, scheme
|
|
34
|
-
# request's `URI` rather than from Rack's readers. The
|
|
35
|
-
# `query` is the parsed form, so the URI is where the
|
|
33
|
+
# Webmachine isn't Rack: the path, scheme, query string, host and port
|
|
34
|
+
# come off the request's `URI` rather than from Rack's readers. The
|
|
35
|
+
# request's own `query` is the parsed form, so the URI is where the
|
|
36
|
+
# string itself is. Webmachine has no environment to read the protocol
|
|
37
|
+
# version from, so that one is left undescribed.
|
|
36
38
|
transaction.add_opentelemetry_attributes(
|
|
37
39
|
Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
|
|
38
40
|
:method => request.method,
|
|
39
41
|
:path => request.uri&.path,
|
|
40
42
|
:scheme => request.uri&.scheme,
|
|
41
|
-
:query => request.uri&.query
|
|
43
|
+
:query => request.uri&.query,
|
|
44
|
+
:host => request.uri&.host,
|
|
45
|
+
:port => request.uri&.port
|
|
42
46
|
)
|
|
43
47
|
)
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
begin
|
|
47
51
|
transaction.add_query_parameters_if_nil { request.query }
|
|
48
|
-
|
|
52
|
+
# `Webmachine::Headers` names a header the way OpenTelemetry does, in
|
|
53
|
+
# lowercase and with dashes, so these are headers and nothing else.
|
|
54
|
+
transaction.add_request_headers_if_nil do
|
|
55
|
+
request.headers if request.respond_to?(:headers)
|
|
56
|
+
end
|
|
49
57
|
|
|
50
58
|
Appsignal.instrument(
|
|
51
59
|
"process_action.webmachine",
|
|
@@ -21,6 +21,15 @@ module Appsignal
|
|
|
21
21
|
# collector filters it with the `filter_request_query_parameters` option, and
|
|
22
22
|
# builds the request's query parameters out of it.
|
|
23
23
|
#
|
|
24
|
+
# The host and the port describe where the client addressed the request. The
|
|
25
|
+
# conventions ask for the host the client used, so a caller reads it through
|
|
26
|
+
# the `Forwarded` and `X-Forwarded-Host` headers before falling back to the
|
|
27
|
+
# `Host` header and then to the server's own name. That is the order
|
|
28
|
+
# `Rack::Request#hostname` already follows. The port is only reported
|
|
29
|
+
# alongside a host, which is the condition the conventions put on it.
|
|
30
|
+
#
|
|
31
|
+
# The protocol version is the version out of `HTTP/1.1`, without the name.
|
|
32
|
+
#
|
|
24
33
|
# Every value is optional, because reading any of them from the request can
|
|
25
34
|
# fail. An attribute we have no value for is left out rather than sent
|
|
26
35
|
# empty.
|
|
@@ -28,17 +37,42 @@ module Appsignal
|
|
|
28
37
|
PATH_ATTRIBUTE = "url.path"
|
|
29
38
|
SCHEME_ATTRIBUTE = "url.scheme"
|
|
30
39
|
QUERY_ATTRIBUTE = "url.query"
|
|
40
|
+
ADDRESS_ATTRIBUTE = "server.address"
|
|
41
|
+
PORT_ATTRIBUTE = "server.port"
|
|
42
|
+
PROTOCOL_VERSION_ATTRIBUTE = "network.protocol.version"
|
|
31
43
|
|
|
32
44
|
class << self
|
|
33
45
|
# The attributes describing the given request, as a Hash to pass to
|
|
34
46
|
# `add_opentelemetry_attributes`.
|
|
35
|
-
def attributes_for(
|
|
47
|
+
def attributes_for( # rubocop:disable Metrics/ParameterLists
|
|
48
|
+
method:, path: nil, scheme: nil, query: nil, host: nil, port: nil, protocol: nil
|
|
49
|
+
)
|
|
36
50
|
attributes = HttpMethod.attributes_for(method)
|
|
37
51
|
attributes[PATH_ATTRIBUTE] = path.to_s unless path.to_s.empty?
|
|
38
52
|
attributes[SCHEME_ATTRIBUTE] = scheme.to_s unless scheme.to_s.empty?
|
|
39
53
|
attributes[QUERY_ATTRIBUTE] = query.to_s unless query.to_s.empty?
|
|
54
|
+
|
|
55
|
+
unless host.to_s.empty?
|
|
56
|
+
attributes[ADDRESS_ATTRIBUTE] = host.to_s
|
|
57
|
+
port = Integer(port, :exception => false)
|
|
58
|
+
attributes[PORT_ATTRIBUTE] = port if port
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
version = protocol_version_for(protocol)
|
|
62
|
+
attributes[PROTOCOL_VERSION_ATTRIBUTE] = version if version
|
|
63
|
+
|
|
40
64
|
attributes
|
|
41
65
|
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# `network.protocol.name` is asked for alongside the version only when
|
|
70
|
+
# the protocol is not HTTP. A value shaped any other way than `HTTP/x`
|
|
71
|
+
# is one we cannot name, so it is left out entirely rather than sent as
|
|
72
|
+
# a version with no name to go with it.
|
|
73
|
+
def protocol_version_for(protocol)
|
|
74
|
+
protocol.to_s[%r{\AHTTP/(.+)\z}, 1]
|
|
75
|
+
end
|
|
42
76
|
end
|
|
43
77
|
end
|
|
44
78
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Appsignal
|
|
6
|
+
module OpenTelemetry
|
|
7
|
+
# @!visibility private
|
|
8
|
+
#
|
|
9
|
+
# Routes an OTLP exporter's requests through the proxy in the
|
|
10
|
+
# `http_proxy` config option.
|
|
11
|
+
#
|
|
12
|
+
# The exporters accept no proxy and expose no way to reach the connection
|
|
13
|
+
# they build. They do build it in one method, `http_connection`, which
|
|
14
|
+
# this module overrides to configure the connection it returns.
|
|
15
|
+
#
|
|
16
|
+
# This module is included into a subclass rather than prepended onto the
|
|
17
|
+
# exporter itself, so that an application using the OpenTelemetry gems for
|
|
18
|
+
# its own exporting is unaffected.
|
|
19
|
+
#
|
|
20
|
+
# That method is not part of the exporters' public API, so a later version
|
|
21
|
+
# can rename it, stop calling it, or return something other than a
|
|
22
|
+
# `Net::HTTP` from it. This module does not try to predict which of those
|
|
23
|
+
# happened by inspecting the method. It records whether it managed to
|
|
24
|
+
# configure a connection, and {#initialize} reports it when it did not, so
|
|
25
|
+
# a version we cannot proxy through is visible in the log rather than
|
|
26
|
+
# silently sending around the proxy.
|
|
27
|
+
module ProxiedExporter
|
|
28
|
+
def initialize(appsignal_http_proxy:, **kwargs)
|
|
29
|
+
@appsignal_http_proxy = appsignal_http_proxy
|
|
30
|
+
@appsignal_proxy_applied = false
|
|
31
|
+
|
|
32
|
+
# The exporters build their connection while they initialize, so by
|
|
33
|
+
# the time this returns the override below has either run or never
|
|
34
|
+
# will.
|
|
35
|
+
super(**kwargs)
|
|
36
|
+
|
|
37
|
+
return if @appsignal_proxy_applied
|
|
38
|
+
|
|
39
|
+
Appsignal.internal_logger.error(
|
|
40
|
+
"Not sending #{self.class.superclass} data through the proxy in " \
|
|
41
|
+
"the `http_proxy` option: this version of the OpenTelemetry " \
|
|
42
|
+
"exporters does not build its connection where AppSignal " \
|
|
43
|
+
"configures the proxy."
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Whether the proxy was applied to the connection this exporter sends
|
|
48
|
+
# through. False means the exporter sends straight to its endpoint.
|
|
49
|
+
def appsignal_proxy_applied?
|
|
50
|
+
@appsignal_proxy_applied
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Accepts and forwards whatever the exporter calls this with, because
|
|
56
|
+
# none of the arguments are read here. A version that adds an argument,
|
|
57
|
+
# positional or keyword, is passed straight through instead of raising
|
|
58
|
+
# on the way to `super`.
|
|
59
|
+
def http_connection(*args, **kwargs)
|
|
60
|
+
apply_appsignal_proxy(super)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Point a connection at the proxy. Returns the connection either way, so
|
|
64
|
+
# an exporter whose connection cannot be proxied still sends its data.
|
|
65
|
+
def apply_appsignal_proxy(http)
|
|
66
|
+
return http unless http.respond_to?(:proxy_from_env=)
|
|
67
|
+
|
|
68
|
+
proxy = URI.parse(@appsignal_http_proxy)
|
|
69
|
+
|
|
70
|
+
# `Net::HTTP#proxy?` reads the address only when the connection is not
|
|
71
|
+
# taking its proxy from the environment, which it does by default.
|
|
72
|
+
http.proxy_from_env = false
|
|
73
|
+
http.proxy_address = proxy.host
|
|
74
|
+
http.proxy_port = proxy.port
|
|
75
|
+
http.proxy_user = proxy.user
|
|
76
|
+
http.proxy_pass = proxy.password
|
|
77
|
+
|
|
78
|
+
@appsignal_proxy_applied = true
|
|
79
|
+
http
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -8,12 +8,18 @@ require "appsignal/opentelemetry/http_method"
|
|
|
8
8
|
require "appsignal/opentelemetry/http_response"
|
|
9
9
|
require "appsignal/opentelemetry/http_server_request"
|
|
10
10
|
require "appsignal/opentelemetry/messaging"
|
|
11
|
+
require "appsignal/opentelemetry/proxied_exporter"
|
|
11
12
|
require "appsignal/opentelemetry/rendering"
|
|
12
13
|
require "appsignal/opentelemetry/sql_db_system"
|
|
13
14
|
|
|
14
15
|
module Appsignal
|
|
15
16
|
# @!visibility private
|
|
16
17
|
module OpenTelemetry
|
|
18
|
+
# The carrier key that marks an Active Job job as one of a batch. Not a W3C
|
|
19
|
+
# trace context header, and deliberately not named like one, so no
|
|
20
|
+
# propagator reads it as one.
|
|
21
|
+
ACTIVE_JOB_BATCH_HEADER = "appsignal-batch"
|
|
22
|
+
|
|
17
23
|
class << self
|
|
18
24
|
# Configure the global OpenTelemetry SDK to export OTLP/HTTP protobuf to
|
|
19
25
|
# the collector endpoint in `config[:collector_endpoint]`.
|
|
@@ -55,14 +61,16 @@ module Appsignal
|
|
|
55
61
|
# resource for the tracer provider to keep all three in sync.
|
|
56
62
|
resource = ::OpenTelemetry::SDK::Resources::Resource.default.merge(build_resource(config))
|
|
57
63
|
|
|
64
|
+
span_exporter = build_exporter(
|
|
65
|
+
::OpenTelemetry::Exporter::OTLP::Exporter,
|
|
66
|
+
config,
|
|
67
|
+
:endpoint => "#{endpoint}/v1/traces"
|
|
68
|
+
)
|
|
69
|
+
|
|
58
70
|
::OpenTelemetry::SDK.configure do |c|
|
|
59
71
|
c.resource = resource
|
|
60
72
|
c.add_span_processor(
|
|
61
|
-
::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
62
|
-
::OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
63
|
-
:endpoint => "#{endpoint}/v1/traces"
|
|
64
|
-
)
|
|
65
|
-
)
|
|
73
|
+
::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(span_exporter)
|
|
66
74
|
)
|
|
67
75
|
end
|
|
68
76
|
|
|
@@ -72,22 +80,26 @@ module Appsignal
|
|
|
72
80
|
# `force_flush` is a no-op.
|
|
73
81
|
::OpenTelemetry.meter_provider =
|
|
74
82
|
::OpenTelemetry::SDK::Metrics::MeterProvider.new(:resource => resource)
|
|
83
|
+
metrics_exporter = build_exporter(
|
|
84
|
+
::OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter,
|
|
85
|
+
config,
|
|
86
|
+
:endpoint => "#{endpoint}/v1/metrics"
|
|
87
|
+
)
|
|
75
88
|
::OpenTelemetry.meter_provider.add_metric_reader(
|
|
76
89
|
::OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
|
|
77
|
-
:exporter =>
|
|
78
|
-
:endpoint => "#{endpoint}/v1/metrics"
|
|
79
|
-
)
|
|
90
|
+
:exporter => metrics_exporter
|
|
80
91
|
)
|
|
81
92
|
)
|
|
82
93
|
|
|
94
|
+
logs_exporter = build_exporter(
|
|
95
|
+
::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter,
|
|
96
|
+
config,
|
|
97
|
+
:endpoint => "#{endpoint}/v1/logs"
|
|
98
|
+
)
|
|
83
99
|
::OpenTelemetry.logger_provider =
|
|
84
100
|
::OpenTelemetry::SDK::Logs::LoggerProvider.new(:resource => resource)
|
|
85
101
|
::OpenTelemetry.logger_provider.add_log_record_processor(
|
|
86
|
-
::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(
|
|
87
|
-
::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
|
|
88
|
-
:endpoint => "#{endpoint}/v1/logs"
|
|
89
|
-
)
|
|
90
|
-
)
|
|
102
|
+
::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(logs_exporter)
|
|
91
103
|
)
|
|
92
104
|
|
|
93
105
|
@started = true
|
|
@@ -159,9 +171,8 @@ module Appsignal
|
|
|
159
171
|
def extract_job_context(item)
|
|
160
172
|
if_started do
|
|
161
173
|
carrier = item
|
|
162
|
-
nested = item["__otel_headers"]
|
|
163
|
-
|
|
164
|
-
carrier = item.merge(nested) if nested.is_a?(Hash)
|
|
174
|
+
nested = otel_headers_hash(item["__otel_headers"])
|
|
175
|
+
carrier = item.merge(nested) if nested
|
|
165
176
|
# Extract onto an empty context rather than the default
|
|
166
177
|
# `Context.current`, for the same reason as `extract_rack_context`:
|
|
167
178
|
# a job with no injected trace context must not inherit an ambient
|
|
@@ -174,6 +185,107 @@ module Appsignal
|
|
|
174
185
|
end
|
|
175
186
|
end
|
|
176
187
|
|
|
188
|
+
# Read the trace context off the serialized Active Job job data that a
|
|
189
|
+
# queue adapter's job wraps, so the transaction the adapter creates links
|
|
190
|
+
# back to the enqueuer.
|
|
191
|
+
#
|
|
192
|
+
# Every adapter wraps the job data as the single argument of its own job
|
|
193
|
+
# wrapper, but each keeps it somewhere different, so finding the job data
|
|
194
|
+
# is the adapter integration's business and reading a context out of it is
|
|
195
|
+
# this method's.
|
|
196
|
+
#
|
|
197
|
+
# This is the layer every integration prefers. Active Job owns the job
|
|
198
|
+
# whichever adapter carries it, its carrier survives an adapter that has
|
|
199
|
+
# nowhere of its own to put a header, and it does not compete with the
|
|
200
|
+
# user's own data for a carrier with a hard limit on what fits.
|
|
201
|
+
#
|
|
202
|
+
# Returns `nil` when the SDK has not booted, when this is not Active Job
|
|
203
|
+
# job data, and when the job data carries no usable context. A caller reads
|
|
204
|
+
# that `nil` as "nothing here" and falls back to its own native carrier.
|
|
205
|
+
# That fallback matters twice over: it is where a job enqueued by a service
|
|
206
|
+
# that instruments only the adapter carries its context, and it is the only
|
|
207
|
+
# carrier a job that is not an Active Job job has at all.
|
|
208
|
+
def extract_active_job_context(job_data)
|
|
209
|
+
return unless job_data.is_a?(Hash)
|
|
210
|
+
|
|
211
|
+
if_started do
|
|
212
|
+
headers = otel_headers_hash(job_data["__otel_headers"])
|
|
213
|
+
next unless headers
|
|
214
|
+
|
|
215
|
+
# Extract onto an empty context, for the same reason as
|
|
216
|
+
# `extract_job_context` above.
|
|
217
|
+
context = ::OpenTelemetry.propagation.extract(
|
|
218
|
+
headers,
|
|
219
|
+
:context => ::OpenTelemetry::Context.empty
|
|
220
|
+
)
|
|
221
|
+
context if remote_span_context(context)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Marks an outgoing Active Job carrier as belonging to a batch, so the
|
|
226
|
+
# integration that later performs the job can tell the two enqueue paths
|
|
227
|
+
# apart. Every job in a batch shares the one producer span, and a span can
|
|
228
|
+
# have only one parent, so a batch has to link back rather than parent
|
|
229
|
+
# under it -- and only the enqueue side knows it was a batch.
|
|
230
|
+
#
|
|
231
|
+
# The marker rides in the same carrier as the trace context.
|
|
232
|
+
# `propagation.extract` ignores a key it does not recognise, so it is
|
|
233
|
+
# invisible to every other reader of that carrier, including
|
|
234
|
+
# OpenTelemetry's own Active Job instrumentation.
|
|
235
|
+
#
|
|
236
|
+
# Does nothing to a carrier nothing was injected into. Without a context
|
|
237
|
+
# there is no producer span to link back to, so the marker would have
|
|
238
|
+
# nothing to say.
|
|
239
|
+
def mark_active_job_batch(headers)
|
|
240
|
+
return if headers.empty?
|
|
241
|
+
|
|
242
|
+
headers[ACTIVE_JOB_BATCH_HEADER] = "1"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Whether Active Job job data says the job was enqueued as part of a batch.
|
|
246
|
+
# An integration reads this to choose between linking the performed job
|
|
247
|
+
# back to the enqueuer and also parenting it under them.
|
|
248
|
+
def active_job_batch?(job_data)
|
|
249
|
+
return false unless job_data.is_a?(Hash)
|
|
250
|
+
|
|
251
|
+
headers = otel_headers_hash(job_data["__otel_headers"])
|
|
252
|
+
return false unless headers
|
|
253
|
+
|
|
254
|
+
headers[ACTIVE_JOB_BATCH_HEADER] == "1"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# How a performed job should relate to the span that enqueued it.
|
|
258
|
+
#
|
|
259
|
+
# A job enqueued on its own is the only job its producer span produced, so
|
|
260
|
+
# it can be a child of that span as well as link to it. Every job in a
|
|
261
|
+
# batch shares one producer span, and a span can have only one parent, so
|
|
262
|
+
# parenting a batch would hang the whole batch off that single span. Only
|
|
263
|
+
# link those, which is what the OpenTelemetry messaging conventions ask
|
|
264
|
+
# for: they use links as the default, and allow the producer to be the
|
|
265
|
+
# parent only when it produced a single message.
|
|
266
|
+
def active_job_relationship(job_data)
|
|
267
|
+
active_job_batch?(job_data) ? :link : :both
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# The remote parent's SpanContext from an incoming OTel context, or `nil`
|
|
271
|
+
# when there is no context or the span in it is invalid.
|
|
272
|
+
#
|
|
273
|
+
# `propagation.extract` returns a context whether or not the carrier held
|
|
274
|
+
# anything, so this is what tells "read a context" apart from "read
|
|
275
|
+
# nothing". A caller that can fall back to another carrier uses it to
|
|
276
|
+
# decide whether to, and a caller that parents or links a span uses it to
|
|
277
|
+
# decide between doing that and starting a plain root span.
|
|
278
|
+
#
|
|
279
|
+
# Only ever called with a context that came from the OpenTelemetry SDK, so
|
|
280
|
+
# it does not gate on the SDK having booted the way the extract methods do.
|
|
281
|
+
def remote_span_context(opentelemetry_context)
|
|
282
|
+
return unless opentelemetry_context
|
|
283
|
+
|
|
284
|
+
span_context =
|
|
285
|
+
::OpenTelemetry::Trace.current_span(opentelemetry_context).context
|
|
286
|
+
span_context if span_context.valid?
|
|
287
|
+
end
|
|
288
|
+
|
|
177
289
|
# Run `block` only when the OpenTelemetry SDK has booted (collector mode),
|
|
178
290
|
# returning its result; a no-op returning `nil` otherwise. The block can
|
|
179
291
|
# touch the OTel SDK freely -- it only runs when the SDK is loaded.
|
|
@@ -212,10 +324,12 @@ module Appsignal
|
|
|
212
324
|
|
|
213
325
|
# Build the OpenTelemetry Resource that carries AppSignal config to the
|
|
214
326
|
# collector. Attributes whose underlying option is nil or an empty array
|
|
215
|
-
# are omitted so the collector applies its own defaults.
|
|
327
|
+
# are omitted so the collector applies its own defaults. The revision,
|
|
328
|
+
# service name and host name are the exception: they fall back to a
|
|
329
|
+
# value here, so they are always sent.
|
|
216
330
|
def build_resource(config)
|
|
217
331
|
revision = config[:revision].to_s.empty? ? "unknown" : config[:revision]
|
|
218
|
-
service_name = config[:service_name].to_s.empty? ? "
|
|
332
|
+
service_name = config[:service_name].to_s.empty? ? "app" : config[:service_name]
|
|
219
333
|
host_name = config[:hostname].to_s.empty? ? "unknown" : config[:hostname]
|
|
220
334
|
|
|
221
335
|
attrs = {
|
|
@@ -223,6 +337,8 @@ module Appsignal
|
|
|
223
337
|
"appsignal.config.environment" => config.env,
|
|
224
338
|
"appsignal.config.push_api_key" => config[:push_api_key],
|
|
225
339
|
"appsignal.config.revision" => revision,
|
|
340
|
+
"appsignal.config.app_path" => config.root_path&.to_s,
|
|
341
|
+
"appsignal.config.platform" => config[:platform],
|
|
226
342
|
"appsignal.config.language_integration" => "ruby",
|
|
227
343
|
"service.name" => service_name,
|
|
228
344
|
"host.name" => host_name,
|
|
@@ -234,25 +350,67 @@ module Appsignal
|
|
|
234
350
|
"appsignal.config.filter_request_session_data" => config[:filter_session_data],
|
|
235
351
|
"appsignal.config.ignore_actions" => config[:ignore_actions],
|
|
236
352
|
"appsignal.config.ignore_errors" => config[:ignore_errors],
|
|
353
|
+
"appsignal.config.ignore_logs" => config[:ignore_logs],
|
|
237
354
|
"appsignal.config.ignore_namespaces" => config[:ignore_namespaces],
|
|
238
|
-
"appsignal.config.response_headers" =>
|
|
239
|
-
|
|
355
|
+
"appsignal.config.response_headers" =>
|
|
356
|
+
normalized_header_names(config[:response_headers]),
|
|
357
|
+
# The collector filters `http.request.header.*` attributes by their
|
|
358
|
+
# OpenTelemetry names, which is what `keep_request_headers` holds.
|
|
359
|
+
"appsignal.config.request_headers" =>
|
|
360
|
+
normalized_header_names(config[:keep_request_headers]),
|
|
240
361
|
"appsignal.config.send_function_parameters" => config[:send_function_parameters],
|
|
241
362
|
"appsignal.config.send_request_query_parameters" =>
|
|
242
363
|
config[:send_request_query_parameters],
|
|
243
364
|
"appsignal.config.send_request_payload" => config[:send_request_payload],
|
|
244
365
|
"appsignal.config.send_request_session_data" => config[:send_session_data]
|
|
245
366
|
}
|
|
246
|
-
|
|
367
|
+
# An absent attribute leaves the collector its own default, which an empty
|
|
368
|
+
# allowlist cannot say, so an empty list is sent and an unset option is not.
|
|
369
|
+
attrs.reject! { |_, value| value.nil? || value == "" }
|
|
247
370
|
::OpenTelemetry::SDK::Resources::Resource.create(attrs)
|
|
248
371
|
end
|
|
249
372
|
|
|
250
373
|
private
|
|
251
374
|
|
|
375
|
+
def normalized_header_names(names)
|
|
376
|
+
names.map { |name| Appsignal::Utils::RequestHeaders.normalize(name) }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Build one OTLP exporter, applying the `ca_file_path` and `http_proxy`
|
|
380
|
+
# options to the requests it sends. The certificate file is a keyword
|
|
381
|
+
# argument the exporters accept; the proxy is not, so an exporter that
|
|
382
|
+
# needs one is a subclass that applies it to its own connection.
|
|
383
|
+
def build_exporter(base, config, **kwargs)
|
|
384
|
+
certificate_file = config[:ca_file_path].to_s
|
|
385
|
+
kwargs[:certificate_file] = certificate_file unless certificate_file.empty?
|
|
386
|
+
|
|
387
|
+
http_proxy = config[:http_proxy].to_s
|
|
388
|
+
return base.new(**kwargs) if http_proxy.empty?
|
|
389
|
+
|
|
390
|
+
proxied_exporter_class(base).new(:appsignal_http_proxy => http_proxy, **kwargs)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# A subclass of an OTLP exporter that routes its requests through a
|
|
394
|
+
# proxy. Built here rather than declared, because the exporter gems are
|
|
395
|
+
# only loaded once collector mode is configured.
|
|
396
|
+
def proxied_exporter_class(base)
|
|
397
|
+
Class.new(base) { include ProxiedExporter }
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# A `__otel_headers` value as a hash carrier, or `nil` when there is no
|
|
401
|
+
# usable one. Active Job puts the headers through its argument serializer,
|
|
402
|
+
# which turns the hash into an array of `[key, value]` pairs, so both
|
|
403
|
+
# shapes arrive. Anything else, including a malformed array, gives `nil`
|
|
404
|
+
# rather than raising on `to_h` inside a job perform.
|
|
405
|
+
def otel_headers_hash(value)
|
|
406
|
+
return value if value.is_a?(Hash)
|
|
407
|
+
return value.to_h if otel_header_pairs?(value)
|
|
408
|
+
|
|
409
|
+
nil
|
|
410
|
+
end
|
|
411
|
+
|
|
252
412
|
# Whether a `__otel_headers` value is the array-of-`[key, value]`-pairs
|
|
253
|
-
# shape produced by ActiveJob's argument serializer
|
|
254
|
-
# into a hash carrier. Anything else (including a malformed array) is left
|
|
255
|
-
# alone rather than raising on `to_h` inside a job perform.
|
|
413
|
+
# shape produced by ActiveJob's argument serializer.
|
|
256
414
|
def otel_header_pairs?(value)
|
|
257
415
|
value.is_a?(Array) && value.all? { |pair| pair.is_a?(Array) && pair.size == 2 }
|
|
258
416
|
end
|
|
@@ -98,7 +98,10 @@ module Appsignal
|
|
|
98
98
|
:method => Appsignal::Rack::Utils.request_method_from(request),
|
|
99
99
|
:path => Appsignal::Rack::Utils.request_value_from(request, :path),
|
|
100
100
|
:scheme => Appsignal::Rack::Utils.request_value_from(request, :scheme),
|
|
101
|
-
:query => Appsignal::Rack::Utils.request_value_from(request, :query_string)
|
|
101
|
+
:query => Appsignal::Rack::Utils.request_value_from(request, :query_string),
|
|
102
|
+
:host => Appsignal::Rack::Utils.request_value_from(request, :hostname),
|
|
103
|
+
:port => Appsignal::Rack::Utils.request_value_from(request, :port),
|
|
104
|
+
:protocol => Appsignal::Rack::Utils.request_env_value_from(request, "SERVER_PROTOCOL")
|
|
102
105
|
)
|
|
103
106
|
)
|
|
104
107
|
end
|
|
@@ -81,7 +81,11 @@ module Appsignal
|
|
|
81
81
|
:method => Appsignal::Rack::Utils.request_method_from(request),
|
|
82
82
|
:path => Appsignal::Rack::Utils.request_value_from(request, :path),
|
|
83
83
|
:scheme => Appsignal::Rack::Utils.request_value_from(request, :scheme),
|
|
84
|
-
:query => Appsignal::Rack::Utils.request_value_from(request, :query_string)
|
|
84
|
+
:query => Appsignal::Rack::Utils.request_value_from(request, :query_string),
|
|
85
|
+
:host => Appsignal::Rack::Utils.request_value_from(request, :hostname),
|
|
86
|
+
:port => Appsignal::Rack::Utils.request_value_from(request, :port),
|
|
87
|
+
:protocol =>
|
|
88
|
+
Appsignal::Rack::Utils.request_env_value_from(request, "SERVER_PROTOCOL")
|
|
85
89
|
)
|
|
86
90
|
)
|
|
87
91
|
transaction.start_event(
|
|
@@ -141,7 +145,10 @@ module Appsignal
|
|
|
141
145
|
self.class.safe_execution("Appsignal::Rack::EventHandler#on_finish") do
|
|
142
146
|
transaction.finish_event("process_request.rack", "callback: on_finish", "")
|
|
143
147
|
transaction.add_request_payload_if_nil { request.params }
|
|
144
|
-
|
|
148
|
+
headers, environment =
|
|
149
|
+
Appsignal::Utils::RequestHeaders.split_lazily { request.env }
|
|
150
|
+
transaction.add_request_headers_if_nil(&headers)
|
|
151
|
+
transaction.add_request_environment_if_nil(&environment)
|
|
145
152
|
transaction.add_session_data_if_nil do
|
|
146
153
|
request.session if request.respond_to?(:session)
|
|
147
154
|
end
|