foam-otel 3.0.0.alpha.5 → 3.0.0.alpha.6
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 +28 -1
- data/lib/foam/otel/internal/ignored_headers.rb +31 -0
- data/lib/foam/otel/internal/ignored_hosts.rb +20 -10
- data/lib/foam/otel/internal/init.rb +24 -3
- data/lib/foam/otel/internal/logger_bridge.rb +19 -0
- data/lib/foam/otel/internal/patch_resolver.rb +152 -0
- data/lib/foam/otel/internal/reinject.rb +119 -0
- data/lib/foam/otel/version.rb +1 -1
- data/lib/foam/otel.rb +6 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4458e9959661525a33f70692beaeba2f32df91a68b01b7e611ec2ae2e2901b39
|
|
4
|
+
data.tar.gz: bb9127c1d897dec9f1a8268a338382fa1ffdfe772918abdf920d413ff10b986e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d10e3d644eee2036278ae2c5310fc1afd33ec9acda7b27189247a0cb1dd88e1ebc433538cca528567278f97fa9103319a2e573d97f2ca201e24338b50f1c2764
|
|
7
|
+
data.tar.gz: 96eefe317fbf6eb8906e30242618fe69d90d303f0b3d2e74ceb008c968adc94884a39279b0a887e8e1ab7e5c1b4595dfd8aaf6e31eaeca123b451a002438f52f
|
data/README.md
CHANGED
|
@@ -134,6 +134,7 @@ Foam.init(
|
|
|
134
134
|
disable_log_sending: false,
|
|
135
135
|
network_capture: :basic,
|
|
136
136
|
ignored_outbound_hosts: [],
|
|
137
|
+
ignored_outbound_headers: [],
|
|
137
138
|
redact: nil,
|
|
138
139
|
before_send: nil
|
|
139
140
|
)
|
|
@@ -150,7 +151,8 @@ Foam.init(
|
|
|
150
151
|
- `additional_span_processors`, `additional_log_record_processors`, `additional_metric_readers` - the app's own OpenTelemetry components, added to Foam's providers after Foam's own exporters.
|
|
151
152
|
- `additional_resource_attributes` - attributes merged into the resource of every signal. `service.name`, `service.version`, and `deployment.environment.name` come from the options above and `service.instance.id` from Foam; a value for one of them here is reported on stderr and ignored.
|
|
152
153
|
- `additional_instrumentations` - instrumentations to install next to the bundle. An entry is an instrumentation's registry name, which is its class name without the trailing `::Instrumentation` (`"OpenTelemetry::Instrumentation::Logger"`), or a `[name, config]` pair to install it with options (`["OpenTelemetry::Instrumentation::PG", {db_statement: :include}]`). Require the instrumentation's gem before `init`; an entry whose instrumentation is not loaded fails `init`. An entry naming a bundled instrumentation replaces Foam's configuration for it.
|
|
153
|
-
- `ignored_outbound_hosts` - skips tracing of outbound HTTP requests to these
|
|
154
|
+
- `ignored_outbound_hosts` - skips tracing of outbound HTTP requests to these hosts, each written `host` or `host:port`. The endpoint host is always skipped. Use it for hosts whose traffic would drown the traces, such as a sidecar health check on `localhost`, another SDK's agent on `127.0.0.1:8126`, or another vendor's OTLP ingest host. Hosts are compared as written; `localhost` and `127.0.0.1` are different hosts, and an entry with a port matches that port only. A request to a skipped host still carries a `traceparent` marked not sampled, so an OpenTelemetry service behind it records nothing for that request.
|
|
155
|
+
- `ignored_outbound_headers` - skips tracing of outbound Net::HTTP requests carrying any of these request headers, compared case-insensitively. Another SDK's transport stamps its own header on every request to its backend, such as `Datadog-Meta-Tracer-Version`, which tells that traffic apart when it has no host to ignore, as over a Unix socket. A connection opened before the request, as `Net::HTTP.start` does, has its connect span before any header is seen, so over TCP ignore the host as well.
|
|
154
156
|
- `disable_log_sending` - `true` stops shipping the app's `Logger` lines to Foam. `Foam.log` is unaffected. See Loggers.
|
|
155
157
|
- `network_capture` - `:off`, `:basic`, or `:advanced`. Use `:advanced` unless security and compliance rule it out. See Network capture.
|
|
156
158
|
- `redact` - keys to mask beyond the built-in list, as `{secrets: [...], pii: [...]}`. A value under a `secrets` key keeps its last four characters when it is 12 characters or longer; a value under a `pii` key is fully masked. See Redaction.
|
|
@@ -254,6 +256,8 @@ end
|
|
|
254
256
|
|
|
255
257
|
`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.
|
|
256
258
|
|
|
259
|
+
On the outbound requests Foam instruments, the propagation headers are written last, at the transport, after any other SDK's: Net::HTTP, HTTPClient, Excon, http.rb, Ethon, gRPC, and Sidekiq job payloads, and Faraday, RestClient, and Typhoeus through those. Another SDK's own propagation headers are left as written.
|
|
260
|
+
|
|
257
261
|
### `set_baggage(key, value)`
|
|
258
262
|
|
|
259
263
|
Adds a key-value pair to the current context's baggage. Baggage travels with the trace context to every downstream service, where `baggage(key)` reads it; it is not written onto spans. The value lasts for the rest of the current scope, for example the rest of the request when called from a Rails controller, and is dropped when that scope ends. Outside any span or context scope the call is ignored and warned about once, so a value can never outlive the work it belongs to. No-op before `init`.
|
|
@@ -436,6 +440,8 @@ Covered:
|
|
|
436
440
|
|
|
437
441
|
Not covered, since they do not build on `Logger`: Semantic Logger, `logging`, log4r, yell, and Puma's own server lines. Use `Foam.log` for a record from these. Semantic Logger also ships an `:open_telemetry` appender. `puts` and direct writes to `$stdout` or `$stderr` are never captured, nor is a `Logger` whose device is `nil` or `File::NULL`.
|
|
438
442
|
|
|
443
|
+
Lines another SDK writes through its own `Logger` are shipped like the app's; `before_send` can drop them.
|
|
444
|
+
|
|
439
445
|
If another SDK owns logs, set `disable_log_sending: true` so records do not arrive twice, and attach `create_foam_ingest_log_record_processor` to that SDK's provider. `Foam.log` delivers either way.
|
|
440
446
|
|
|
441
447
|
Secrets in a log line are masked before export. See Redaction.
|
|
@@ -459,6 +465,27 @@ Thread.new do
|
|
|
459
465
|
end
|
|
460
466
|
```
|
|
461
467
|
|
|
468
|
+
## Other backends installed
|
|
469
|
+
|
|
470
|
+
An app may already run another vendor's SDK, such as Datadog, which records its own telemetry and sends it to that vendor. Do not change its configuration.
|
|
471
|
+
|
|
472
|
+
Call `init` after configuring the existing SDK. If the app already configures OpenTelemetry itself with `OpenTelemetry::SDK.configure`, instead attach the ingest helpers to the app's providers and do not call `init`; see `create_foam_ingest_span_processor` under Public API.
|
|
473
|
+
|
|
474
|
+
### Datadog
|
|
475
|
+
|
|
476
|
+
Datadog sends its data to its agent over HTTP, which Foam would otherwise record as spans. Pass the agent's address and Datadog's request header to `init`. This works whether the agent is on TCP or a Unix socket:
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
agent = Datadog::Core::Configuration::AgentSettingsResolver.call(Datadog.configuration)
|
|
480
|
+
agent_hosts = agent.uds_path ? [] : ["#{agent.hostname}:#{agent.port}"]
|
|
481
|
+
|
|
482
|
+
Foam.init(
|
|
483
|
+
name: "checkout-api", environment: "production", token: ENV["FOAM_OTEL_TOKEN"],
|
|
484
|
+
ignored_outbound_hosts: agent_hosts,
|
|
485
|
+
ignored_outbound_headers: ["Datadog-Meta-Tracer-Version"]
|
|
486
|
+
)
|
|
487
|
+
```
|
|
488
|
+
|
|
462
489
|
## Redaction
|
|
463
490
|
|
|
464
491
|
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, with their framing kept. When masking a body fails, the whole body is replaced with `********`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Drops the spans of a request carrying one of the ignored headers: the request runs under a
|
|
7
|
+
# flagged context, and the sampler drops every span started there. Another SDK's transport stamps
|
|
8
|
+
# its header on every request to its backend, whether over TCP or a Unix socket. A connection
|
|
9
|
+
# opened before the request has its connect span before any header is seen; ignoring the host
|
|
10
|
+
# covers that one.
|
|
11
|
+
module IgnoredHeaders
|
|
12
|
+
CONTEXT_KEY = OpenTelemetry::Context.create_key("foam-ignored-request")
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_reader :names
|
|
16
|
+
|
|
17
|
+
def install(names)
|
|
18
|
+
@names = names
|
|
19
|
+
::Net::HTTP.prepend(self) unless names.empty?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def request(req, body = nil, &block)
|
|
24
|
+
return super unless IgnoredHeaders.names.any? { |name| req.key?(name) }
|
|
25
|
+
|
|
26
|
+
OpenTelemetry::Context.with_value(CONTEXT_KEY, true) { super }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -6,26 +6,32 @@ require "opentelemetry-semantic_conventions"
|
|
|
6
6
|
module Foam
|
|
7
7
|
module Otel
|
|
8
8
|
module Internal
|
|
9
|
-
# The tracer provider's sampler: drops every outbound span to an ignored host and
|
|
10
|
-
# to the sampler built from sample_rate. A dropped span
|
|
9
|
+
# The tracer provider's sampler: drops every outbound span to an ignored host and every span under
|
|
10
|
+
# an ignored request, and leaves the rest to the sampler built from sample_rate. A dropped span
|
|
11
|
+
# never records, so no processor sees it.
|
|
11
12
|
class IgnoredHosts
|
|
12
|
-
# Where the client instrumentations put the host when the span starts.
|
|
13
|
+
# Where the client instrumentations put the host and port when the span starts.
|
|
13
14
|
HOST_KEYS = [
|
|
14
15
|
OpenTelemetry::SemConv::SERVER::SERVER_ADDRESS,
|
|
15
16
|
OpenTelemetry::SemConv::Incubating::NET::NET_PEER_NAME
|
|
16
17
|
].freeze
|
|
18
|
+
PORT_KEYS = [
|
|
19
|
+
OpenTelemetry::SemConv::SERVER::SERVER_PORT,
|
|
20
|
+
OpenTelemetry::SemConv::Incubating::NET::NET_PEER_PORT
|
|
21
|
+
].freeze
|
|
17
22
|
|
|
18
23
|
# Skipped: server spans carry the local host in server.address.
|
|
19
24
|
INBOUND_KINDS = %i[server consumer].freeze
|
|
20
25
|
|
|
21
|
-
# Hosts as written, no resolution: localhost and 127.0.0.1 are different hosts.
|
|
26
|
+
# Hosts as written, no resolution: localhost and 127.0.0.1 are different hosts. An entry
|
|
27
|
+
# written host:port matches that port only.
|
|
22
28
|
def initialize(hosts, delegate)
|
|
23
29
|
@hosts = Set.new(hosts)
|
|
24
30
|
@delegate = delegate
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
|
|
28
|
-
return drop(parent_context) if ignored?(kind, attributes)
|
|
34
|
+
return drop(parent_context) if parent_context.value(IgnoredHeaders::CONTEXT_KEY) || ignored?(kind, attributes)
|
|
29
35
|
|
|
30
36
|
@delegate.should_sample?(
|
|
31
37
|
trace_id: trace_id, parent_context: parent_context, links: links,
|
|
@@ -52,16 +58,20 @@ module Foam
|
|
|
52
58
|
def ignored?(kind, attributes)
|
|
53
59
|
return false if attributes.nil? || INBOUND_KINDS.include?(kind)
|
|
54
60
|
|
|
55
|
-
# The host the client instrumentation set, if it set
|
|
61
|
+
# The host and port the client instrumentation set, if it set them.
|
|
56
62
|
host = attributes.values_at(*HOST_KEYS).compact.first
|
|
63
|
+
port = attributes.values_at(*PORT_KEYS).compact.first
|
|
57
64
|
# The rest-client gem's instrumentation sets no host, only the URL.
|
|
58
|
-
|
|
65
|
+
url = parsed(attributes[OpenTelemetry::SemConv::URL::URL_FULL])
|
|
66
|
+
host ||= url&.host
|
|
67
|
+
port ||= url&.port
|
|
68
|
+
return false if host.nil?
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
@hosts.include?(host) || @hosts.include?("#{host}:#{port}")
|
|
61
71
|
end
|
|
62
72
|
|
|
63
|
-
def
|
|
64
|
-
URI(url)
|
|
73
|
+
def parsed(url)
|
|
74
|
+
URI(url) if url
|
|
65
75
|
rescue URI::Error
|
|
66
76
|
nil
|
|
67
77
|
end
|
|
@@ -73,6 +73,23 @@ module Foam
|
|
|
73
73
|
begin
|
|
74
74
|
ForkHooks.attach
|
|
75
75
|
|
|
76
|
+
# Where another SDK's patch and an instrumentation's define the same helper on a class,
|
|
77
|
+
# each keeps its own, whichever of the two came first.
|
|
78
|
+
Safely.call("patch resolver") { PatchResolver.install }
|
|
79
|
+
|
|
80
|
+
# Drops the spans of the requests carrying an ignored header, in every capture mode.
|
|
81
|
+
IgnoredHeaders.install(options[:ignored_outbound_headers])
|
|
82
|
+
|
|
83
|
+
# Writes the W3C headers last on the requests Foam instruments, in every capture mode.
|
|
84
|
+
Reinject.install
|
|
85
|
+
|
|
86
|
+
# The logger instrumentation emits a record from inside every Logger call, and that emit
|
|
87
|
+
# reads the current OpenTelemetry context. An SDK that supplies its own context can raise
|
|
88
|
+
# there while it boots, which would make every app log line raise. LoggerBridge is
|
|
89
|
+
# prepended after the instrumentation's patch, so it sits in front of it and drops just
|
|
90
|
+
# the record whose emit failed.
|
|
91
|
+
::Logger.prepend(LoggerBridge) if Instrumentations.instance(Instrumentations::LOGGER).installed?
|
|
92
|
+
|
|
76
93
|
# The client patches set the safe headers on every client span and, in advanced mode,
|
|
77
94
|
# capture bodies too. Bodies are redacted by the same rules as attributes, so only
|
|
78
95
|
# advanced builds a body redactor; its presence is what switches body capture on.
|
|
@@ -229,8 +246,8 @@ module Foam
|
|
|
229
246
|
|
|
230
247
|
# The caller's hosts plus the endpoint host, so Foam's own exports never become client spans.
|
|
231
248
|
def ignored_hosts(hosts)
|
|
232
|
-
unless hosts.is_a?(Enumerable)
|
|
233
|
-
raise ArgumentError, "ignored_outbound_hosts must be an array of hostnames, got #{hosts.inspect}"
|
|
249
|
+
unless hosts.is_a?(Enumerable) && hosts.all? { |host| host.is_a?(String) && !Internal.blank?(host) }
|
|
250
|
+
raise ArgumentError, "ignored_outbound_hosts must be an array of hostnames, each with an optional :port, got #{hosts.inspect}"
|
|
234
251
|
end
|
|
235
252
|
|
|
236
253
|
Set.new(hosts) << URI(Endpoint.current).host
|
|
@@ -239,7 +256,7 @@ module Foam
|
|
|
239
256
|
# The first option holding a value init cannot use, described as "<option> must be ..., got
|
|
240
257
|
# <value>", or nil when every option is usable.
|
|
241
258
|
def invalid_option(
|
|
242
|
-
sample_rate:, disable_log_sending:, network_capture:, redact:, before_send:, **
|
|
259
|
+
sample_rate:, disable_log_sending:, network_capture:, ignored_outbound_headers:, redact:, before_send:, **
|
|
243
260
|
)
|
|
244
261
|
unless sample_rate.is_a?(Numeric) && sample_rate.finite? && (0..1).cover?(sample_rate)
|
|
245
262
|
return "sample_rate must be a number from 0.0 to 1.0, got #{sample_rate.inspect}"
|
|
@@ -253,6 +270,10 @@ module Foam
|
|
|
253
270
|
return "network_capture must be :off, :basic, or :advanced, got #{network_capture.inspect}"
|
|
254
271
|
end
|
|
255
272
|
|
|
273
|
+
unless ignored_outbound_headers.is_a?(Enumerable) && ignored_outbound_headers.all? { |name| name.is_a?(String) && !Internal.blank?(name) }
|
|
274
|
+
return "ignored_outbound_headers must be an array of header names, got #{ignored_outbound_headers.inspect}"
|
|
275
|
+
end
|
|
276
|
+
|
|
256
277
|
if (problem = Redactor.invalid_option(redact) || BeforeSend.invalid_option(before_send))
|
|
257
278
|
return problem
|
|
258
279
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Guards the emit the logger instrumentation runs inside every Logger line, so a failing emit
|
|
7
|
+
# drops that one record instead of raising into the app. Prepended on Logger by init once that
|
|
8
|
+
# instrumentation installed, so it sits ahead of the patch.
|
|
9
|
+
module LoggerBridge
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Another SDK's OpenTelemetry bridge can raise from Context.current while that SDK boots.
|
|
13
|
+
def emit_to_otel(severity, datetime, body)
|
|
14
|
+
Safely.call("logger bridge") { super }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Keeps each patch on a class calling its own helper when another patch defines one of the same
|
|
7
|
+
# name. Without this, Ruby would resolve the bare call from the front of the ancestor chain,
|
|
8
|
+
# whichever module made it, so one patch would always reach the other's helper.
|
|
9
|
+
module PatchResolver
|
|
10
|
+
# The namespace of the instrumentation patches init installs. A collision is resolved only
|
|
11
|
+
# when one of them is party to it; two unrelated gems fighting over a class are not Foam's to fix.
|
|
12
|
+
INSTRUMENTATION = "OpenTelemetry::Instrumentation::"
|
|
13
|
+
|
|
14
|
+
# Foam's own patches wrap a helper by calling super, so they are never party to a collision.
|
|
15
|
+
FOAM = "Foam::Otel::"
|
|
16
|
+
|
|
17
|
+
# The Thread.current key holding the patch whose method is running.
|
|
18
|
+
RUNNING = :foam_otel_running_patch
|
|
19
|
+
|
|
20
|
+
# Marks the modules the resolver prepends on the class, so a chain scan can leave them out.
|
|
21
|
+
module Dispatcher
|
|
22
|
+
attr_reader :names
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Marks the modules the resolver prepends on an owner.
|
|
26
|
+
module Marker
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Prepended on the class's singleton, so a module prepended later does not stay ahead.
|
|
30
|
+
module Watch
|
|
31
|
+
def prepend(*modules)
|
|
32
|
+
super
|
|
33
|
+
PatchResolver.assert(self)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Watches every loaded class carrying an instrumentation patch and resolves what already
|
|
40
|
+
# collides on it. Runs after the instrumentations installed.
|
|
41
|
+
def install
|
|
42
|
+
patched_classes.each do |klass|
|
|
43
|
+
klass.singleton_class.prepend(Watch) unless klass.singleton_class.include?(Watch)
|
|
44
|
+
assert(klass)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def patched_classes
|
|
49
|
+
ObjectSpace.each_object(Class).select do |klass|
|
|
50
|
+
!klass.singleton_class? && patches(klass).any? { |mod| instrumentation?(mod) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Marks the owners and prepends a dispatcher for the helpers that collide on klass, unless
|
|
55
|
+
# the front of the chain already holds one for the same helpers. Warns when a collision is
|
|
56
|
+
# first resolved, once per helper.
|
|
57
|
+
def assert(klass)
|
|
58
|
+
modules = patches(klass)
|
|
59
|
+
names = collisions(klass, modules)
|
|
60
|
+
return if names.empty?
|
|
61
|
+
|
|
62
|
+
front = klass.ancestors.first
|
|
63
|
+
return if front.is_a?(Dispatcher) && front.names == names
|
|
64
|
+
|
|
65
|
+
owners = modules.select { |mod| names.any? { |name| defines?(mod, name) } }
|
|
66
|
+
if owners.map { |owner| mark(owner, names) }.any?
|
|
67
|
+
names.each do |name|
|
|
68
|
+
holders = owners.select { |mod| defines?(mod, name) }
|
|
69
|
+
Diagnostics.warn("#{klass}: #{name} is defined by #{holders.join(" and ")}; each patch keeps its own")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
klass.prepend(dispatcher(names, owners))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The helpers two or more of the modules define, an instrumentation patch among them, that
|
|
76
|
+
# nothing behind the modules defines. A method the class itself has is overridden on purpose
|
|
77
|
+
# and chains through super, as Net::HTTP#request does under two SDKs; a helper exists nowhere
|
|
78
|
+
# below the patches, so two definitions fight.
|
|
79
|
+
def collisions(klass, modules)
|
|
80
|
+
behind = klass.ancestors.drop_while { |ancestor| !ancestor.equal?(klass) }
|
|
81
|
+
counts = modules.flat_map { |mod| mod.instance_methods(false) + mod.private_instance_methods(false) }.tally
|
|
82
|
+
counts.filter_map do |name, count|
|
|
83
|
+
next if count < 2 || behind.any? { |ancestor| defines?(ancestor, name) }
|
|
84
|
+
|
|
85
|
+
name if modules.any? { |mod| instrumentation?(mod) && defines?(mod, name) }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The modules prepended on klass, the resolver's own and Foam's left out.
|
|
90
|
+
def patches(klass)
|
|
91
|
+
klass.ancestors.take_while { |ancestor| !ancestor.equal?(klass) }
|
|
92
|
+
.reject { |mod| mod.is_a?(Dispatcher) || mod.is_a?(Marker) || mod.name.to_s.start_with?(FOAM) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def instrumentation?(mod)
|
|
96
|
+
mod.name.to_s.start_with?(INSTRUMENTATION)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def defines?(mod, name)
|
|
100
|
+
mod.method_defined?(name, false) || mod.private_method_defined?(name, false)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Records the owner as the running patch for the duration of each of its methods, the
|
|
104
|
+
# helpers themselves aside. Prepended on the owner module, the wrapper sits right ahead of it
|
|
105
|
+
# in every class that prepended the owner. Returns whether the owner was marked now rather
|
|
106
|
+
# than earlier.
|
|
107
|
+
def mark(owner, names)
|
|
108
|
+
return false if owner.ancestors.any? { |mod| mod.is_a?(Marker) }
|
|
109
|
+
|
|
110
|
+
marker = Module.new do
|
|
111
|
+
extend Marker
|
|
112
|
+
|
|
113
|
+
methods = owner.instance_methods(false) + owner.private_instance_methods(false) - names
|
|
114
|
+
methods.each do |method_name|
|
|
115
|
+
define_method(method_name) do |*args, **options, &block|
|
|
116
|
+
previous = Thread.current[RUNNING]
|
|
117
|
+
Thread.current[RUNNING] = owner
|
|
118
|
+
super(*args, **options, &block)
|
|
119
|
+
ensure
|
|
120
|
+
Thread.current[RUNNING] = previous
|
|
121
|
+
end
|
|
122
|
+
private method_name if owner.private_method_defined?(method_name, false)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
owner.prepend(marker)
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Answers each colliding helper with the running patch's own implementation. A call from
|
|
130
|
+
# outside every marked patch continues down the chain, where the first owner's helper
|
|
131
|
+
# answers as it would without the resolver. A helper every owner keeps private stays private.
|
|
132
|
+
def dispatcher(names, owners)
|
|
133
|
+
Module.new do
|
|
134
|
+
extend Dispatcher
|
|
135
|
+
|
|
136
|
+
@names = names
|
|
137
|
+
names.each do |name|
|
|
138
|
+
holders = owners.select { |owner| PatchResolver.defines?(owner, name) }
|
|
139
|
+
implementations = holders.to_h { |owner| [owner, owner.instance_method(name)] }
|
|
140
|
+
define_method(name) do |*args, **options, &block|
|
|
141
|
+
implementation = implementations[Thread.current[RUNNING]]
|
|
142
|
+
implementation ? implementation.bind_call(self, *args, **options, &block) : super(*args, **options, &block)
|
|
143
|
+
end
|
|
144
|
+
private name if holders.all? { |owner| owner.private_method_defined?(name, false) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Handles another backend SDK in the same process, such as Datadog. Both SDKs write trace
|
|
7
|
+
# headers on the outbound requests, and whichever writes last owns traceparent. Foam writes
|
|
8
|
+
# its W3C headers again at the transport, so traceparent always carries Foam's trace. The
|
|
9
|
+
# other SDK's own headers (such as Datadog's x-datadog-*) are left as written, so each
|
|
10
|
+
# backend continues its own trace downstream. Installed by init in every capture mode.
|
|
11
|
+
module Reinject
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The global propagator, the one the instrumentation used. Without a span there is nothing
|
|
15
|
+
# the instrumentation wrote, so nothing is written here either.
|
|
16
|
+
def call(carrier, context: OpenTelemetry::Context.current)
|
|
17
|
+
return if OwnRequest.current? || !OpenTelemetry::Trace.current_span(context).context.valid?
|
|
18
|
+
|
|
19
|
+
OpenTelemetry.propagation.inject(carrier, context: context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module NetHttp
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def transport_request(req)
|
|
26
|
+
Safely.call("propagation") { Reinject.call(req) }
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module HttpClient
|
|
32
|
+
def query(req)
|
|
33
|
+
Safely.call("propagation") { Reinject.call(req.header) }
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module Excon
|
|
39
|
+
def request_call(datum)
|
|
40
|
+
Safely.call("propagation") { Reinject.call(datum[:headers]) }
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module HttpRb
|
|
46
|
+
def send_request(req)
|
|
47
|
+
Safely.call("propagation") { Reinject.call(req.headers) }
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Every header write reaches libcurl through this setter, the instrumentation's and any
|
|
53
|
+
# other SDK's alike, so the last one carries Foam's headers whichever it was.
|
|
54
|
+
module Ethon
|
|
55
|
+
def headers=(headers)
|
|
56
|
+
headers = (headers || {}).dup
|
|
57
|
+
Safely.call("propagation") { Reinject.call(headers, context: foam_ethon_context) }
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# The instrumentation starts its span in @otel_span without making it current.
|
|
64
|
+
def foam_ethon_context
|
|
65
|
+
span = @otel_span if instance_variable_defined?(:@otel_span)
|
|
66
|
+
span ? OpenTelemetry::Trace.context_with_span(span) : OpenTelemetry::Context.current
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The client interceptors, where both SDKs inject, run outside these calls.
|
|
71
|
+
module Grpc
|
|
72
|
+
%i[request_response client_streamer server_streamer bidi_streamer].each do |name|
|
|
73
|
+
define_method(name) do |*args, metadata: {}, **rest, &block|
|
|
74
|
+
Safely.call("propagation") { Reinject.call(metadata) }
|
|
75
|
+
super(*args, metadata: metadata, **rest, &block)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The chain's final block runs inside every client middleware, where both SDKs inject, with
|
|
81
|
+
# the producer span current.
|
|
82
|
+
module Sidekiq
|
|
83
|
+
# The client chain is invoked as (worker_class, job, queue, redis_pool), the server chain as
|
|
84
|
+
# (worker, job, queue).
|
|
85
|
+
def invoke(*args, &block)
|
|
86
|
+
return super unless args.size == 4
|
|
87
|
+
|
|
88
|
+
_worker_class, job, _queue, _redis_pool = args
|
|
89
|
+
super do
|
|
90
|
+
Safely.call("propagation") { Reinject.call(job) }
|
|
91
|
+
block.call
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The transport class each hook prepends onto, and the instrumentation that has to be
|
|
97
|
+
# installed for the hook to apply; Net::HTTP is patched without a check, since foam-otel
|
|
98
|
+
# loads it for its own exporters.
|
|
99
|
+
HOOKS = {
|
|
100
|
+
NetHttp => ["Net::HTTP", nil],
|
|
101
|
+
HttpClient => ["HTTPClient::Session", Clients::HttpClient::INSTRUMENTATION],
|
|
102
|
+
Excon => ["Excon::Connection", Clients::Excon::INSTRUMENTATION],
|
|
103
|
+
HttpRb => ["HTTP::Connection", "OpenTelemetry::Instrumentation::HTTP"],
|
|
104
|
+
Ethon => ["Ethon::Easy", "OpenTelemetry::Instrumentation::Ethon"],
|
|
105
|
+
Grpc => ["GRPC::ActiveCall", "OpenTelemetry::Instrumentation::Grpc"],
|
|
106
|
+
Sidekiq => ["Sidekiq::Middleware::Chain", "OpenTelemetry::Instrumentation::Sidekiq"]
|
|
107
|
+
}.freeze
|
|
108
|
+
|
|
109
|
+
def install
|
|
110
|
+
HOOKS.each do |hook, (klass, instrumentation)|
|
|
111
|
+
next unless instrumentation.nil? || Instrumentations.instance(instrumentation).installed?
|
|
112
|
+
|
|
113
|
+
Object.const_get(klass).prepend(hook)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/foam/otel/version.rb
CHANGED
data/lib/foam/otel.rb
CHANGED
|
@@ -26,9 +26,13 @@ require_relative "otel/internal/aws_lambda"
|
|
|
26
26
|
require_relative "otel/internal/rails_middleware"
|
|
27
27
|
require_relative "otel/internal/before_send"
|
|
28
28
|
require_relative "otel/internal/instrumentations"
|
|
29
|
+
require_relative "otel/internal/ignored_headers"
|
|
30
|
+
require_relative "otel/internal/logger_bridge"
|
|
31
|
+
require_relative "otel/internal/reinject"
|
|
29
32
|
require_relative "otel/internal/ignored_hosts"
|
|
30
33
|
require_relative "otel/internal/slots"
|
|
31
34
|
require_relative "otel/internal/fork_hooks"
|
|
35
|
+
require_relative "otel/internal/patch_resolver"
|
|
32
36
|
require_relative "otel/internal/resource"
|
|
33
37
|
require_relative "otel/internal/init"
|
|
34
38
|
require_relative "otel/internal/exporters"
|
|
@@ -58,6 +62,7 @@ module Foam
|
|
|
58
62
|
disable_log_sending: false,
|
|
59
63
|
network_capture: :basic,
|
|
60
64
|
ignored_outbound_hosts: [],
|
|
65
|
+
ignored_outbound_headers: [],
|
|
61
66
|
redact: nil,
|
|
62
67
|
before_send: nil
|
|
63
68
|
)
|
|
@@ -77,6 +82,7 @@ module Foam
|
|
|
77
82
|
disable_log_sending: disable_log_sending,
|
|
78
83
|
network_capture: network_capture,
|
|
79
84
|
ignored_outbound_hosts: ignored_outbound_hosts,
|
|
85
|
+
ignored_outbound_headers: ignored_outbound_headers,
|
|
80
86
|
redact: redact,
|
|
81
87
|
before_send: before_send
|
|
82
88
|
)
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foam
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: opentelemetry-exporter-otlp
|
|
@@ -176,21 +176,25 @@ files:
|
|
|
176
176
|
- lib/foam/otel/internal/endpoint.rb
|
|
177
177
|
- lib/foam/otel/internal/exporters.rb
|
|
178
178
|
- lib/foam/otel/internal/fork_hooks.rb
|
|
179
|
+
- lib/foam/otel/internal/ignored_headers.rb
|
|
179
180
|
- lib/foam/otel/internal/ignored_hosts.rb
|
|
180
181
|
- lib/foam/otel/internal/ingest.rb
|
|
181
182
|
- lib/foam/otel/internal/init.rb
|
|
182
183
|
- lib/foam/otel/internal/instrumentations.rb
|
|
183
184
|
- lib/foam/otel/internal/lifecycle.rb
|
|
185
|
+
- lib/foam/otel/internal/logger_bridge.rb
|
|
184
186
|
- lib/foam/otel/internal/logs.rb
|
|
185
187
|
- lib/foam/otel/internal/metrics.rb
|
|
186
188
|
- lib/foam/otel/internal/otlp.rb
|
|
187
189
|
- lib/foam/otel/internal/own_request.rb
|
|
190
|
+
- lib/foam/otel/internal/patch_resolver.rb
|
|
188
191
|
- lib/foam/otel/internal/propagation.rb
|
|
189
192
|
- lib/foam/otel/internal/rack/body_tee.rb
|
|
190
193
|
- lib/foam/otel/internal/rack/input_tee.rb
|
|
191
194
|
- lib/foam/otel/internal/rails_middleware.rb
|
|
192
195
|
- lib/foam/otel/internal/redactor.rb
|
|
193
196
|
- lib/foam/otel/internal/redactor/keys.rb
|
|
197
|
+
- lib/foam/otel/internal/reinject.rb
|
|
194
198
|
- lib/foam/otel/internal/report.rb
|
|
195
199
|
- lib/foam/otel/internal/resource.rb
|
|
196
200
|
- lib/foam/otel/internal/safely.rb
|