foam-otel 3.0.0.alpha.9 → 3.0.0.alpha.12
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/README.md +58 -3
- data/lib/foam/otel/internal/aws_lambda.rb +1 -1
- data/lib/foam/otel/internal/clients/excon.rb +1 -1
- data/lib/foam/otel/internal/clients/faraday.rb +1 -1
- data/lib/foam/otel/internal/clients/http_client.rb +1 -1
- data/lib/foam/otel/internal/endpoint.rb +1 -1
- data/lib/foam/otel/internal/exporters.rb +33 -19
- data/lib/foam/otel/internal/init.rb +6 -2
- data/lib/foam/otel/internal/instrumentations.rb +10 -2
- data/lib/foam/otel/internal/propagation.rb +78 -2
- data/lib/foam/otel/internal/que.rb +45 -0
- data/lib/foam/otel/internal/rails_middleware.rb +1 -1
- data/lib/foam/otel/internal/reinject.rb +4 -3
- data/lib/foam/otel/internal/state.rb +6 -0
- data/lib/foam/otel/version.rb +1 -1
- data/lib/foam/otel.rb +6 -0
- metadata +19 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b67fa02fd12d317a0bceff3c596f63d928b0187995d02e95191f9ecb05b7383e
|
|
4
|
+
data.tar.gz: 6933cac173a8df6fa83fe474f06fd59569d0da61eb4a78f16919c695561b63af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7080090c2024fda65b0bf8d76edbe311961ae7c26814cd3154c11e9c7735e9730c10e100468d1046acc94e1cb2b2d1518b50cdc72b631201187b271b167c722
|
|
7
|
+
data.tar.gz: 184764049f1395f94f270cec7054138cfd78c8f6b40c93c3cafafdcb4cd7a3754bfa1eed0851e47d1f96258f10e1dea51889624862e07f86579e42d73a26353a
|
data/README.md
CHANGED
|
@@ -227,7 +227,7 @@ Do not combine `init` and an ingest component for the same signal. The ingest co
|
|
|
227
227
|
|
|
228
228
|
### `inject_trace_context(carrier)`
|
|
229
229
|
|
|
230
|
-
Writes the W3C `traceparent`, `tracestate`, and `baggage` headers for the current span into `carrier`, a hash of string keys.
|
|
230
|
+
Writes the W3C `traceparent`, `tracestate`, and `baggage` headers for the current span into `carrier`, a hash of string keys. A `tracestate` the carrier already holds is kept, since Foam adds no entry of its own and another SDK may have added one.
|
|
231
231
|
|
|
232
232
|
Use it when the app sends a message itself (a Kafka record, a queue payload, a custom socket). The HTTP instrumentations already inject these headers; do not call it on ordinary HTTP requests. Without an active span it writes only `baggage`, and only when baggage is set.
|
|
233
233
|
|
|
@@ -252,7 +252,18 @@ end
|
|
|
252
252
|
|
|
253
253
|
`inject_trace_context` and `extract_trace_context` always use the W3C `traceparent`, `tracestate`, and `baggage` headers, whatever propagator holds the global slot. Foam does not read `OTEL_PROPAGATORS`. An app that needs a different propagator registers it before `init`, and Foam leaves it in place.
|
|
254
254
|
|
|
255
|
-
On outbound requests, Foam writes its propagation headers after any other SDK's. Another SDK's own propagation headers are left as written.
|
|
255
|
+
On outbound requests, Foam writes its propagation headers after any other SDK's. Another SDK's own propagation headers are left as written, and Foam's baggage entries are merged into the members it wrote to `baggage`. These writes go through the propagator in the global slot.
|
|
256
|
+
|
|
257
|
+
### `propagator`
|
|
258
|
+
|
|
259
|
+
The propagator `inject_trace_context` and `extract_trace_context` use, for an app that sets its own composite in the global slot. List it last, so a caller's `traceparent` wins over another SDK's trace header:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([
|
|
263
|
+
other_sdk_propagator,
|
|
264
|
+
Foam.propagator
|
|
265
|
+
])
|
|
266
|
+
```
|
|
256
267
|
|
|
257
268
|
### `set_baggage(key, value)`
|
|
258
269
|
|
|
@@ -432,7 +443,7 @@ Traces, metrics, and logs are flushed before the handler returns (including fail
|
|
|
432
443
|
|
|
433
444
|
## Pre-forking servers and threads
|
|
434
445
|
|
|
435
|
-
|
|
446
|
+
Call `init` once, in the process that forks: the master of Puma in cluster mode with `preload_app!` (the default since Puma 7), the master of Unicorn with `preload_app true`, or a worker that forks a child per job. Each child exports its own traces, logs, and metrics, carrying its own `process.pid`.
|
|
436
447
|
|
|
437
448
|
The current span and baggage are stored per fiber. A `Thread.new` or `Fiber.new` starts with none, so spans started inside it begin a new trace. To continue the trace, capture the context first and make it current inside the thread:
|
|
438
449
|
|
|
@@ -485,6 +496,50 @@ Foam.init(
|
|
|
485
496
|
)
|
|
486
497
|
```
|
|
487
498
|
|
|
499
|
+
### Sentry
|
|
500
|
+
|
|
501
|
+
Sentry sends its events to the DSN's host over HTTP, which Foam would otherwise record as spans. Pass that address and Sentry's request header to `init`. This works whether Sentry sends inline or from its background worker:
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
dsn = Sentry.configuration.dsn
|
|
505
|
+
|
|
506
|
+
Foam.init(
|
|
507
|
+
name: "checkout-api", environment: "production", token: ENV["FOAM_OTEL_TOKEN"],
|
|
508
|
+
ignored_outbound_hosts: ["#{dsn.host}:#{dsn.port}"],
|
|
509
|
+
ignored_outbound_headers: ["X-Sentry-Auth"]
|
|
510
|
+
)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
If the app puts Sentry's propagator in the global slot, through `sentry-opentelemetry`'s `config.otlp.setup_propagator` or `OpenTelemetry.propagation = Sentry::OpenTelemetry::Propagator.new`, outbound requests carry `sentry-trace` but no `traceparent`, so Foam's traces break between services. Once both `init` and that setting have run, wrap Sentry's propagator with Foam's:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([
|
|
517
|
+
OpenTelemetry.propagation,
|
|
518
|
+
Foam.propagator
|
|
519
|
+
])
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### New Relic
|
|
523
|
+
|
|
524
|
+
New Relic's agent sends its data to its collector over HTTP, and probes the AWS, Azure and GCP metadata endpoints when it connects, which Foam would otherwise record as spans. Pass the collector's address and the metadata hosts to `init`. The agent's requests carry no header of its own, so there is none to pass:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
collector = "#{NewRelic::Agent.config[:host]}:#{NewRelic::Agent.config[:port]}"
|
|
528
|
+
|
|
529
|
+
Foam.init(
|
|
530
|
+
name: "checkout-api", environment: "production", token: ENV["FOAM_OTEL_TOKEN"],
|
|
531
|
+
ignored_outbound_hosts: [
|
|
532
|
+
collector,
|
|
533
|
+
"169.254.169.254", # AWS and Azure metadata
|
|
534
|
+
"metadata.google.internal" # GCP metadata
|
|
535
|
+
]
|
|
536
|
+
)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Start New Relic before `init`, as Rails does through the agent's Railtie. If the agent instruments Net::HTTP in chain mode and starts after `init`, every Net::HTTP request raises `SystemStackError`. The agent uses chain mode when `instrumentation.net_http` is `chain`, or on its own next to ScoutApm, Rack::MiniProfiler, or Airbrake before 10.0.2. Where the order cannot change, set `instrumentation.net_http` to `prepend`.
|
|
540
|
+
|
|
541
|
+
If the app turns on New Relic's OpenTelemetry bridge (`opentelemetry.enabled`), New Relic takes the tracer provider and propagator, and Foam records no traces. Logs and metrics still reach Foam.
|
|
542
|
+
|
|
488
543
|
## Redaction
|
|
489
544
|
|
|
490
545
|
Foam masks the values of sensitive keys in everything it exports: span, span event, and span link attributes; log bodies and attributes; metric data point attributes; and resource attributes. A built-in list of credential and personal-data keys always applies, matched by exact name or by a sensitive segment of the key (`legacy_api_key_2`, `stripeToken`). Keys match after normalization: case, `-`, `.`, and camelCase boundaries are folded, so `apiKey`, `api-key`, and `API_KEY` are one key. Header attributes match on the header name. SQL statements arrive from the database instrumentations with every literal replaced with `?` (see Instrumentation) and are redacted like any other string. Captured HTTP bodies are masked by content type with the same keys. When masking a body fails, the whole body is replaced with `********`.
|
|
@@ -17,7 +17,7 @@ module Foam
|
|
|
17
17
|
DECOMPRESSED = %w[gzip x-gzip deflate].freeze
|
|
18
18
|
|
|
19
19
|
def self.install
|
|
20
|
-
return unless Instrumentations.
|
|
20
|
+
return unless Instrumentations.installed?(INSTRUMENTATION)
|
|
21
21
|
|
|
22
22
|
stack = ::Excon.defaults[:middlewares]
|
|
23
23
|
return if stack.include?(self)
|
|
@@ -13,7 +13,7 @@ module Foam
|
|
|
13
13
|
INFLATED_ENCODINGS = %w[gzip x-gzip deflate].freeze
|
|
14
14
|
|
|
15
15
|
def self.install
|
|
16
|
-
return unless Instrumentations.
|
|
16
|
+
return unless Instrumentations.installed?(INSTRUMENTATION)
|
|
17
17
|
|
|
18
18
|
::HTTPClient::Session.prepend(self)
|
|
19
19
|
end
|
|
@@ -12,12 +12,16 @@ module Foam
|
|
|
12
12
|
# A copy has the before_send hook's edits, Foam's identity merged over its resource, and every
|
|
13
13
|
# sensitive value masked.
|
|
14
14
|
module ExportCopies
|
|
15
|
-
# Explicit path and
|
|
15
|
+
# Explicit path, headers, and compression so the SDK does not derive them from OTEL_* env.
|
|
16
16
|
def initialize(token:, redactor:, before_send: BeforeSend::NONE, stamp: {})
|
|
17
17
|
@redactor = redactor
|
|
18
18
|
@before_send = before_send
|
|
19
19
|
@stamp = OpenTelemetry::SDK::Resources::Resource.create(stamp)
|
|
20
|
-
super(
|
|
20
|
+
super(
|
|
21
|
+
endpoint: Endpoint.url(self.class::PATH),
|
|
22
|
+
headers: Constants.bearer_headers(token),
|
|
23
|
+
compression: "gzip"
|
|
24
|
+
)
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
# The encoder groups items by resource object, so items sharing one must share its
|
|
@@ -50,12 +54,6 @@ module Foam
|
|
|
50
54
|
def redacted_attributes(attributes)
|
|
51
55
|
attributes && @redactor.redact_attributes(attributes)
|
|
52
56
|
end
|
|
53
|
-
|
|
54
|
-
# The encoder derives a dropped count as the total minus what is present, so the copy's total
|
|
55
|
-
# must follow its size to keep the SDK's dropped count.
|
|
56
|
-
def total_recorded(original_total, original_list, list)
|
|
57
|
-
original_total - original_list&.size.to_i + list.size
|
|
58
|
-
end
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
class TraceExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
|
@@ -70,8 +68,31 @@ module Foam
|
|
|
70
68
|
}.freeze
|
|
71
69
|
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
72
70
|
|
|
71
|
+
# The upstream span encoder, with a hash the hook added exported as a nested map, as it is in
|
|
72
|
+
# a log attribute. The upstream encoder has no case for a hash and sends an empty value.
|
|
73
|
+
module Encoder
|
|
74
|
+
include OpenTelemetry::Exporter::OTLP::Common
|
|
75
|
+
extend self
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def as_otlp_any_value(value)
|
|
80
|
+
return super unless value.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
values = value.map { |key, item| as_otlp_key_value(key.to_s, item) }
|
|
83
|
+
Opentelemetry::Proto::Common::V1::AnyValue.new(
|
|
84
|
+
kvlist_value: Opentelemetry::Proto::Common::V1::KeyValueList.new(values: values)
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
73
89
|
private
|
|
74
90
|
|
|
91
|
+
# The inherited method calls the upstream encoder directly.
|
|
92
|
+
def encode(span_data)
|
|
93
|
+
Encoder.as_encoded_etsr(span_data)
|
|
94
|
+
end
|
|
95
|
+
|
|
75
96
|
# The span as the hook left it, with its own, event, and link attributes and its status redacted; nil when
|
|
76
97
|
# the hook dropped it.
|
|
77
98
|
def redacted_copy(span, resource, &on_raise)
|
|
@@ -142,15 +163,10 @@ module Foam
|
|
|
142
163
|
)
|
|
143
164
|
end
|
|
144
165
|
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
values = value.map { |key, item| as_otlp_key_value(key.to_s, item) }
|
|
151
|
-
Opentelemetry::Proto::Common::V1::AnyValue.new(
|
|
152
|
-
kvlist_value: Opentelemetry::Proto::Common::V1::KeyValueList.new(values: values)
|
|
153
|
-
)
|
|
166
|
+
# The encoder derives a dropped count as the total minus what is present, so the copy's total
|
|
167
|
+
# must follow its size to keep the SDK's dropped count.
|
|
168
|
+
def total_recorded(original_total, original_list, list)
|
|
169
|
+
original_total - original_list&.size.to_i + list.size
|
|
154
170
|
end
|
|
155
171
|
end
|
|
156
172
|
|
|
@@ -172,8 +188,6 @@ module Foam
|
|
|
172
188
|
copy.severity_text = event[:severity_text]
|
|
173
189
|
copy.severity_number = event[:severity_number]
|
|
174
190
|
copy.attributes = redacted_attributes(event[:attributes])
|
|
175
|
-
copy.total_recorded_attributes =
|
|
176
|
-
total_recorded(record.total_recorded_attributes, record.attributes, copy.attributes)
|
|
177
191
|
copy.resource = resource
|
|
178
192
|
end
|
|
179
193
|
end
|
|
@@ -36,7 +36,7 @@ module Foam
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# A second configure would install a second pipeline next to the first.
|
|
39
|
-
if State.
|
|
39
|
+
if State.initialized?
|
|
40
40
|
Report.call(severity: :warn, message: "SDK already initialized", **identity)
|
|
41
41
|
return
|
|
42
42
|
end
|
|
@@ -90,7 +90,7 @@ module Foam
|
|
|
90
90
|
# there while it boots, which would make every app log line raise. LoggerBridge is
|
|
91
91
|
# prepended after the instrumentation's patch, so it sits in front of it and drops just
|
|
92
92
|
# the record whose emit failed.
|
|
93
|
-
::Logger.prepend(LoggerBridge) if Instrumentations.
|
|
93
|
+
::Logger.prepend(LoggerBridge) if Instrumentations.installed?(Instrumentations::LOGGER)
|
|
94
94
|
|
|
95
95
|
# The client patches set the safe headers on every client span and, in advanced mode,
|
|
96
96
|
# capture bodies too. Bodies are redacted by the same rules as attributes, so only
|
|
@@ -102,6 +102,10 @@ module Foam
|
|
|
102
102
|
# Foam's logger provider where it flushes the tracer and meter providers.
|
|
103
103
|
Safely.call("aws_lambda") { AwsLambda.install }
|
|
104
104
|
|
|
105
|
+
# Leaves out a propagation header Que would refuse as a job tag, so the app's enqueue
|
|
106
|
+
# does not raise on a long baggage.
|
|
107
|
+
Safely.call("que") { Que.install }
|
|
108
|
+
|
|
105
109
|
# A Rails app cannot add the middleware in config.ru the way a plain Rack app does, so
|
|
106
110
|
# Foam adds it through Rails itself.
|
|
107
111
|
Safely.call("rails middleware") { RailsMiddleware.install } unless options[:network_capture] == :off
|
|
@@ -104,7 +104,7 @@ module Foam
|
|
|
104
104
|
|
|
105
105
|
# Labels of the instrumentations that installed, in table order, then the caller's entries.
|
|
106
106
|
def installed(config_map)
|
|
107
|
-
config_map.filter_map { |name, _config| label(short_name(name)) if
|
|
107
|
+
config_map.filter_map { |name, _config| label(short_name(name)) if installed?(name) }
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
# Labels of the instrumentations whose library arrived after init: present, enabled, and
|
|
@@ -113,8 +113,10 @@ module Foam
|
|
|
113
113
|
config_map.filter_map { |name, config| label(short_name(name)) if late?(instance(name), config) }
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# An entry another SDK deleted from the registry after init (see installed?) has no instance to
|
|
117
|
+
# ask, so it counts as neither installed nor late.
|
|
116
118
|
def late?(instance, config)
|
|
117
|
-
!instance.installed? && instance.present? && instance.enabled?(config) && compatible?(instance)
|
|
119
|
+
!instance.nil? && !instance.installed? && instance.present? && instance.enabled?(config) && compatible?(instance)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# A compatibility block reads the library's version constant, which can raise on an odd install.
|
|
@@ -128,6 +130,12 @@ module Foam
|
|
|
128
130
|
OpenTelemetry::Instrumentation.registry.lookup(name)
|
|
129
131
|
end
|
|
130
132
|
|
|
133
|
+
# Another SDK can remove entries from the registry, as New Relic's OpenTelemetry bridge does
|
|
134
|
+
# for the libraries it covers itself, so a missing entry reads as not installed.
|
|
135
|
+
def installed?(name)
|
|
136
|
+
instance(name)&.installed? || false
|
|
137
|
+
end
|
|
138
|
+
|
|
131
139
|
# The table's short name, or one derived the same way for an instrumentation outside the bundle.
|
|
132
140
|
def short_name(name)
|
|
133
141
|
TABLE.fetch(name) do
|
|
@@ -4,10 +4,86 @@ module Foam
|
|
|
4
4
|
module Otel
|
|
5
5
|
module Internal
|
|
6
6
|
module Propagation
|
|
7
|
+
# Writes W3C trace context, as OpenTelemetry's propagator does, but does not replace a
|
|
8
|
+
# tracestate header that is already on the outgoing request. Foam has no tracestate entry of
|
|
9
|
+
# its own. Another SDK, such as New Relic or Datadog, can write tracestate first and put its
|
|
10
|
+
# entry (@nr, dd) before the caller's entries. If Foam wrote tracestate again, that entry
|
|
11
|
+
# would be lost. Foam always writes traceparent.
|
|
12
|
+
module KeptTracestate
|
|
13
|
+
UPSTREAM = OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator
|
|
14
|
+
|
|
15
|
+
# Passes every header through but tracestate.
|
|
16
|
+
SkipTracestate = Struct.new(:setter) do
|
|
17
|
+
def set(carrier, key, value)
|
|
18
|
+
setter.set(carrier, key, value) unless key == "tracestate"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def inject(carrier, context:, setter:)
|
|
25
|
+
setter = SkipTracestate.new(setter) if header(carrier)
|
|
26
|
+
UPSTREAM.inject(carrier, context: context, setter: setter)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def extract(carrier, **) = UPSTREAM.extract(carrier, **)
|
|
30
|
+
|
|
31
|
+
def fields = UPSTREAM.fields
|
|
32
|
+
|
|
33
|
+
# Reads the carrier's tracestate header as one string, or nil when it has none. Carriers
|
|
34
|
+
# differ: HTTPClient returns an array of header values, the others a string. A list carrier,
|
|
35
|
+
# such as Que's job tags, cannot be read by header name, so there is nothing to keep.
|
|
36
|
+
def header(carrier)
|
|
37
|
+
return if carrier.is_a?(Array)
|
|
38
|
+
|
|
39
|
+
value = Array(carrier["tracestate"]).join(",")
|
|
40
|
+
value unless value.empty?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The W3C baggage header is shared between SDKs: Sentry, for one, writes its sampling context
|
|
45
|
+
# there as sentry-* members. OpenTelemetry's baggage propagator overwrites the whole header
|
|
46
|
+
# with Foam's entries, so whichever SDK wrote first loses its members. This wraps it to merge
|
|
47
|
+
# instead, keeping the other SDK's members and letting Foam's value win a shared key.
|
|
48
|
+
module MergingBaggage
|
|
49
|
+
UPSTREAM = OpenTelemetry::Baggage::Propagation.text_map_propagator
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
def inject(carrier, context:, setter:)
|
|
54
|
+
theirs = header(carrier)
|
|
55
|
+
UPSTREAM.inject(carrier, context: context, setter: setter)
|
|
56
|
+
ours = header(carrier)
|
|
57
|
+
return if theirs.nil? || ours == theirs
|
|
58
|
+
|
|
59
|
+
keys = ours.split(",").map { |member| key(member) }
|
|
60
|
+
kept = theirs.split(",").map(&:strip).reject { |member| member.empty? || keys.include?(key(member)) }
|
|
61
|
+
setter.set(carrier, "baggage", [ours, *kept].join(","))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract(carrier, **) = UPSTREAM.extract(carrier, **)
|
|
65
|
+
|
|
66
|
+
def fields = UPSTREAM.fields
|
|
67
|
+
|
|
68
|
+
# Reads the carrier's baggage header as one string, or nil when it has none. inject reads it
|
|
69
|
+
# before and after the upstream write, to tell the other SDK's members from Foam's. Carriers
|
|
70
|
+
# differ: HTTPClient returns an array of header values, the others a string. A list carrier,
|
|
71
|
+
# such as Que's job tags, is written through its setter and cannot be read by header name,
|
|
72
|
+
# so there is nothing to merge.
|
|
73
|
+
def header(carrier)
|
|
74
|
+
return if carrier.is_a?(Array)
|
|
75
|
+
|
|
76
|
+
value = Array(carrier["baggage"]).join(",")
|
|
77
|
+
value unless value.empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def key(member) = member.split("=", 2).first.strip
|
|
81
|
+
end
|
|
82
|
+
|
|
7
83
|
# Foam's own W3C propagator, so the helpers work the same whoever configured the global one.
|
|
8
84
|
PROPAGATOR = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([
|
|
9
|
-
|
|
10
|
-
|
|
85
|
+
KeptTracestate,
|
|
86
|
+
MergingBaggage
|
|
11
87
|
])
|
|
12
88
|
|
|
13
89
|
module_function
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Foam's addition to the que instrumentation, prepended on its TagSetter by init after
|
|
7
|
+
# installation. The instrumentation writes each propagation header into the job's tags as
|
|
8
|
+
# "key:value", and Que refuses to enqueue a job with a tag longer than MAXIMUM_TAG_LENGTH or
|
|
9
|
+
# more than MAXIMUM_TAGS_COUNT tags, so a long baggage, the app's or another SDK's, would make
|
|
10
|
+
# the app's enqueue raise. The setter leaves out a header Que would refuse, warning once, and
|
|
11
|
+
# the job enqueues without it.
|
|
12
|
+
module Que
|
|
13
|
+
INSTRUMENTATION = "OpenTelemetry::Instrumentation::Que"
|
|
14
|
+
|
|
15
|
+
def self.install
|
|
16
|
+
return unless Instrumentations.installed?(INSTRUMENTATION)
|
|
17
|
+
|
|
18
|
+
::OpenTelemetry::Instrumentation::Que::TagSetter.singleton_class.prepend(TagSetter)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.fits?(tags, tag)
|
|
22
|
+
tag.length <= ::Que::Job::MAXIMUM_TAG_LENGTH && tags.length < ::Que::Job::MAXIMUM_TAGS_COUNT
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.warn_left_out(key)
|
|
26
|
+
return if @warned
|
|
27
|
+
|
|
28
|
+
@warned = true
|
|
29
|
+
Diagnostics.warn(
|
|
30
|
+
"propagation header #{key} left out of a Que job's tags; Que allows at most " \
|
|
31
|
+
"#{::Que::Job::MAXIMUM_TAGS_COUNT} tags of #{::Que::Job::MAXIMUM_TAG_LENGTH} characters"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module TagSetter
|
|
36
|
+
def set(carrier, key, value)
|
|
37
|
+
return super if Que.fits?(carrier, "#{key}:#{value}")
|
|
38
|
+
|
|
39
|
+
Que.warn_left_out(key)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -46,7 +46,7 @@ module Foam
|
|
|
46
46
|
# it builds the real stack late in initialize!. Anchored on the OpenTelemetry class, not
|
|
47
47
|
# position 0, so another SDK inserting at the top cannot come between the two.
|
|
48
48
|
def insert(app)
|
|
49
|
-
return unless Instrumentations.
|
|
49
|
+
return unless Instrumentations.installed?(ACTION_PACK)
|
|
50
50
|
|
|
51
51
|
otel = Instrumentations.instance(RACK).middleware_args.first
|
|
52
52
|
app.middleware.insert_after(otel, Foam::Rack::Middleware)
|
|
@@ -11,8 +11,9 @@ module Foam
|
|
|
11
11
|
module Reinject
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
-
# The global propagator, the one the instrumentation used
|
|
15
|
-
# the
|
|
14
|
+
# The global propagator, the one the instrumentation used: whoever holds the slot, before or
|
|
15
|
+
# after init, decides the headers. Without a span there is nothing the instrumentation
|
|
16
|
+
# wrote, so nothing is written here either.
|
|
16
17
|
def call(carrier, context: OpenTelemetry::Context.current)
|
|
17
18
|
return if OwnRequest.current? || !OpenTelemetry::Trace.current_span(context).context.valid?
|
|
18
19
|
|
|
@@ -108,7 +109,7 @@ module Foam
|
|
|
108
109
|
|
|
109
110
|
def install
|
|
110
111
|
HOOKS.each do |hook, (klass, instrumentation)|
|
|
111
|
-
next unless instrumentation.nil? || Instrumentations.
|
|
112
|
+
next unless instrumentation.nil? || Instrumentations.installed?(instrumentation)
|
|
112
113
|
|
|
113
114
|
Object.const_get(klass).prepend(hook)
|
|
114
115
|
end
|
|
@@ -35,6 +35,12 @@ module Foam
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Whether init has completed, without reading the live slots as current does, which warns
|
|
39
|
+
# about a slot another SDK took since.
|
|
40
|
+
def initialized?
|
|
41
|
+
@mutex.synchronize { @initialized }
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
# Gate for the helpers: the hash is replaced whole, never mutated, so no lock.
|
|
39
45
|
def active?(signal)
|
|
40
46
|
@signals[signal] != :none
|
data/lib/foam/otel/version.rb
CHANGED
data/lib/foam/otel.rb
CHANGED
|
@@ -23,6 +23,7 @@ require_relative "otel/internal/clients/excon"
|
|
|
23
23
|
require_relative "otel/internal/clients/http_client"
|
|
24
24
|
require_relative "otel/internal/clients/faraday"
|
|
25
25
|
require_relative "otel/internal/aws_lambda"
|
|
26
|
+
require_relative "otel/internal/que"
|
|
26
27
|
require_relative "otel/internal/rails_middleware"
|
|
27
28
|
require_relative "otel/internal/before_send"
|
|
28
29
|
require_relative "otel/internal/instrumentations"
|
|
@@ -129,6 +130,11 @@ module Foam
|
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
133
|
+
# The propagator the two helpers above use, for an app that composes its own.
|
|
134
|
+
def propagator
|
|
135
|
+
Internal::Propagation::PROPAGATOR
|
|
136
|
+
end
|
|
137
|
+
|
|
132
138
|
def set_baggage(key, value)
|
|
133
139
|
Internal::Safely.call("set_baggage") do
|
|
134
140
|
Internal::Propagation.set_baggage(key, value)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foam-otel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.0.alpha.
|
|
4
|
+
version: 3.0.0.alpha.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foam
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: opentelemetry-exporter-otlp
|
|
@@ -16,56 +16,56 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
19
|
+
version: 0.37.0
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.
|
|
26
|
+
version: 0.37.0
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: opentelemetry-exporter-otlp-logs
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.6.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
40
|
+
version: 0.6.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: opentelemetry-exporter-otlp-metrics
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.
|
|
47
|
+
version: 0.13.0
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.
|
|
54
|
+
version: 0.13.0
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: opentelemetry-instrumentation-all
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.
|
|
61
|
+
version: 0.97.0
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.
|
|
68
|
+
version: 0.97.0
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: opentelemetry-instrumentation-logger
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,56 +86,56 @@ dependencies:
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: 0.
|
|
89
|
+
version: 0.8.0
|
|
90
90
|
type: :runtime
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: 0.
|
|
96
|
+
version: 0.8.0
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: opentelemetry-logs-api
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: 0.
|
|
103
|
+
version: 0.6.0
|
|
104
104
|
type: :runtime
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: 0.
|
|
110
|
+
version: 0.6.0
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
112
|
name: opentelemetry-metrics-api
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: 0.
|
|
117
|
+
version: 0.9.0
|
|
118
118
|
type: :runtime
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: 0.
|
|
124
|
+
version: 0.9.0
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
126
|
name: opentelemetry-metrics-sdk
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
128
128
|
requirements:
|
|
129
129
|
- - "~>"
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: 0.
|
|
131
|
+
version: 0.20.0
|
|
132
132
|
type: :runtime
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: 0.
|
|
138
|
+
version: 0.20.0
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
140
|
name: opentelemetry-sdk
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -204,6 +204,7 @@ files:
|
|
|
204
204
|
- lib/foam/otel/internal/own_request.rb
|
|
205
205
|
- lib/foam/otel/internal/patch_resolver.rb
|
|
206
206
|
- lib/foam/otel/internal/propagation.rb
|
|
207
|
+
- lib/foam/otel/internal/que.rb
|
|
207
208
|
- lib/foam/otel/internal/rack/body_tee.rb
|
|
208
209
|
- lib/foam/otel/internal/rack/input_tee.rb
|
|
209
210
|
- lib/foam/otel/internal/rails_middleware.rb
|