appsignal 4.10.4-java → 5.0.0-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 +108 -0
- data/Rakefile +195 -4
- data/appsignal.gemspec +8 -0
- data/build_matrix.yml +12 -0
- data/ext/appsignal_extension.c +14 -0
- data/lib/appsignal/backends.rb +55 -0
- data/lib/appsignal/cli/diagnose.rb +2 -10
- data/lib/appsignal/config.rb +377 -13
- data/lib/appsignal/demo.rb +12 -10
- 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 +324 -20
- data/lib/appsignal/helpers/metrics.rb +3 -24
- data/lib/appsignal/hooks/action_cable.rb +26 -8
- data/lib/appsignal/hooks/active_job.rb +184 -47
- 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 +81 -8
- 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 +256 -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 +81 -11
- data/lib/appsignal/integrations/shoryuken.rb +159 -12
- data/lib/appsignal/integrations/sidekiq.rb +94 -16
- data/lib/appsignal/integrations/webmachine.rb +56 -5
- 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 +79 -0
- data/lib/appsignal/opentelemetry/messaging.rb +82 -0
- data/lib/appsignal/opentelemetry/proxied_exporter.rb +83 -0
- data/lib/appsignal/opentelemetry/rendering.rb +29 -0
- data/lib/appsignal/opentelemetry/sql_db_system.rb +89 -0
- data/lib/appsignal/opentelemetry.rb +495 -0
- data/lib/appsignal/rack/abstract_middleware.rb +66 -4
- data/lib/appsignal/rack/body_wrapper.rb +18 -5
- data/lib/appsignal/rack/event_handler.rb +44 -4
- 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 +68 -12
- data/lib/appsignal/sample_data.rb +4 -0
- data/lib/appsignal/transaction/base_backend.rb +128 -0
- data/lib/appsignal/transaction/extension_backend.rb +229 -0
- data/lib/appsignal/transaction/opentelemetry_backend.rb +847 -0
- data/lib/appsignal/transaction.rb +714 -164
- data/lib/appsignal/utils/request_headers.rb +78 -0
- data/lib/appsignal/utils/stdout_and_logger_message.rb +9 -0
- data/lib/appsignal/utils.rb +1 -0
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +10 -0
- data/sig/appsignal.rbi +630 -37
- data/sig/appsignal.rbs +582 -27
- metadata +25 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appsignal
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# @!visibility private
|
|
6
|
+
#
|
|
7
|
+
# Maps a SQL library's own name for the engine it is talking to onto the
|
|
8
|
+
# `db.system.name` semantic conventions value for that engine.
|
|
9
|
+
#
|
|
10
|
+
# Each library names engines differently, and none of them matches the
|
|
11
|
+
# semantic conventions spelling, so every SQL integration needs a lookup.
|
|
12
|
+
# Kept as one map per library, each keyed on that library's own
|
|
13
|
+
# vocabulary, rather than one shared map: ActiveRecord's `ADAPTER_NAME`
|
|
14
|
+
# strings, Sequel's `database_type` symbols, and DataMapper's
|
|
15
|
+
# `DataObjects` connection class names do not collide today, but nothing
|
|
16
|
+
# about that is enforced by combining them. A wrong value passed into the
|
|
17
|
+
# wrong library's lookup would silently return another library's answer
|
|
18
|
+
# instead of nil, and a library's own gaps would be invisible next to two
|
|
19
|
+
# other libraries' complete entries. ROM is the exception: its payload
|
|
20
|
+
# already carries Sequel's own `database_type` symbol, so it is
|
|
21
|
+
# legitimate for it to share Sequel's map, not just convenient.
|
|
22
|
+
#
|
|
23
|
+
# A name none of these maps recognise returns `nil`, so the caller's own
|
|
24
|
+
# `other_sql` fallback applies -- exactly what already happens for a
|
|
25
|
+
# library this gem has no mapping for at all.
|
|
26
|
+
#
|
|
27
|
+
# This intentionally follows the older semantic conventions value set:
|
|
28
|
+
# SQL Server maps to `mssql`, not the current registry's
|
|
29
|
+
# `microsoft.sql_server`, and Oracle maps to `oracle`, not `oracle.db`.
|
|
30
|
+
# That is what the AppSignal collector's sanitizer recognizes today;
|
|
31
|
+
# emitting the newer values would silently turn sanitization off for
|
|
32
|
+
# those engines' queries.
|
|
33
|
+
module SqlDbSystem
|
|
34
|
+
# ActiveRecord's `ADAPTER_NAME`. Rails bundles the Postgres, Mysql2,
|
|
35
|
+
# SQLite and (7.1+) Trilogy adapters; SQL Server and Oracle come from
|
|
36
|
+
# the separate `activerecord-sqlserver-adapter` and
|
|
37
|
+
# `activerecord-oracle_enhanced-adapter` gems, which declare
|
|
38
|
+
# `ADAPTER_NAME` the same way.
|
|
39
|
+
ACTIVE_RECORD = {
|
|
40
|
+
"PostgreSQL" => "postgresql",
|
|
41
|
+
"Mysql2" => "mysql",
|
|
42
|
+
"Trilogy" => "mysql",
|
|
43
|
+
"SQLite" => "sqlite",
|
|
44
|
+
"SQLServer" => "mssql",
|
|
45
|
+
"OracleEnhanced" => "oracle"
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Sequel's `database_type`. ROM's dry-monitor payload reuses this
|
|
49
|
+
# symbol directly, so `name_for_sequel` is also ROM's lookup.
|
|
50
|
+
SEQUEL = {
|
|
51
|
+
:postgres => "postgresql",
|
|
52
|
+
:mysql => "mysql",
|
|
53
|
+
:sqlite => "sqlite",
|
|
54
|
+
:mssql => "mssql",
|
|
55
|
+
:oracle => "oracle"
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
# DataMapper's `DataObjects` connection classes.
|
|
59
|
+
DATA_MAPPER = {
|
|
60
|
+
"DataObjects::Postgres::Connection" => "postgresql",
|
|
61
|
+
"DataObjects::Mysql::Connection" => "mysql",
|
|
62
|
+
"DataObjects::Sqlite3::Connection" => "sqlite",
|
|
63
|
+
"DataObjects::SqlServer::Connection" => "mssql"
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# The `db.system.name` value for an ActiveRecord connection's
|
|
68
|
+
# `adapter_name`, or `nil` when the adapter is not one this map
|
|
69
|
+
# recognises.
|
|
70
|
+
def name_for_active_record(adapter_name)
|
|
71
|
+
ACTIVE_RECORD[adapter_name]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The `db.system.name` value for Sequel's `database_type`, or `nil`
|
|
75
|
+
# when it is not one this map recognises. Also the lookup ROM's
|
|
76
|
+
# formatter uses, since ROM reports this same symbol.
|
|
77
|
+
def name_for_sequel(database_type)
|
|
78
|
+
SEQUEL[database_type]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The `db.system.name` value for DataMapper's connection class name,
|
|
82
|
+
# or `nil` when it is not one this map recognises.
|
|
83
|
+
def name_for_data_mapper(connection_class_name)
|
|
84
|
+
DATA_MAPPER[connection_class_name]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,495 @@
|
|
|
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/proxied_exporter"
|
|
12
|
+
require "appsignal/opentelemetry/rendering"
|
|
13
|
+
require "appsignal/opentelemetry/sql_db_system"
|
|
14
|
+
|
|
15
|
+
module Appsignal
|
|
16
|
+
# @!visibility private
|
|
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
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Configure the global OpenTelemetry SDK to export OTLP/HTTP protobuf to
|
|
25
|
+
# the collector endpoint in `config[:collector_endpoint]`.
|
|
26
|
+
#
|
|
27
|
+
# The SDK and exporter gems are required lazily, so an application not in
|
|
28
|
+
# collector mode does not pay the load cost. Sets `@started`, which
|
|
29
|
+
# {.started?} reads to decide whether to route through the OTel backends.
|
|
30
|
+
def configure(config)
|
|
31
|
+
# The OTel Ruby SDK exposes no programmatic knob for the default
|
|
32
|
+
# aggregation temporality; this env var is the only way to set
|
|
33
|
+
# it. We pick `:delta` to match the Python integration. (Note:
|
|
34
|
+
# the Ruby SDK keeps `UpDownCounter` cumulative regardless of
|
|
35
|
+
# this preference, per the OTel spec.)
|
|
36
|
+
ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] ||= "delta"
|
|
37
|
+
|
|
38
|
+
# With the metrics and logs SDK gems loaded, `SDK.configure` below
|
|
39
|
+
# auto-installs a metrics reader and a log processor from these env vars,
|
|
40
|
+
# each with its own background thread. Both providers are replaced right
|
|
41
|
+
# after, which would leave those threads running and unreachable by any
|
|
42
|
+
# shutdown. Set unconditionally, so a user-set "otlp" cannot slip past
|
|
43
|
+
# and reintroduce them.
|
|
44
|
+
ENV["OTEL_METRICS_EXPORTER"] = "none"
|
|
45
|
+
ENV["OTEL_LOGS_EXPORTER"] = "none"
|
|
46
|
+
|
|
47
|
+
require_sdk_gems
|
|
48
|
+
|
|
49
|
+
# The OpenTelemetry gems are optional and installed by the user (not
|
|
50
|
+
# declared in the gemspec). If they're present but older than the
|
|
51
|
+
# versions we support, fall back to the agent rather than booting an
|
|
52
|
+
# SDK that may misbehave (e.g. a metrics SDK without fork hooks).
|
|
53
|
+
return unless required_gem_versions_met?
|
|
54
|
+
|
|
55
|
+
endpoint = config[:collector_endpoint].to_s.sub(%r{/+\z}, "")
|
|
56
|
+
# Merge with the SDK's default resource so all three signal types
|
|
57
|
+
# carry the same `telemetry.sdk.*` and `process.*` attributes that
|
|
58
|
+
# `SDK.configure` would have added on its own. `MeterProvider` and
|
|
59
|
+
# `LoggerProvider` take a `resource:` kwarg that replaces (not
|
|
60
|
+
# merges), so we do the merge ourselves and use the same merged
|
|
61
|
+
# resource for the tracer provider to keep all three in sync.
|
|
62
|
+
resource = ::OpenTelemetry::SDK::Resources::Resource.default.merge(build_resource(config))
|
|
63
|
+
|
|
64
|
+
span_exporter = build_exporter(
|
|
65
|
+
::OpenTelemetry::Exporter::OTLP::Exporter,
|
|
66
|
+
config,
|
|
67
|
+
:endpoint => "#{endpoint}/v1/traces"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
::OpenTelemetry::SDK.configure do |c|
|
|
71
|
+
c.resource = resource
|
|
72
|
+
c.add_span_processor(
|
|
73
|
+
::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(span_exporter)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Wrap the OTLP MetricsExporter in a PeriodicMetricReader so that
|
|
78
|
+
# `MeterProvider#force_flush` actually triggers an export. The OTLP
|
|
79
|
+
# exporter itself is also a MetricReader but its inherited
|
|
80
|
+
# `force_flush` is a no-op.
|
|
81
|
+
::OpenTelemetry.meter_provider =
|
|
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
|
+
)
|
|
88
|
+
::OpenTelemetry.meter_provider.add_metric_reader(
|
|
89
|
+
::OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
|
|
90
|
+
:exporter => metrics_exporter
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logs_exporter = build_exporter(
|
|
95
|
+
::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter,
|
|
96
|
+
config,
|
|
97
|
+
:endpoint => "#{endpoint}/v1/logs"
|
|
98
|
+
)
|
|
99
|
+
::OpenTelemetry.logger_provider =
|
|
100
|
+
::OpenTelemetry::SDK::Logs::LoggerProvider.new(:resource => resource)
|
|
101
|
+
::OpenTelemetry.logger_provider.add_log_record_processor(
|
|
102
|
+
::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(logs_exporter)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@started = true
|
|
106
|
+
rescue LoadError => e
|
|
107
|
+
@started = false
|
|
108
|
+
Appsignal::Utils::StdoutAndLoggerMessage.error(
|
|
109
|
+
"Cannot configure OpenTelemetry SDK for collector mode: #{e.class}: #{e.message}"
|
|
110
|
+
)
|
|
111
|
+
rescue => e
|
|
112
|
+
@started = false
|
|
113
|
+
Appsignal::Utils::StdoutAndLoggerMessage.error(
|
|
114
|
+
"Error configuring OpenTelemetry SDK for collector mode: " \
|
|
115
|
+
"#{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}"
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether {.configure} has successfully booted the OpenTelemetry SDK
|
|
120
|
+
# for this process. Returns `false` before {.configure} runs and
|
|
121
|
+
# `false` if it ran but raised.
|
|
122
|
+
def started?
|
|
123
|
+
defined?(@started) ? @started : false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Write the current trace context onto an outgoing carrier (HTTP request,
|
|
127
|
+
# job hash, ...) with the configured propagator, so a downstream service
|
|
128
|
+
# joins the same trace.
|
|
129
|
+
#
|
|
130
|
+
# A no-op unless the SDK has booted. The carrier is injected from whatever
|
|
131
|
+
# span is current at call time, which inside an `Appsignal.instrument`
|
|
132
|
+
# block is the event's own span.
|
|
133
|
+
def inject_context(carrier)
|
|
134
|
+
if_started do
|
|
135
|
+
::OpenTelemetry.propagation.inject(carrier)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Read the trace context off an incoming Rack request env using the
|
|
140
|
+
# globally configured propagator, so an AppSignal transaction created for
|
|
141
|
+
# the request can continue the upstream trace. Returns an
|
|
142
|
+
# `OpenTelemetry::Context` (its current span is the remote parent), or
|
|
143
|
+
# `nil` when the SDK has not booted -- outside collector mode there is
|
|
144
|
+
# nothing to continue. `rack_env_getter` reads the `HTTP_*`-mangled header
|
|
145
|
+
# names Rack puts in the env.
|
|
146
|
+
def extract_rack_context(env)
|
|
147
|
+
if_started do
|
|
148
|
+
# Extract onto an empty context, not `Context.current`. The W3C
|
|
149
|
+
# extractor returns its base context unchanged when the carrier has no
|
|
150
|
+
# `traceparent`, so a request with no incoming context would inherit
|
|
151
|
+
# whatever span is ambient on the fiber. Starting empty means "no
|
|
152
|
+
# carrier" yields no parent, and the transaction starts its own trace.
|
|
153
|
+
::OpenTelemetry.propagation.extract(
|
|
154
|
+
env,
|
|
155
|
+
:context => ::OpenTelemetry::Context.empty,
|
|
156
|
+
:getter => ::OpenTelemetry::Common::Propagation.rack_env_getter
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Read the trace context off an incoming background job hash, so the
|
|
162
|
+
# transaction can link back to the enqueuer. Returns an
|
|
163
|
+
# `OpenTelemetry::Context`, or `nil` when the SDK has not booted.
|
|
164
|
+
#
|
|
165
|
+
# Reads both carriers a job can arrive with: top-level `traceparent` and
|
|
166
|
+
# `tracestate` keys, as OpenTelemetry's Sidekiq instrumentation injects
|
|
167
|
+
# them, and a nested `__otel_headers`, as its Active Job one does. Active
|
|
168
|
+
# Job puts that through its argument serializer, so it can arrive as an
|
|
169
|
+
# array of pairs rather than a hash. The nested keys win, being the more
|
|
170
|
+
# specific layer.
|
|
171
|
+
def extract_job_context(item)
|
|
172
|
+
if_started do
|
|
173
|
+
carrier = item
|
|
174
|
+
nested = otel_headers_hash(item["__otel_headers"])
|
|
175
|
+
carrier = item.merge(nested) if nested
|
|
176
|
+
# Extract onto an empty context rather than the default
|
|
177
|
+
# `Context.current`, for the same reason as `extract_rack_context`:
|
|
178
|
+
# a job with no injected trace context must not inherit an ambient
|
|
179
|
+
# span left on the fiber. Otherwise the job's transaction would link
|
|
180
|
+
# back to an unrelated leaked span instead of standing on its own.
|
|
181
|
+
::OpenTelemetry.propagation.extract(
|
|
182
|
+
carrier,
|
|
183
|
+
:context => ::OpenTelemetry::Context.empty
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
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
|
+
|
|
289
|
+
# Run `block` only when the OpenTelemetry SDK has booted (collector mode),
|
|
290
|
+
# returning its result; a no-op returning `nil` otherwise. The block can
|
|
291
|
+
# touch the OTel SDK freely -- it only runs when the SDK is loaded.
|
|
292
|
+
#
|
|
293
|
+
# This is the gate every integration's OTel-specific work goes through, so
|
|
294
|
+
# integration-specific carrier/getter/setter logic lives in the
|
|
295
|
+
# integration rather than as a bespoke helper here.
|
|
296
|
+
def if_started
|
|
297
|
+
return unless started?
|
|
298
|
+
|
|
299
|
+
yield
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# @!visibility private
|
|
303
|
+
#
|
|
304
|
+
# Test-only. Drops the started flag so subsequent tests start from a
|
|
305
|
+
# clean slate; does not touch the global `::OpenTelemetry` providers.
|
|
306
|
+
def reset!
|
|
307
|
+
@started = false
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Flush and shut down the OpenTelemetry SDK providers booted by
|
|
311
|
+
# {.configure}. Called from `Appsignal.stop` so buffered
|
|
312
|
+
# metrics/logs/spans don't get dropped on exit.
|
|
313
|
+
def shutdown
|
|
314
|
+
return unless started?
|
|
315
|
+
|
|
316
|
+
::OpenTelemetry.tracer_provider&.shutdown
|
|
317
|
+
::OpenTelemetry.meter_provider&.shutdown
|
|
318
|
+
::OpenTelemetry.logger_provider&.shutdown
|
|
319
|
+
rescue => e
|
|
320
|
+
Appsignal.internal_logger.error(
|
|
321
|
+
"Error shutting down OpenTelemetry SDK: #{e.class}: #{e.message}"
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Build the OpenTelemetry Resource that carries AppSignal config to the
|
|
326
|
+
# collector. Attributes whose underlying option is nil or an empty array
|
|
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.
|
|
330
|
+
def build_resource(config)
|
|
331
|
+
revision = config[:revision].to_s.empty? ? "unknown" : config[:revision]
|
|
332
|
+
service_name = config[:service_name].to_s.empty? ? "app" : config[:service_name]
|
|
333
|
+
host_name = config[:hostname].to_s.empty? ? "unknown" : config[:hostname]
|
|
334
|
+
|
|
335
|
+
attrs = {
|
|
336
|
+
"appsignal.config.name" => config[:name],
|
|
337
|
+
"appsignal.config.environment" => config.env,
|
|
338
|
+
"appsignal.config.push_api_key" => config[:push_api_key],
|
|
339
|
+
"appsignal.config.revision" => revision,
|
|
340
|
+
"appsignal.config.app_path" => config.root_path&.to_s,
|
|
341
|
+
"appsignal.config.platform" => config[:platform],
|
|
342
|
+
"appsignal.config.language_integration" => "ruby",
|
|
343
|
+
"service.name" => service_name,
|
|
344
|
+
"host.name" => host_name,
|
|
345
|
+
"appsignal.config.filter_attributes" => config[:filter_attributes],
|
|
346
|
+
"appsignal.config.filter_function_parameters" => config[:filter_function_parameters],
|
|
347
|
+
"appsignal.config.filter_request_query_parameters" =>
|
|
348
|
+
config[:filter_request_query_parameters],
|
|
349
|
+
"appsignal.config.filter_request_payload" => config[:filter_request_payload],
|
|
350
|
+
"appsignal.config.filter_request_session_data" => config[:filter_session_data],
|
|
351
|
+
"appsignal.config.ignore_actions" => config[:ignore_actions],
|
|
352
|
+
"appsignal.config.ignore_errors" => config[:ignore_errors],
|
|
353
|
+
"appsignal.config.ignore_logs" => config[:ignore_logs],
|
|
354
|
+
"appsignal.config.ignore_namespaces" => config[:ignore_namespaces],
|
|
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]),
|
|
361
|
+
"appsignal.config.send_function_parameters" => config[:send_function_parameters],
|
|
362
|
+
"appsignal.config.send_request_query_parameters" =>
|
|
363
|
+
config[:send_request_query_parameters],
|
|
364
|
+
"appsignal.config.send_request_payload" => config[:send_request_payload],
|
|
365
|
+
"appsignal.config.send_request_session_data" => config[:send_session_data]
|
|
366
|
+
}
|
|
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 == "" }
|
|
370
|
+
::OpenTelemetry::SDK::Resources::Resource.create(attrs)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
private
|
|
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
|
+
|
|
412
|
+
# Whether a `__otel_headers` value is the array-of-`[key, value]`-pairs
|
|
413
|
+
# shape produced by ActiveJob's argument serializer.
|
|
414
|
+
def otel_header_pairs?(value)
|
|
415
|
+
value.is_a?(Array) && value.all? { |pair| pair.is_a?(Array) && pair.size == 2 }
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# The optional OpenTelemetry gems, required lazily so users not in
|
|
419
|
+
# collector mode don't pay the load cost. A missing gem raises LoadError,
|
|
420
|
+
# caught by {.configure}.
|
|
421
|
+
def require_sdk_gems
|
|
422
|
+
require "opentelemetry/sdk"
|
|
423
|
+
require "opentelemetry-common"
|
|
424
|
+
require "opentelemetry/exporter/otlp"
|
|
425
|
+
require "opentelemetry-metrics-sdk"
|
|
426
|
+
require "opentelemetry-exporter-otlp-metrics"
|
|
427
|
+
require "opentelemetry-logs-sdk"
|
|
428
|
+
require "opentelemetry-exporter-otlp-logs"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Checks the installed OpenTelemetry gem versions against {REQUIRED_GEMS}.
|
|
432
|
+
# On a shortfall, warns and flags the SDK as not started so the caller
|
|
433
|
+
# falls back to the agent; returns whether all requirements are met.
|
|
434
|
+
def required_gem_versions_met?
|
|
435
|
+
incompatible = incompatible_gems
|
|
436
|
+
return true if incompatible.nil?
|
|
437
|
+
|
|
438
|
+
@started = false
|
|
439
|
+
Appsignal::Utils::StdoutAndLoggerMessage.warning(
|
|
440
|
+
collector_gems_warning(incompatible)
|
|
441
|
+
)
|
|
442
|
+
false
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Builds the warning shown when the OpenTelemetry gems collector mode needs
|
|
446
|
+
# are not all installed at a supported version. `incompatible` describes
|
|
447
|
+
# the gems that are installed but at a version we do not support.
|
|
448
|
+
#
|
|
449
|
+
# The message always recommends the `appsignal-opentelemetry` gem, which
|
|
450
|
+
# installs the whole set. It only lists gems when some are installed at an
|
|
451
|
+
# incompatible version, because that usually means a constraint in the
|
|
452
|
+
# bundle that the gem cannot override on its own.
|
|
453
|
+
def collector_gems_warning(incompatible)
|
|
454
|
+
message =
|
|
455
|
+
"AppSignal collector mode requires a set of OpenTelemetry gems. " \
|
|
456
|
+
"Add the `appsignal-opentelemetry` gem to your bundle to install " \
|
|
457
|
+
"them. The AppSignal agent will be used instead."
|
|
458
|
+
|
|
459
|
+
unless incompatible.empty?
|
|
460
|
+
message += "\n\nThese installed OpenTelemetry gems are not compatible " \
|
|
461
|
+
"with this AppSignal version. Update them or remove a version " \
|
|
462
|
+
"constraint:\n"
|
|
463
|
+
message += incompatible.map { |line| "- #{line}" }.join("\n")
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
message
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Checks the installed OpenTelemetry gems against {REQUIRED_GEMS}. Returns
|
|
470
|
+
# `nil` when every required gem is installed at a supported version.
|
|
471
|
+
# Otherwise returns the descriptions of gems that are installed but at a
|
|
472
|
+
# version we do not support. That list is empty when the only problem is
|
|
473
|
+
# that some required gems are not installed at all.
|
|
474
|
+
def incompatible_gems
|
|
475
|
+
missing = false
|
|
476
|
+
incompatible = []
|
|
477
|
+
|
|
478
|
+
REQUIRED_GEMS.each do |name, constraints|
|
|
479
|
+
spec = Gem.loaded_specs[name]
|
|
480
|
+
requirement = Gem::Requirement.new(*constraints)
|
|
481
|
+
|
|
482
|
+
if spec.nil?
|
|
483
|
+
missing = true
|
|
484
|
+
elsif !requirement.satisfied_by?(spec.version)
|
|
485
|
+
incompatible << "#{name} #{spec.version} (requires #{requirement})"
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
return nil if !missing && incompatible.empty?
|
|
490
|
+
|
|
491
|
+
incompatible
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|