appsignal 4.10.0-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 +42 -0
- data/Rakefile +87 -0
- data/appsignal.gemspec +8 -0
- data/build_matrix.yml +14 -1
- data/ext/appsignal_extension.c +14 -0
- data/ext/base.rb +8 -1
- data/lib/appsignal/backends.rb +55 -0
- data/lib/appsignal/config.rb +140 -1
- data/lib/appsignal/demo.rb +1 -1
- data/lib/appsignal/event_formatter/action_view/render_formatter.rb +34 -22
- data/lib/appsignal/event_formatter/active_job/perform_formatter.rb +35 -0
- data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +19 -0
- data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +27 -0
- data/lib/appsignal/event_formatter/recorded_elsewhere.rb +17 -0
- data/lib/appsignal/event_formatter/rom/sql_formatter.rb +24 -0
- data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +5 -0
- data/lib/appsignal/event_formatter/view_component/render_formatter.rb +21 -10
- data/lib/appsignal/event_formatter.rb +78 -0
- data/lib/appsignal/extension.rb +4 -0
- data/lib/appsignal/helpers/instrumentation.rb +236 -20
- data/lib/appsignal/helpers/metrics.rb +3 -24
- data/lib/appsignal/hooks/action_cable.rb +18 -6
- data/lib/appsignal/hooks/active_job.rb +128 -39
- data/lib/appsignal/hooks/at_exit.rb +4 -1
- data/lib/appsignal/hooks/excon.rb +20 -0
- data/lib/appsignal/hooks/faraday.rb +16 -0
- data/lib/appsignal/hooks/http.rb +5 -0
- data/lib/appsignal/hooks/resque.rb +1 -1
- data/lib/appsignal/hooks/sequel.rb +32 -2
- data/lib/appsignal/hooks/shoryuken.rb +3 -3
- data/lib/appsignal/hooks/sidekiq.rb +1 -1
- data/lib/appsignal/integrations/action_cable.rb +5 -2
- data/lib/appsignal/integrations/active_support_notifications.rb +59 -14
- data/lib/appsignal/integrations/data_mapper.rb +14 -2
- data/lib/appsignal/integrations/delayed_job_plugin.rb +45 -5
- data/lib/appsignal/integrations/dry_monitor.rb +39 -15
- data/lib/appsignal/integrations/excon/appsignal_middleware.rb +21 -0
- data/lib/appsignal/integrations/excon.rb +52 -15
- data/lib/appsignal/integrations/faraday.rb +47 -12
- data/lib/appsignal/integrations/http.rb +43 -1
- data/lib/appsignal/integrations/mongo_ruby_driver.rb +73 -4
- data/lib/appsignal/integrations/net_http.rb +31 -2
- data/lib/appsignal/integrations/puma.rb +4 -1
- data/lib/appsignal/integrations/que.rb +224 -37
- data/lib/appsignal/integrations/railtie.rb +4 -1
- data/lib/appsignal/integrations/rake.rb +9 -3
- data/lib/appsignal/integrations/redis.rb +22 -1
- data/lib/appsignal/integrations/redis_client.rb +22 -1
- data/lib/appsignal/integrations/resque.rb +57 -10
- data/lib/appsignal/integrations/shoryuken.rb +140 -12
- data/lib/appsignal/integrations/sidekiq.rb +73 -16
- data/lib/appsignal/integrations/webmachine.rb +47 -4
- data/lib/appsignal/loaders/padrino.rb +2 -1
- data/lib/appsignal/logger/extension_backend.rb +24 -0
- data/lib/appsignal/logger/opentelemetry_backend.rb +66 -0
- data/lib/appsignal/logger.rb +13 -9
- data/lib/appsignal/metrics/extension_backend.rb +47 -0
- data/lib/appsignal/metrics/opentelemetry_backend.rb +89 -0
- data/lib/appsignal/opentelemetry/attributes.rb +31 -0
- data/lib/appsignal/opentelemetry/dependencies.rb +35 -0
- data/lib/appsignal/opentelemetry/error_type.rb +37 -0
- data/lib/appsignal/opentelemetry/http_client_request.rb +83 -0
- data/lib/appsignal/opentelemetry/http_method.rb +59 -0
- data/lib/appsignal/opentelemetry/http_response.rb +30 -0
- data/lib/appsignal/opentelemetry/http_server_request.rb +45 -0
- data/lib/appsignal/opentelemetry/messaging.rb +82 -0
- data/lib/appsignal/opentelemetry/rendering.rb +29 -0
- data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
- data/lib/appsignal/opentelemetry.rb +337 -0
- data/lib/appsignal/rack/abstract_middleware.rb +63 -4
- data/lib/appsignal/rack/body_wrapper.rb +18 -5
- data/lib/appsignal/rack/event_handler.rb +36 -3
- data/lib/appsignal/rack/grape_middleware.rb +1 -0
- data/lib/appsignal/rack/hanami_middleware.rb +2 -1
- data/lib/appsignal/rack/instrumentation_middleware.rb +1 -0
- data/lib/appsignal/rack/rails_instrumentation.rb +1 -0
- data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -0
- data/lib/appsignal/rack.rb +39 -11
- data/lib/appsignal/sample_data.rb +4 -0
- data/lib/appsignal/transaction/base_backend.rb +107 -0
- data/lib/appsignal/transaction/extension_backend.rb +203 -0
- data/lib/appsignal/transaction/opentelemetry_backend.rb +796 -0
- data/lib/appsignal/transaction.rb +537 -144
- data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +9 -0
- data/sig/appsignal.rbi +426 -37
- data/sig/appsignal.rbs +386 -27
- metadata +23 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "appsignal/opentelemetry/attributes"
|
|
4
|
+
require "appsignal/opentelemetry/dependencies"
|
|
5
|
+
require "appsignal/opentelemetry/error_type"
|
|
6
|
+
require "appsignal/opentelemetry/http_client_request"
|
|
7
|
+
require "appsignal/opentelemetry/http_method"
|
|
8
|
+
require "appsignal/opentelemetry/http_response"
|
|
9
|
+
require "appsignal/opentelemetry/http_server_request"
|
|
10
|
+
require "appsignal/opentelemetry/messaging"
|
|
11
|
+
require "appsignal/opentelemetry/rendering"
|
|
12
|
+
require "appsignal/opentelemetry/sql_db_system"
|
|
13
|
+
|
|
14
|
+
module Appsignal
|
|
15
|
+
# @!visibility private
|
|
16
|
+
module OpenTelemetry
|
|
17
|
+
class << self
|
|
18
|
+
# Configure the global OpenTelemetry SDK to export OTLP/HTTP protobuf to
|
|
19
|
+
# the collector endpoint in `config[:collector_endpoint]`.
|
|
20
|
+
#
|
|
21
|
+
# The SDK and exporter gems are required lazily, so an application not in
|
|
22
|
+
# collector mode does not pay the load cost. Sets `@started`, which
|
|
23
|
+
# {.started?} reads to decide whether to route through the OTel backends.
|
|
24
|
+
def configure(config)
|
|
25
|
+
# The OTel Ruby SDK exposes no programmatic knob for the default
|
|
26
|
+
# aggregation temporality; this env var is the only way to set
|
|
27
|
+
# it. We pick `:delta` to match the Python integration. (Note:
|
|
28
|
+
# the Ruby SDK keeps `UpDownCounter` cumulative regardless of
|
|
29
|
+
# this preference, per the OTel spec.)
|
|
30
|
+
ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] ||= "delta"
|
|
31
|
+
|
|
32
|
+
# With the metrics and logs SDK gems loaded, `SDK.configure` below
|
|
33
|
+
# auto-installs a metrics reader and a log processor from these env vars,
|
|
34
|
+
# each with its own background thread. Both providers are replaced right
|
|
35
|
+
# after, which would leave those threads running and unreachable by any
|
|
36
|
+
# shutdown. Set unconditionally, so a user-set "otlp" cannot slip past
|
|
37
|
+
# and reintroduce them.
|
|
38
|
+
ENV["OTEL_METRICS_EXPORTER"] = "none"
|
|
39
|
+
ENV["OTEL_LOGS_EXPORTER"] = "none"
|
|
40
|
+
|
|
41
|
+
require_sdk_gems
|
|
42
|
+
|
|
43
|
+
# The OpenTelemetry gems are optional and installed by the user (not
|
|
44
|
+
# declared in the gemspec). If they're present but older than the
|
|
45
|
+
# versions we support, fall back to the agent rather than booting an
|
|
46
|
+
# SDK that may misbehave (e.g. a metrics SDK without fork hooks).
|
|
47
|
+
return unless required_gem_versions_met?
|
|
48
|
+
|
|
49
|
+
endpoint = config[:collector_endpoint].to_s.sub(%r{/+\z}, "")
|
|
50
|
+
# Merge with the SDK's default resource so all three signal types
|
|
51
|
+
# carry the same `telemetry.sdk.*` and `process.*` attributes that
|
|
52
|
+
# `SDK.configure` would have added on its own. `MeterProvider` and
|
|
53
|
+
# `LoggerProvider` take a `resource:` kwarg that replaces (not
|
|
54
|
+
# merges), so we do the merge ourselves and use the same merged
|
|
55
|
+
# resource for the tracer provider to keep all three in sync.
|
|
56
|
+
resource = ::OpenTelemetry::SDK::Resources::Resource.default.merge(build_resource(config))
|
|
57
|
+
|
|
58
|
+
::OpenTelemetry::SDK.configure do |c|
|
|
59
|
+
c.resource = resource
|
|
60
|
+
c.add_span_processor(
|
|
61
|
+
::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
62
|
+
::OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
63
|
+
:endpoint => "#{endpoint}/v1/traces"
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Wrap the OTLP MetricsExporter in a PeriodicMetricReader so that
|
|
70
|
+
# `MeterProvider#force_flush` actually triggers an export. The OTLP
|
|
71
|
+
# exporter itself is also a MetricReader but its inherited
|
|
72
|
+
# `force_flush` is a no-op.
|
|
73
|
+
::OpenTelemetry.meter_provider =
|
|
74
|
+
::OpenTelemetry::SDK::Metrics::MeterProvider.new(:resource => resource)
|
|
75
|
+
::OpenTelemetry.meter_provider.add_metric_reader(
|
|
76
|
+
::OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
|
|
77
|
+
:exporter => ::OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new(
|
|
78
|
+
:endpoint => "#{endpoint}/v1/metrics"
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
::OpenTelemetry.logger_provider =
|
|
84
|
+
::OpenTelemetry::SDK::Logs::LoggerProvider.new(:resource => resource)
|
|
85
|
+
::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
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@started = true
|
|
94
|
+
rescue LoadError => e
|
|
95
|
+
@started = false
|
|
96
|
+
Appsignal::Utils::StdoutAndLoggerMessage.error(
|
|
97
|
+
"Cannot configure OpenTelemetry SDK for collector mode: #{e.class}: #{e.message}"
|
|
98
|
+
)
|
|
99
|
+
rescue => e
|
|
100
|
+
@started = false
|
|
101
|
+
Appsignal::Utils::StdoutAndLoggerMessage.error(
|
|
102
|
+
"Error configuring OpenTelemetry SDK for collector mode: " \
|
|
103
|
+
"#{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Whether {.configure} has successfully booted the OpenTelemetry SDK
|
|
108
|
+
# for this process. Returns `false` before {.configure} runs and
|
|
109
|
+
# `false` if it ran but raised.
|
|
110
|
+
def started?
|
|
111
|
+
defined?(@started) ? @started : false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Write the current trace context onto an outgoing carrier (HTTP request,
|
|
115
|
+
# job hash, ...) with the configured propagator, so a downstream service
|
|
116
|
+
# joins the same trace.
|
|
117
|
+
#
|
|
118
|
+
# A no-op unless the SDK has booted. The carrier is injected from whatever
|
|
119
|
+
# span is current at call time, which inside an `Appsignal.instrument`
|
|
120
|
+
# block is the event's own span.
|
|
121
|
+
def inject_context(carrier)
|
|
122
|
+
if_started do
|
|
123
|
+
::OpenTelemetry.propagation.inject(carrier)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Read the trace context off an incoming Rack request env using the
|
|
128
|
+
# globally configured propagator, so an AppSignal transaction created for
|
|
129
|
+
# the request can continue the upstream trace. Returns an
|
|
130
|
+
# `OpenTelemetry::Context` (its current span is the remote parent), or
|
|
131
|
+
# `nil` when the SDK has not booted -- outside collector mode there is
|
|
132
|
+
# nothing to continue. `rack_env_getter` reads the `HTTP_*`-mangled header
|
|
133
|
+
# names Rack puts in the env.
|
|
134
|
+
def extract_rack_context(env)
|
|
135
|
+
if_started do
|
|
136
|
+
# Extract onto an empty context, not `Context.current`. The W3C
|
|
137
|
+
# extractor returns its base context unchanged when the carrier has no
|
|
138
|
+
# `traceparent`, so a request with no incoming context would inherit
|
|
139
|
+
# whatever span is ambient on the fiber. Starting empty means "no
|
|
140
|
+
# carrier" yields no parent, and the transaction starts its own trace.
|
|
141
|
+
::OpenTelemetry.propagation.extract(
|
|
142
|
+
env,
|
|
143
|
+
:context => ::OpenTelemetry::Context.empty,
|
|
144
|
+
:getter => ::OpenTelemetry::Common::Propagation.rack_env_getter
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Read the trace context off an incoming background job hash, so the
|
|
150
|
+
# transaction can link back to the enqueuer. Returns an
|
|
151
|
+
# `OpenTelemetry::Context`, or `nil` when the SDK has not booted.
|
|
152
|
+
#
|
|
153
|
+
# Reads both carriers a job can arrive with: top-level `traceparent` and
|
|
154
|
+
# `tracestate` keys, as OpenTelemetry's Sidekiq instrumentation injects
|
|
155
|
+
# them, and a nested `__otel_headers`, as its Active Job one does. Active
|
|
156
|
+
# Job puts that through its argument serializer, so it can arrive as an
|
|
157
|
+
# array of pairs rather than a hash. The nested keys win, being the more
|
|
158
|
+
# specific layer.
|
|
159
|
+
def extract_job_context(item)
|
|
160
|
+
if_started do
|
|
161
|
+
carrier = item
|
|
162
|
+
nested = item["__otel_headers"]
|
|
163
|
+
nested = nested.to_h if otel_header_pairs?(nested)
|
|
164
|
+
carrier = item.merge(nested) if nested.is_a?(Hash)
|
|
165
|
+
# Extract onto an empty context rather than the default
|
|
166
|
+
# `Context.current`, for the same reason as `extract_rack_context`:
|
|
167
|
+
# a job with no injected trace context must not inherit an ambient
|
|
168
|
+
# span left on the fiber. Otherwise the job's transaction would link
|
|
169
|
+
# back to an unrelated leaked span instead of standing on its own.
|
|
170
|
+
::OpenTelemetry.propagation.extract(
|
|
171
|
+
carrier,
|
|
172
|
+
:context => ::OpenTelemetry::Context.empty
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Run `block` only when the OpenTelemetry SDK has booted (collector mode),
|
|
178
|
+
# returning its result; a no-op returning `nil` otherwise. The block can
|
|
179
|
+
# touch the OTel SDK freely -- it only runs when the SDK is loaded.
|
|
180
|
+
#
|
|
181
|
+
# This is the gate every integration's OTel-specific work goes through, so
|
|
182
|
+
# integration-specific carrier/getter/setter logic lives in the
|
|
183
|
+
# integration rather than as a bespoke helper here.
|
|
184
|
+
def if_started
|
|
185
|
+
return unless started?
|
|
186
|
+
|
|
187
|
+
yield
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @!visibility private
|
|
191
|
+
#
|
|
192
|
+
# Test-only. Drops the started flag so subsequent tests start from a
|
|
193
|
+
# clean slate; does not touch the global `::OpenTelemetry` providers.
|
|
194
|
+
def reset!
|
|
195
|
+
@started = false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Flush and shut down the OpenTelemetry SDK providers booted by
|
|
199
|
+
# {.configure}. Called from `Appsignal.stop` so buffered
|
|
200
|
+
# metrics/logs/spans don't get dropped on exit.
|
|
201
|
+
def shutdown
|
|
202
|
+
return unless started?
|
|
203
|
+
|
|
204
|
+
::OpenTelemetry.tracer_provider&.shutdown
|
|
205
|
+
::OpenTelemetry.meter_provider&.shutdown
|
|
206
|
+
::OpenTelemetry.logger_provider&.shutdown
|
|
207
|
+
rescue => e
|
|
208
|
+
Appsignal.internal_logger.error(
|
|
209
|
+
"Error shutting down OpenTelemetry SDK: #{e.class}: #{e.message}"
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Build the OpenTelemetry Resource that carries AppSignal config to the
|
|
214
|
+
# collector. Attributes whose underlying option is nil or an empty array
|
|
215
|
+
# are omitted so the collector applies its own defaults.
|
|
216
|
+
def build_resource(config)
|
|
217
|
+
revision = config[:revision].to_s.empty? ? "unknown" : config[:revision]
|
|
218
|
+
service_name = config[:service_name].to_s.empty? ? "unknown" : config[:service_name]
|
|
219
|
+
host_name = config[:hostname].to_s.empty? ? "unknown" : config[:hostname]
|
|
220
|
+
|
|
221
|
+
attrs = {
|
|
222
|
+
"appsignal.config.name" => config[:name],
|
|
223
|
+
"appsignal.config.environment" => config.env,
|
|
224
|
+
"appsignal.config.push_api_key" => config[:push_api_key],
|
|
225
|
+
"appsignal.config.revision" => revision,
|
|
226
|
+
"appsignal.config.language_integration" => "ruby",
|
|
227
|
+
"service.name" => service_name,
|
|
228
|
+
"host.name" => host_name,
|
|
229
|
+
"appsignal.config.filter_attributes" => config[:filter_attributes],
|
|
230
|
+
"appsignal.config.filter_function_parameters" => config[:filter_function_parameters],
|
|
231
|
+
"appsignal.config.filter_request_query_parameters" =>
|
|
232
|
+
config[:filter_request_query_parameters],
|
|
233
|
+
"appsignal.config.filter_request_payload" => config[:filter_request_payload],
|
|
234
|
+
"appsignal.config.filter_request_session_data" => config[:filter_session_data],
|
|
235
|
+
"appsignal.config.ignore_actions" => config[:ignore_actions],
|
|
236
|
+
"appsignal.config.ignore_errors" => config[:ignore_errors],
|
|
237
|
+
"appsignal.config.ignore_namespaces" => config[:ignore_namespaces],
|
|
238
|
+
"appsignal.config.response_headers" => config[:response_headers],
|
|
239
|
+
"appsignal.config.request_headers" => config[:request_headers],
|
|
240
|
+
"appsignal.config.send_function_parameters" => config[:send_function_parameters],
|
|
241
|
+
"appsignal.config.send_request_query_parameters" =>
|
|
242
|
+
config[:send_request_query_parameters],
|
|
243
|
+
"appsignal.config.send_request_payload" => config[:send_request_payload],
|
|
244
|
+
"appsignal.config.send_request_session_data" => config[:send_session_data]
|
|
245
|
+
}
|
|
246
|
+
attrs.reject! { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
|
|
247
|
+
::OpenTelemetry::SDK::Resources::Resource.create(attrs)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
# Whether a `__otel_headers` value is the array-of-`[key, value]`-pairs
|
|
253
|
+
# shape produced by ActiveJob's argument serializer, so it can be turned
|
|
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.
|
|
256
|
+
def otel_header_pairs?(value)
|
|
257
|
+
value.is_a?(Array) && value.all? { |pair| pair.is_a?(Array) && pair.size == 2 }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# The optional OpenTelemetry gems, required lazily so users not in
|
|
261
|
+
# collector mode don't pay the load cost. A missing gem raises LoadError,
|
|
262
|
+
# caught by {.configure}.
|
|
263
|
+
def require_sdk_gems
|
|
264
|
+
require "opentelemetry/sdk"
|
|
265
|
+
require "opentelemetry-common"
|
|
266
|
+
require "opentelemetry/exporter/otlp"
|
|
267
|
+
require "opentelemetry-metrics-sdk"
|
|
268
|
+
require "opentelemetry-exporter-otlp-metrics"
|
|
269
|
+
require "opentelemetry-logs-sdk"
|
|
270
|
+
require "opentelemetry-exporter-otlp-logs"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Checks the installed OpenTelemetry gem versions against {REQUIRED_GEMS}.
|
|
274
|
+
# On a shortfall, warns and flags the SDK as not started so the caller
|
|
275
|
+
# falls back to the agent; returns whether all requirements are met.
|
|
276
|
+
def required_gem_versions_met?
|
|
277
|
+
incompatible = incompatible_gems
|
|
278
|
+
return true if incompatible.nil?
|
|
279
|
+
|
|
280
|
+
@started = false
|
|
281
|
+
Appsignal::Utils::StdoutAndLoggerMessage.warning(
|
|
282
|
+
collector_gems_warning(incompatible)
|
|
283
|
+
)
|
|
284
|
+
false
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Builds the warning shown when the OpenTelemetry gems collector mode needs
|
|
288
|
+
# are not all installed at a supported version. `incompatible` describes
|
|
289
|
+
# the gems that are installed but at a version we do not support.
|
|
290
|
+
#
|
|
291
|
+
# The message always recommends the `appsignal-opentelemetry` gem, which
|
|
292
|
+
# installs the whole set. It only lists gems when some are installed at an
|
|
293
|
+
# incompatible version, because that usually means a constraint in the
|
|
294
|
+
# bundle that the gem cannot override on its own.
|
|
295
|
+
def collector_gems_warning(incompatible)
|
|
296
|
+
message =
|
|
297
|
+
"AppSignal collector mode requires a set of OpenTelemetry gems. " \
|
|
298
|
+
"Add the `appsignal-opentelemetry` gem to your bundle to install " \
|
|
299
|
+
"them. The AppSignal agent will be used instead."
|
|
300
|
+
|
|
301
|
+
unless incompatible.empty?
|
|
302
|
+
message += "\n\nThese installed OpenTelemetry gems are not compatible " \
|
|
303
|
+
"with this AppSignal version. Update them or remove a version " \
|
|
304
|
+
"constraint:\n"
|
|
305
|
+
message += incompatible.map { |line| "- #{line}" }.join("\n")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
message
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Checks the installed OpenTelemetry gems against {REQUIRED_GEMS}. Returns
|
|
312
|
+
# `nil` when every required gem is installed at a supported version.
|
|
313
|
+
# Otherwise returns the descriptions of gems that are installed but at a
|
|
314
|
+
# version we do not support. That list is empty when the only problem is
|
|
315
|
+
# that some required gems are not installed at all.
|
|
316
|
+
def incompatible_gems
|
|
317
|
+
missing = false
|
|
318
|
+
incompatible = []
|
|
319
|
+
|
|
320
|
+
REQUIRED_GEMS.each do |name, constraints|
|
|
321
|
+
spec = Gem.loaded_specs[name]
|
|
322
|
+
requirement = Gem::Requirement.new(*constraints)
|
|
323
|
+
|
|
324
|
+
if spec.nil?
|
|
325
|
+
missing = true
|
|
326
|
+
elsif !requirement.satisfied_by?(spec.version)
|
|
327
|
+
incompatible << "#{name} #{spec.version} (requires #{requirement})"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
return nil if !missing && incompatible.empty?
|
|
332
|
+
|
|
333
|
+
incompatible
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
@@ -20,6 +20,10 @@ module Appsignal
|
|
|
20
20
|
@options = options
|
|
21
21
|
@request_class = options.fetch(:request_class, ::Rack::Request)
|
|
22
22
|
@instrument_event_name = options.fetch(:instrument_event_name, nil)
|
|
23
|
+
# The OpenTelemetry instrumentation scope for the request's spans in
|
|
24
|
+
# collector mode. Each framework middleware sets its own; nil falls back
|
|
25
|
+
# to the default scope in the backend.
|
|
26
|
+
@opentelemetry_scope = options.fetch(:opentelemetry_scope, nil)
|
|
23
27
|
@report_errors = options.fetch(:report_errors, DEFAULT_ERROR_REPORTING)
|
|
24
28
|
end
|
|
25
29
|
|
|
@@ -33,7 +37,11 @@ module Appsignal
|
|
|
33
37
|
if wrapped_instrumentation
|
|
34
38
|
env[Appsignal::Rack::APPSIGNAL_TRANSACTION]
|
|
35
39
|
else
|
|
36
|
-
Appsignal::Transaction.create(
|
|
40
|
+
Appsignal::Transaction.create(
|
|
41
|
+
Appsignal::Transaction::HTTP_REQUEST,
|
|
42
|
+
:opentelemetry_context => Appsignal::OpenTelemetry.extract_rack_context(env),
|
|
43
|
+
:opentelemetry_scope => @opentelemetry_scope
|
|
44
|
+
)
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
unless wrapped_instrumentation
|
|
@@ -41,6 +49,7 @@ module Appsignal
|
|
|
41
49
|
# middleware can detect if there is parent instrumentation
|
|
42
50
|
# middleware active.
|
|
43
51
|
env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = transaction
|
|
52
|
+
add_opentelemetry_request_attributes(transaction, request)
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
begin
|
|
@@ -59,8 +68,13 @@ module Appsignal
|
|
|
59
68
|
ensure
|
|
60
69
|
add_transaction_metadata_after(transaction, request)
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
unless wrapped_instrumentation
|
|
72
|
+
add_opentelemetry_response_attributes(transaction, request)
|
|
73
|
+
|
|
74
|
+
# Complete transaction because this is the top instrumentation
|
|
75
|
+
# middleware.
|
|
76
|
+
Appsignal::Transaction.complete_current!
|
|
77
|
+
end
|
|
64
78
|
end
|
|
65
79
|
else
|
|
66
80
|
@app.call(env)
|
|
@@ -69,6 +83,45 @@ module Appsignal
|
|
|
69
83
|
|
|
70
84
|
private
|
|
71
85
|
|
|
86
|
+
# Describes the transaction's span as an incoming HTTP request. Together
|
|
87
|
+
# with the SERVER span kind the transaction already carries, this is what
|
|
88
|
+
# the trace timeline reads to recognize a web request.
|
|
89
|
+
#
|
|
90
|
+
# Set where the transaction is created, rather than with the rest of the
|
|
91
|
+
# request metadata in {ApplyRackRequest}, which runs after the app has been
|
|
92
|
+
# called: by then an event span may be open, and the attributes would land
|
|
93
|
+
# on that instead. Only the middleware that created the transaction sets
|
|
94
|
+
# them, so nested middleware does not write them again.
|
|
95
|
+
def add_opentelemetry_request_attributes(transaction, request)
|
|
96
|
+
transaction.add_opentelemetry_attributes(
|
|
97
|
+
Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
|
|
98
|
+
:method => Appsignal::Rack::Utils.request_method_from(request),
|
|
99
|
+
:path => Appsignal::Rack::Utils.request_value_from(request, :path),
|
|
100
|
+
:scheme => Appsignal::Rack::Utils.request_value_from(request, :scheme),
|
|
101
|
+
:query => Appsignal::Rack::Utils.request_value_from(request, :query_string)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Describes the response the app produced on the transaction's span, which
|
|
107
|
+
# the semantic conventions ask for whenever a response was sent.
|
|
108
|
+
#
|
|
109
|
+
# Set from the `ensure` in {#call} rather than from {#call_app}, where the
|
|
110
|
+
# status is first known. That runs inside the instrumented event, so the
|
|
111
|
+
# attribute would land on the event span instead of on the transaction's
|
|
112
|
+
# own span. The status travels between the two in the request environment
|
|
113
|
+
# for that reason.
|
|
114
|
+
#
|
|
115
|
+
# A request whose app raised never produced a status, and is described
|
|
116
|
+
# without one.
|
|
117
|
+
def add_opentelemetry_response_attributes(transaction, request)
|
|
118
|
+
transaction.add_opentelemetry_attributes(
|
|
119
|
+
Appsignal::OpenTelemetry::HttpResponse.attributes_for(
|
|
120
|
+
request.env[Appsignal::Rack::APPSIGNAL_RESPONSE_STATUS]
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
72
125
|
# Another instrumentation middleware is active earlier in the stack, so
|
|
73
126
|
# don't report any exceptions here, the top instrumentation middleware
|
|
74
127
|
# will be the one reporting the exception.
|
|
@@ -79,7 +132,10 @@ module Appsignal
|
|
|
79
132
|
# @see #instrument_app_call_with_exception_handling
|
|
80
133
|
def instrument_app_call(env, transaction)
|
|
81
134
|
if @instrument_event_name
|
|
82
|
-
Appsignal.instrument(
|
|
135
|
+
Appsignal.instrument(
|
|
136
|
+
@instrument_event_name,
|
|
137
|
+
:opentelemetry_scope => @opentelemetry_scope
|
|
138
|
+
) do
|
|
83
139
|
call_app(env, transaction)
|
|
84
140
|
end
|
|
85
141
|
else
|
|
@@ -89,6 +145,9 @@ module Appsignal
|
|
|
89
145
|
|
|
90
146
|
def call_app(env, transaction)
|
|
91
147
|
status, headers, obody = @app.call(env)
|
|
148
|
+
# Remember the status for {#add_opentelemetry_response_attributes}, which
|
|
149
|
+
# runs once this event has closed.
|
|
150
|
+
env[Appsignal::Rack::APPSIGNAL_RESPONSE_STATUS] = status
|
|
92
151
|
body =
|
|
93
152
|
if env[Appsignal::Rack::APPSIGNAL_RESPONSE_INSTRUMENTED]
|
|
94
153
|
obody
|
|
@@ -48,7 +48,10 @@ module Appsignal
|
|
|
48
48
|
# of the body has already closed itself (as prescribed) we do not
|
|
49
49
|
# attempt to close it twice
|
|
50
50
|
if !@body_already_closed && @body.respond_to?(:close)
|
|
51
|
-
Appsignal.instrument(
|
|
51
|
+
Appsignal.instrument(
|
|
52
|
+
"close_response_body.rack",
|
|
53
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
54
|
+
) { @body.close }
|
|
52
55
|
end
|
|
53
56
|
@body_already_closed = true
|
|
54
57
|
rescue *IGNORED_ERRORS # Do not report
|
|
@@ -104,7 +107,11 @@ module Appsignal
|
|
|
104
107
|
# in a blockless way it is still a good idea to have it in place.
|
|
105
108
|
return enum_for(:each) unless block_given?
|
|
106
109
|
|
|
107
|
-
Appsignal.instrument(
|
|
110
|
+
Appsignal.instrument(
|
|
111
|
+
"process_response_body.rack",
|
|
112
|
+
"Process Rack response body (#each)",
|
|
113
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
114
|
+
) do
|
|
108
115
|
@body.each(&blk)
|
|
109
116
|
end
|
|
110
117
|
rescue *IGNORED_ERRORS # Do not report
|
|
@@ -125,7 +132,11 @@ module Appsignal
|
|
|
125
132
|
def call(stream)
|
|
126
133
|
# `stream` will be closed by the app we are calling, no need for us
|
|
127
134
|
# to close it ourselves
|
|
128
|
-
Appsignal.instrument(
|
|
135
|
+
Appsignal.instrument(
|
|
136
|
+
"process_response_body.rack",
|
|
137
|
+
"Process Rack response body (#call)",
|
|
138
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
139
|
+
) do
|
|
129
140
|
@body.call(stream)
|
|
130
141
|
end
|
|
131
142
|
rescue *IGNORED_ERRORS # Do not report
|
|
@@ -150,7 +161,8 @@ module Appsignal
|
|
|
150
161
|
@body_already_closed = true
|
|
151
162
|
Appsignal.instrument(
|
|
152
163
|
"process_response_body.rack",
|
|
153
|
-
"Process Rack response body (#to_ary)"
|
|
164
|
+
"Process Rack response body (#to_ary)",
|
|
165
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
154
166
|
) do
|
|
155
167
|
@body.to_ary
|
|
156
168
|
end
|
|
@@ -168,7 +180,8 @@ module Appsignal
|
|
|
168
180
|
def to_path
|
|
169
181
|
Appsignal.instrument(
|
|
170
182
|
"process_response_body.rack",
|
|
171
|
-
"Process Rack response body (#to_path)"
|
|
183
|
+
"Process Rack response body (#to_path)",
|
|
184
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
172
185
|
) do
|
|
173
186
|
@body.to_path
|
|
174
187
|
end
|
|
@@ -63,8 +63,30 @@ module Appsignal
|
|
|
63
63
|
request.env[APPSIGNAL_EVENT_HANDLER_ID] ||= id
|
|
64
64
|
return unless request_handler?(request.env[APPSIGNAL_EVENT_HANDLER_ID])
|
|
65
65
|
|
|
66
|
-
transaction = Appsignal::Transaction.create(
|
|
67
|
-
|
|
66
|
+
transaction = Appsignal::Transaction.create(
|
|
67
|
+
Appsignal::Transaction::HTTP_REQUEST,
|
|
68
|
+
:opentelemetry_context => Appsignal::OpenTelemetry.extract_rack_context(request.env),
|
|
69
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
70
|
+
)
|
|
71
|
+
# Describes the transaction's span as an incoming HTTP request.
|
|
72
|
+
# Together with the SERVER span kind the transaction already carries,
|
|
73
|
+
# this is what the trace timeline reads to recognize a web request.
|
|
74
|
+
#
|
|
75
|
+
# Set before the event below starts, because attributes go on
|
|
76
|
+
# whichever span is open and these belong on the transaction's own
|
|
77
|
+
# span. That event stays open until the response finishes, so there is
|
|
78
|
+
# no later point in the request at which these could be set.
|
|
79
|
+
transaction.add_opentelemetry_attributes(
|
|
80
|
+
Appsignal::OpenTelemetry::HttpServerRequest.attributes_for(
|
|
81
|
+
:method => Appsignal::Rack::Utils.request_method_from(request),
|
|
82
|
+
:path => Appsignal::Rack::Utils.request_value_from(request, :path),
|
|
83
|
+
:scheme => Appsignal::Rack::Utils.request_value_from(request, :scheme),
|
|
84
|
+
:query => Appsignal::Rack::Utils.request_value_from(request, :query_string)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
transaction.start_event(
|
|
88
|
+
:opentelemetry_scope => ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
89
|
+
)
|
|
68
90
|
request.env[APPSIGNAL_TRANSACTION] = transaction
|
|
69
91
|
|
|
70
92
|
request.env[RACK_AFTER_REPLY] ||= []
|
|
@@ -118,13 +140,24 @@ module Appsignal
|
|
|
118
140
|
|
|
119
141
|
self.class.safe_execution("Appsignal::Rack::EventHandler#on_finish") do
|
|
120
142
|
transaction.finish_event("process_request.rack", "callback: on_finish", "")
|
|
121
|
-
transaction.
|
|
143
|
+
transaction.add_request_payload_if_nil { request.params }
|
|
122
144
|
transaction.add_headers_if_nil { request.env }
|
|
123
145
|
transaction.add_session_data_if_nil do
|
|
124
146
|
request.session if request.respond_to?(:session)
|
|
125
147
|
end
|
|
126
148
|
queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
|
|
127
149
|
transaction.set_queue_start(queue_start) if queue_start
|
|
150
|
+
# Describes the response on the transaction's span, which the semantic
|
|
151
|
+
# conventions ask for whenever a response was sent. It can be set here
|
|
152
|
+
# because the `process_request.rack` event was finished above, which
|
|
153
|
+
# leaves the transaction's own span as the one attributes go on.
|
|
154
|
+
#
|
|
155
|
+
# Only a response the app actually produced counts. The 500 below
|
|
156
|
+
# stands in for a status that was never sent, so it is reported as a
|
|
157
|
+
# tag and a metric but not as this attribute.
|
|
158
|
+
transaction.add_opentelemetry_attributes(
|
|
159
|
+
Appsignal::OpenTelemetry::HttpResponse.attributes_for(response&.status)
|
|
160
|
+
)
|
|
128
161
|
response_status =
|
|
129
162
|
if response
|
|
130
163
|
response.status
|
|
@@ -7,6 +7,7 @@ module Appsignal
|
|
|
7
7
|
# @api private
|
|
8
8
|
def initialize(app, options = {})
|
|
9
9
|
options[:instrument_event_name] = "process_request.grape"
|
|
10
|
+
options[:opentelemetry_scope] = ["appsignal-ruby/grape", Appsignal::VERSION]
|
|
10
11
|
options[:report_errors] = lambda { |env| !env["grape.skip_appsignal_error"] }
|
|
11
12
|
super
|
|
12
13
|
end
|
|
@@ -7,6 +7,7 @@ module Appsignal
|
|
|
7
7
|
def initialize(app, options = {})
|
|
8
8
|
options[:params_method] = nil
|
|
9
9
|
options[:instrument_event_name] ||= "process_action.hanami"
|
|
10
|
+
options[:opentelemetry_scope] ||= ["appsignal-ruby/hanami", Appsignal::VERSION]
|
|
10
11
|
super
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -19,7 +20,7 @@ module Appsignal
|
|
|
19
20
|
def add_transaction_metadata_after(transaction, request)
|
|
20
21
|
action_name = fetch_hanami_action(request.env)
|
|
21
22
|
transaction.set_action_if_nil(action_name) if action_name
|
|
22
|
-
transaction.
|
|
23
|
+
transaction.add_request_payload { params_for(request) }
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def params_for(request)
|
|
@@ -55,6 +55,7 @@ module Appsignal
|
|
|
55
55
|
class InstrumentationMiddleware < AbstractMiddleware
|
|
56
56
|
def initialize(app, options = {})
|
|
57
57
|
options[:instrument_event_name] ||= "process_request_middleware.rack"
|
|
58
|
+
options[:opentelemetry_scope] ||= ["appsignal-ruby/rack", Appsignal::VERSION]
|
|
58
59
|
super
|
|
59
60
|
end
|
|
60
61
|
end
|
|
@@ -8,6 +8,7 @@ module Appsignal
|
|
|
8
8
|
options[:request_class] ||= ActionDispatch::Request
|
|
9
9
|
options[:params_method] ||= :filtered_parameters
|
|
10
10
|
options[:instrument_event_name] = nil
|
|
11
|
+
options[:opentelemetry_scope] = ["appsignal-ruby/rails", Appsignal::VERSION]
|
|
11
12
|
options[:report_errors] = true
|
|
12
13
|
super
|
|
13
14
|
end
|
|
@@ -33,6 +33,7 @@ module Appsignal
|
|
|
33
33
|
options[:request_class] ||= Sinatra::Request
|
|
34
34
|
options[:params_method] ||= :params
|
|
35
35
|
options[:instrument_event_name] ||= "process_action.sinatra"
|
|
36
|
+
options[:opentelemetry_scope] ||= ["appsignal-ruby/sinatra", Appsignal::VERSION]
|
|
36
37
|
super
|
|
37
38
|
@raise_errors_on = raise_errors?(app)
|
|
38
39
|
end
|