foam-otel 1.0.2 → 1.2.0
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/GOTCHAS.md +522 -53
- data/README.md +341 -31
- data/RESEARCH.md +170 -70
- data/THIRD-PARTY-NOTICES +21 -10
- data/lib/foam/otel/api.rb +124 -16
- data/lib/foam/otel/classifier.rb +44 -6
- data/lib/foam/otel/config.rb +22 -2
- data/lib/foam/otel/constants.rb +15 -89
- data/lib/foam/otel/errors.rb +4 -1
- data/lib/foam/otel/fork_hooks.rb +7 -0
- data/lib/foam/otel/ingest.rb +617 -0
- data/lib/foam/otel/ingest_metric_reader.rb +87 -0
- data/lib/foam/otel/init.rb +351 -34
- data/lib/foam/otel/instance.rb +4 -1
- data/lib/foam/otel/llm/anthropic_shim.rb +84 -0
- data/lib/foam/otel/llm/gemini_shim.rb +113 -0
- data/lib/foam/otel/llm/openai_shim.rb +123 -0
- data/lib/foam/otel/llm/ruby_llm_shim.rb +131 -0
- data/lib/foam/otel/llm.rb +272 -0
- data/lib/foam/otel/logger_bridge.rb +187 -0
- data/lib/foam/otel/metrics.rb +32 -7
- data/lib/foam/otel/pipelines.rb +72 -3
- data/lib/foam/otel/redacting_exporter.rb +88 -17
- data/lib/foam/otel/redaction.rb +164 -87
- data/lib/foam/otel/runtime_metrics.rb +120 -0
- data/lib/foam/otel/session_stitching.rb +112 -0
- data/lib/foam/otel/version.rb +1 -1
- metadata +237 -19
data/RESEARCH.md
CHANGED
|
@@ -58,8 +58,13 @@ record_histogram/add_up_down_counter/set_metric`, `log`, `redact`,
|
|
|
58
58
|
|
|
59
59
|
**Context / Baggage / Propagation** — excluded from the helper surface (low-level
|
|
60
60
|
plumbing): `Context.*` (`context.rb`), `Baggage.*` (`baggage.rb`), and
|
|
61
|
-
`OpenTelemetry.propagation
|
|
62
|
-
`lib/foam/otel/init.rb`)
|
|
61
|
+
`OpenTelemetry.propagation`, which foam sets at init (W3C TraceContext + Baggage,
|
|
62
|
+
`lib/foam/otel/init.rb`) ONLY when the slot still holds the API's built-in
|
|
63
|
+
default (`NoopTextMapPropagator`, memoized by the reader —
|
|
64
|
+
`opentelemetry-api opentelemetry.rb:69-71`); the slot is a last-write-wins
|
|
65
|
+
global (`attr_writer :propagation`, `opentelemetry.rb:27`), so a propagator a
|
|
66
|
+
foreign SDK installed first is KEPT with one warning (rule 18 B: "a foreign
|
|
67
|
+
owner keeps them" — GOTCHAS F5). Reachable through the passthroughs where needed.
|
|
63
68
|
|
|
64
69
|
**Lifecycle** (`init/flush/shutdown/after_fork!`) and `redact` are foam-composed
|
|
65
70
|
(no single API primitive): providers' `shutdown`/`force_flush`, the OTLP
|
|
@@ -105,29 +110,58 @@ consumer) or tier-2 (patch/prepend). foam activates by presence: `init` greps
|
|
|
105
110
|
`Gem.loaded_specs` for `opentelemetry-instrumentation-*`, requires each, and
|
|
106
111
|
calls `registry.install_all` (each gem presence-checks its own target). foam
|
|
107
112
|
maintains **no per-gem allow/deny list** — activation is bundle-driven
|
|
108
|
-
(`lib/foam/otel/init.rb`).
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`-
|
|
118
|
-
`-
|
|
119
|
-
`-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
(`lib/foam/otel/init.rb`). Activation is GATED on the traces verdict (rule
|
|
114
|
+
18 B): when a foreign SDK owns the tracer slot, `init` installs NOTHING
|
|
115
|
+
automatically (every contrib gem binds its tracer to the live global — the
|
|
116
|
+
foreign provider) and warns once; explicit `additional_instrumentations` still
|
|
117
|
+
install (`spec/activation_spec.rb` claimed-traces cases).
|
|
118
|
+
|
|
119
|
+
**Always-activated (foam bundles them — the UNIVERSAL FLOOR, landed):**
|
|
120
|
+
`-rack` (direct) + the `-rails 0.42.0` meta-gem (pulls `-action_pack`,
|
|
121
|
+
`-action_view`, `-action_mailer`, `-active_record`, `-active_job`,
|
|
122
|
+
`-active_storage`, `-active_support`, `-concurrent_ruby`); the HTTP clients
|
|
123
|
+
`-net_http 0.29.0` and `-faraday 0.33.0`; the primary datastores `-pg 0.36.0`,
|
|
124
|
+
`-mysql2 0.34.0`, `-redis 0.29.0`, `-mongo 0.26.0`; and `-sidekiq 0.29.0` —
|
|
125
|
+
verified against the gemspec, the ground truth. Every entry is
|
|
126
|
+
presence-checked, so a gem whose target library is absent is an inert skip
|
|
127
|
+
(a non-Rails app skips the Rails family; an app without Postgres skips `-pg`;
|
|
128
|
+
and so on). The landed floor also includes foam's own non-contrib pieces: the
|
|
129
|
+
stdlib **Logger bridge** (§4), hand-written **runtime + GC metrics** (below),
|
|
130
|
+
**session stitching** (baggage→attributes), and the **LLM shims** (§5).
|
|
131
|
+
|
|
132
|
+
**Activated-when-present (FDE bundles the long tail, foam auto-installs):** the
|
|
133
|
+
remaining HTTP clients (`-http`, `-http_client`, `-httpx`, `-excon`, `-ethon`,
|
|
134
|
+
`-restclient`), datastores (`-trilogy`, `-dalli`, `-lmdb`),
|
|
135
|
+
messaging (`-bunny`, `-rdkafka`, `-racecar`, `-ruby_kafka`), jobs (`-resque`,
|
|
136
|
+
`-delayed_job`, `-que`), web/rpc (`-sinatra`, `-grape`, `-graphql`, `-grpc`,
|
|
137
|
+
`-gruf`), and `-aws_sdk`, `-aws_lambda`, `-koala`,
|
|
138
|
+
`-active_model_serializers`, `-rake`, `-anthropic` (context-propagation only —
|
|
139
|
+
see §5).
|
|
140
|
+
|
|
141
|
+
`install_all` receives foam's CENTRAL raw-capture config
|
|
142
|
+
(`FLOOR_INSTRUMENTATION_CONFIG`, `lib/foam/otel/init.rb`): the datastore
|
|
143
|
+
statement posture is **captured RAW, never suppressed** (2026-07-26 ruling —
|
|
144
|
+
coverage is the mission), and the upstream `-pg`/`-mysql2`/`-redis`/`-mongo`
|
|
145
|
+
gems all DEFAULT to `db_statement: :obfuscate` — a sanitizer — so foam
|
|
146
|
+
explicitly activates them with `db_statement: :include` (GOTCHAS F9; pinned
|
|
147
|
+
by `spec/floor_spec.rb`). Redaction stays fully opt-in, applied only for keys
|
|
148
|
+
the customer lists in `redact_keys`/`redact_pii_keys`. An FDE can still clamp
|
|
149
|
+
`db_statement: :omit`/`:obfuscate` per customer via the standard
|
|
150
|
+
`OTEL_RUBY_INSTRUMENTATION_<NAME>_CONFIG_OPTS` env var when a compliance
|
|
151
|
+
context calls for it — per-customer tightening (rule 18 / Area 4), not the
|
|
152
|
+
default.
|
|
153
|
+
|
|
154
|
+
**Runtime + GC metrics (hand-written — landed,
|
|
155
|
+
`lib/foam/otel/runtime_metrics.rb`).** Ruby has no drop-in upstream metrics
|
|
156
|
+
instrumentation gem (unlike Python's `-system-metrics` or the Node
|
|
157
|
+
host-metrics package), so foam ships its OWN collector: asynchronous
|
|
158
|
+
observable instruments over `GC.stat` / `Thread.list` / `/proc`, emitting GC
|
|
159
|
+
counts + timing, heap slots, allocation/free totals, thread count, and RSS
|
|
160
|
+
(`process.runtime.ruby.*` + `process.memory.usage`). Registered by `init`
|
|
161
|
+
when foam owns the metrics slot; collection runs on the
|
|
162
|
+
`PeriodicMetricReader`'s thread, which foam's `Process._fork` hook restarts
|
|
163
|
+
in cluster workers (GOTCHAS R1/R7/G15). No labels at all — safe by
|
|
164
|
+
construction (rule 26). Tests: `spec/runtime_metrics_spec.rb`.
|
|
131
165
|
|
|
132
166
|
**Excluded (with reason):** `-factory_bot`, `-rspec` (test-only, no production
|
|
133
167
|
value); `-net_ldap` (niche, PII/credential-adjacent — explicit FDE opt-in);
|
|
@@ -148,36 +182,61 @@ Registry: https://github.com/open-telemetry/opentelemetry-ruby-contrib/tree/main
|
|
|
148
182
|
|
|
149
183
|
Ruby's Logs signal is "development" (https://opentelemetry.io/docs/languages/ruby/).
|
|
150
184
|
foam ships the logs pipeline + `log()` helper (trace-correlated) — the mechanism
|
|
151
|
-
rule 20 requires. **
|
|
152
|
-
|
|
153
|
-
`Logger
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
rule 20 requires. The stdlib **Logger bridge is LANDED as foam's own code**
|
|
186
|
+
(`lib/foam/otel/logger_bridge.rb`, console-logs ruling 2026-07-26): one
|
|
187
|
+
prepend on `Logger#add` turns every message a host logger accepts into a
|
|
188
|
+
trace-correlated OTel log record — body RAW (opt-in key-masking only),
|
|
189
|
+
severity mapped, loop-guarded (the OTel SDK's internal logger is skipped by
|
|
190
|
+
identity + a thread-local re-entrancy flag), gated on foam owning the logs
|
|
191
|
+
slot, and never able to break the host's log call (GOTCHAS F10; tests
|
|
192
|
+
`spec/logger_bridge_spec.rb`). Because `Rails.logger`, Sidekiq's logger, and
|
|
193
|
+
Semantic Logger's appenders all bottom out in stdlib `Logger` instances, the
|
|
194
|
+
one prepend covers most framework logging in practice;
|
|
195
|
+
`BroadcastLogger`/Lograge/Ougai-specific appenders remain uncovered upstream
|
|
196
|
+
(the Rails logs discussion
|
|
197
|
+
https://github.com/open-telemetry/opentelemetry-ruby/discussions/1789 is
|
|
198
|
+
open/unresolved) — a documented gap, narrowed to the non-Logger-backed exotics.
|
|
160
199
|
|
|
161
200
|
---
|
|
162
201
|
|
|
163
202
|
## 5. LLM ecosystem (rule 31)
|
|
164
203
|
|
|
165
|
-
**No
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
**No OFFICIAL OpenTelemetry Ruby gen_ai instrumentation exists for ANY LLM
|
|
205
|
+
SDK — foam ships its own thin gap-filler shims (LANDED,
|
|
206
|
+
`lib/foam/otel/llm.rb` + `llm/*_shim.rb`; provider-parity ruling
|
|
207
|
+
2026-07-26: OpenAI + Anthropic + Gemini in every server language).** The
|
|
208
|
+
shims patch the SDKs' public call sites — official `openai`
|
|
209
|
+
(chat.completions.create + responses.create), official `anthropic`
|
|
210
|
+
(messages.create), `gemini-ai` (generate_content / stream_generate_content;
|
|
211
|
+
Google ships no official Ruby SDK), and `ruby_llm` (Provider#complete, one
|
|
212
|
+
seam covering all its providers) — emitting the same GenAI semconv attribute
|
|
213
|
+
set the fleet's js/python gap-fillers emit. **LLM traces are captured in
|
|
214
|
+
full — activity (model, response id, finish reasons, tokens, latency) AND
|
|
215
|
+
prompt/completion content, raw by default** (2026-07-26 ruling: LLM content
|
|
216
|
+
is never gated or dropped; the FDE reactive clamp is the only per-customer
|
|
217
|
+
restriction). Presence-checked, version-windowed, fail-to-dark, one call =
|
|
218
|
+
one span (the SDK transport rides the untraced suppression context), and
|
|
219
|
+
the model call is never breakable by telemetry (GOTCHAS F11). LLM testing is
|
|
220
|
+
MANDATORY and landed: per-provider specs against the REAL SDKs with only the
|
|
221
|
+
3rd party mocked (`spec/llm_openai_spec.rb`, `spec/llm_anthropic_spec.rb`,
|
|
222
|
+
`spec/llm_gemini_spec.rb`, `spec/llm_ruby_llm_spec.rb` — the last is the
|
|
223
|
+
wire-level Gemini proof over a local fake provider).
|
|
168
224
|
|
|
169
225
|
- `-anthropic` (in the registry / `all` meta-gem) is **context-propagation ONLY**
|
|
170
226
|
— its sole patch stores/restores OTel context across the SDK's connection pool;
|
|
171
227
|
it sets no `gen_ai.*` attributes, captures no tokens/content (CHANGELOG v0.1–0.5
|
|
172
|
-
are context/semconv/version bumps).
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
228
|
+
are context/semconv/version bumps). Not the LLM-activity vehicle — foam's
|
|
229
|
+
shim is.
|
|
230
|
+
- `opentelemetry-instrumentation-ruby_llm` (thoughtbot, third-party) exists
|
|
231
|
+
but is NOT bundled: foam's own `ruby_llm` shim emits the fleet-identical
|
|
232
|
+
attribute set instead, avoiding a non-registry supply-chain dependency and
|
|
233
|
+
a cross-language semconv divergence.
|
|
234
|
+
- `ruby-openai` (community gem), `langchainrb`, AWS Bedrock (via `-aws_sdk`,
|
|
235
|
+
which emits no gen_ai attributes), `llm.rb`: **TIER3-recipe** —
|
|
179
236
|
hand-written spans via helpers + passthroughs with `gen_ai.*` semconv + token
|
|
180
|
-
counts + trace/log correlation.
|
|
237
|
+
counts + trace/log correlation. An uncovered library the FDE hits becomes a
|
|
238
|
+
filed package need (the FDE activates, never authors); until then the tier-3
|
|
239
|
+
recipe covers it.
|
|
181
240
|
- No mainstream library is CANNOT-COVER — no finding against the helper set.
|
|
182
241
|
|
|
183
242
|
Sources: contrib `-anthropic` CHANGELOG; https://github.com/thoughtbot/opentelemetry-instrumentation-ruby_llm ;
|
|
@@ -186,31 +245,40 @@ https://opentelemetry.io/docs/specs/semconv/gen-ai/aws-bedrock/ ; traceloop Ruby
|
|
|
186
245
|
|
|
187
246
|
---
|
|
188
247
|
|
|
189
|
-
## 6. Redaction
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
(
|
|
248
|
+
## 6. Redaction design record (rule 14, as amended 2026-07-26)
|
|
249
|
+
|
|
250
|
+
**Redaction is FULLY OPT-IN — the default is ZERO redaction** (owner ruling
|
|
251
|
+
2026-07-26, holistic-redesign decision log; `lib/foam/otel/redaction.rb`).
|
|
252
|
+
With neither `redact_keys` nor `redact_pii_keys` set (or both empty) every
|
|
253
|
+
value is captured RAW — attributes, headers, bodies, URL query params,
|
|
254
|
+
`db.statement`, log bodies — including secrets and PII. There is NO always-on
|
|
255
|
+
secrets floor and NO value-pattern auto-masking; the package auto-masks
|
|
256
|
+
nothing and ships no preset. The customer owns the privacy/legal posture; the
|
|
257
|
+
FDE reactive clamp is the per-customer valve.
|
|
258
|
+
|
|
259
|
+
**The retired floor**: earlier revisions shipped an always-on researched
|
|
260
|
+
secrets floor in `constants.rb` (roots + value-pattern pass, sourced from the
|
|
261
|
+
OSS-scrubber consensus — raven-ruby, sentry, Elastic APM, Rails
|
|
262
|
+
filter_parameters, gitleaks et al. — modeled on the Node core). It was
|
|
263
|
+
REMOVED by the 2026-07-26 ruling; the provenance record lives in this file's
|
|
264
|
+
git history and the decision log. Do not reintroduce it without a new ruling.
|
|
265
|
+
|
|
266
|
+
**The current engine** (`redaction.rb`): redaction runs ONLY over the keys
|
|
267
|
+
the customer enumerates. `redact_keys` matches → tail mask, shape-preserving
|
|
268
|
+
(`********` + last 4 when ≥ 12 chars; the fixed 8-char body never reveals
|
|
269
|
+
length; non-scalars redact in full); `redact_pii_keys` matches → full
|
|
270
|
+
`[REDACTED]`, no tail. Matching is SUBSTRING, case-insensitive, over the
|
|
271
|
+
lowercased key — the customer's lists, nothing inferred. The pass runs at the
|
|
272
|
+
exporter boundary (`redacting_exporter.rb` — the Ruby SDK freezes span
|
|
273
|
+
attributes at finish before any `on_finish` processor runs, so the exporter
|
|
274
|
+
boundary is the seam; see GOTCHAS) and is a true pass-through when no keys
|
|
275
|
+
are configured. The explicit `redact(value)` helper always masks (calling it
|
|
276
|
+
is the opt-in). Fail-closed for listed keys (mask on any error, cycle + depth
|
|
277
|
+
guarded).
|
|
278
|
+
|
|
279
|
+
Proven by `spec/redaction_spec.rb` (opt-in defaults — nothing masked with no
|
|
280
|
+
keys; both mask shapes; fail-closed) and `spec/redacting_exporter_spec.rb` +
|
|
281
|
+
the conformance app (default-path RAW capture on the wire).
|
|
214
282
|
|
|
215
283
|
---
|
|
216
284
|
|
|
@@ -226,6 +294,38 @@ upstream SDK components foam builds on.
|
|
|
226
294
|
|
|
227
295
|
---
|
|
228
296
|
|
|
297
|
+
## 8. Door-2 attach points (ingest entries) — verified at the pinned versions
|
|
298
|
+
|
|
299
|
+
Ruby is the rare core where every door-2 attach point exists post-construction
|
|
300
|
+
on a foreign provider (the JS >=2.0/Java "only inside their setup" limitation
|
|
301
|
+
does not bite):
|
|
302
|
+
|
|
303
|
+
- **Traces**: `TracerProvider#add_span_processor`
|
|
304
|
+
(`V/opentelemetry-sdk-1.13.0/lib/opentelemetry/sdk/trace/tracer_provider.rb:133`);
|
|
305
|
+
also reachable inside their setup block via
|
|
306
|
+
`Configurator#add_span_processor` (`configurator.rb:125`).
|
|
307
|
+
- **Logs**: `LoggerProvider#add_log_record_processor`
|
|
308
|
+
(`V/opentelemetry-logs-sdk-0.6.1/lib/opentelemetry/sdk/logs/logger_provider.rb:62`).
|
|
309
|
+
- **Metrics**: `MeterProvider#add_metric_reader`
|
|
310
|
+
(`V/opentelemetry-metrics-sdk-0.15.0/.../meter_provider.rb:110-121`) — and
|
|
311
|
+
it BACK-FILLS: the registry walk registers every already-created instrument
|
|
312
|
+
with the new reader's `metric_store`, so attach order is forgiving.
|
|
313
|
+
|
|
314
|
+
Load-bearing internals the taps rely on (each pinned by a test — GOTCHAS
|
|
315
|
+
R5-R8): the `metric_store` seam forcing the `PeriodicMetricReader` subclass;
|
|
316
|
+
shallow-dup'd data points sharing the LIVE aggregation-key attributes hash
|
|
317
|
+
(`aggregation/sum.rb:44-50` — masking must rebuild copies);
|
|
318
|
+
`LogRecord#resource` for self-ingest detection; batch processors converting
|
|
319
|
+
to fresh `SpanData`/`LogRecordData` structs at export (non-mutation is
|
|
320
|
+
structural); all three OTLP exporters sending inside
|
|
321
|
+
`Common::Utilities.untraced` via `Net::HTTP` (the suppression layer of the
|
|
322
|
+
loop story), with the contrib `untraced_hosts` option declared by Net::HTTP
|
|
323
|
+
and Excon only. The logs/metrics SDKs are pre-1.0 — the entries are SHIPPED,
|
|
324
|
+
pinned inside the capped ranges (GOTCHAS R2/R5) so a 0.x drift is a deliberate
|
|
325
|
+
re-prove; the pre-1.0 maturity is a version-pin caveat, not a deferral.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
229
329
|
## Signatures
|
|
230
330
|
|
|
231
331
|
| Reviewer | Date | Package version | Notes |
|
data/THIRD-PARTY-NOTICES
CHANGED
|
@@ -10,17 +10,21 @@ redistributes).
|
|
|
10
10
|
Foam thanks the OpenTelemetry authors and community
|
|
11
11
|
(https://opentelemetry.io) — this package is built on their work.
|
|
12
12
|
|
|
13
|
-
The
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
MIT
|
|
19
|
-
|
|
20
|
-
libraries is a runtime dependency of foam-otel.
|
|
13
|
+
The OPT-IN redaction mask shapes (lib/foam/otel/constants.rb — the 8-char
|
|
14
|
+
MASK_BODY and tail-mask convention applied only to customer-listed
|
|
15
|
+
redact_keys/redact_pii_keys; foam redacts NOTHING by default) are COPIED
|
|
16
|
+
KNOWLEDGE, not vendored code (rule 38): the shapes were researched from the
|
|
17
|
+
published conventions of permissively-licensed open-source scrubbers
|
|
18
|
+
(raven-ruby MIT STRING_MASK precedent among them) and Rails'
|
|
19
|
+
filter_parameters (MIT). Mask FORMATS are facts, not copyrightable; no code
|
|
20
|
+
was copied. None of these libraries is a runtime dependency of foam-otel.
|
|
21
|
+
Provenance: RESEARCH.md.
|
|
21
22
|
|
|
22
23
|
Direct runtime dependencies (name version — license — repository):
|
|
23
24
|
|
|
25
|
+
opentelemetry-api 1.11.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
26
|
+
opentelemetry-metrics-api 0.6.1 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
27
|
+
opentelemetry-logs-api 0.4.1 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
24
28
|
opentelemetry-sdk 1.13.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
25
29
|
opentelemetry-exporter-otlp 0.34.1 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
26
30
|
opentelemetry-logs-sdk 0.6.1 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
@@ -29,10 +33,17 @@ Direct runtime dependencies (name version — license — repository):
|
|
|
29
33
|
opentelemetry-exporter-otlp-metrics 0.10.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
|
30
34
|
opentelemetry-instrumentation-rack 0.31.1 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
31
35
|
opentelemetry-instrumentation-rails 0.42.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
36
|
+
opentelemetry-instrumentation-net_http 0.29.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
37
|
+
opentelemetry-instrumentation-faraday 0.33.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
38
|
+
opentelemetry-instrumentation-pg 0.36.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
39
|
+
opentelemetry-instrumentation-mysql2 0.34.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
40
|
+
opentelemetry-instrumentation-redis 0.29.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
41
|
+
opentelemetry-instrumentation-mongo 0.26.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
42
|
+
opentelemetry-instrumentation-sidekiq 0.29.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby-contrib
|
|
32
43
|
|
|
33
44
|
The opentelemetry-instrumentation-rails meta-gem transitively pulls the Rails
|
|
34
45
|
instrumentation family (action_pack, action_view, action_mailer, active_record,
|
|
35
46
|
active_job, active_storage, active_support, concurrent_ruby), each Apache-2.0,
|
|
36
47
|
from https://github.com/open-telemetry/opentelemetry-ruby-contrib. Additional
|
|
37
|
-
opentelemetry-instrumentation-* gems the FDE bundles
|
|
38
|
-
dependencies, not foam's.
|
|
48
|
+
opentelemetry-instrumentation-* gems the FDE bundles beyond the floor above are
|
|
49
|
+
the customer's declared dependencies, not foam's.
|
data/lib/foam/otel/api.rb
CHANGED
|
@@ -43,53 +43,104 @@ module Foam
|
|
|
43
43
|
def get_logger(name = "foam")
|
|
44
44
|
provider = OpenTelemetry.logger_provider
|
|
45
45
|
warn_if_displaced(:logs, provider)
|
|
46
|
-
|
|
46
|
+
# Never register into the PRE-INIT proxy registry (GOTCHAS L1): the
|
|
47
|
+
# logs-api 0.4.1 ProxyLoggerProvider#delegate= upgrades cached proxy
|
|
48
|
+
# loggers with a POSITIONAL provider.logger(name, version) call, but
|
|
49
|
+
# the logs SDK's #logger is keyword-only — one cached proxy logger
|
|
50
|
+
# makes init's logger_provider assignment raise and would leave the
|
|
51
|
+
# logs pipeline dark for the process. Pre-init logs are a silent
|
|
52
|
+
# no-op by contract, so hand back the API's no-op Logger directly.
|
|
53
|
+
# (Same feature-detected Internal read as the classifier — rule 42,
|
|
54
|
+
# GOTCHAS G14; when the constant is missing this check degrades to
|
|
55
|
+
# the plain provider call below.)
|
|
56
|
+
if defined?(OpenTelemetry::Internal::ProxyLoggerProvider) &&
|
|
57
|
+
provider.instance_of?(OpenTelemetry::Internal::ProxyLoggerProvider)
|
|
58
|
+
return noop_logger
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
provider.logger(name: name) # the logs SDK's keyword-only signature
|
|
63
|
+
rescue ArgumentError
|
|
64
|
+
provider.logger(name) # a positional-signature (older/foreign) provider
|
|
65
|
+
end
|
|
47
66
|
end
|
|
48
67
|
|
|
49
68
|
# ---- traces ----------------------------------------------------------
|
|
50
69
|
|
|
51
70
|
# A custom span around a block. Re-raises the block's error identically,
|
|
52
71
|
# records the exception + ERROR status, and ALWAYS ends the span (rule 9
|
|
53
|
-
# / never-ended-span leak). Safe pre-init (no-op span).
|
|
72
|
+
# / never-ended-span leak). Safe pre-init (no-op span). Argument
|
|
73
|
+
# preparation NEVER throws (rule 9): garbage attributes degrade to nil,
|
|
74
|
+
# a poisoned name to "unnamed" — the block always runs and only ITS
|
|
75
|
+
# error re-raises. When foam is initialized disabled, the block runs on
|
|
76
|
+
# a non-recording no-op span: disabled beats everything, including
|
|
77
|
+
# inert-mode emission through a foreign provider (section 0).
|
|
54
78
|
def span(name, attributes: nil, &block)
|
|
55
|
-
|
|
79
|
+
span_name = begin
|
|
80
|
+
name.to_s
|
|
81
|
+
rescue StandardError, SystemStackError
|
|
82
|
+
# SystemStackError is not a StandardError: a name object whose #to_s
|
|
83
|
+
# recurses unboundedly must degrade to "unnamed", never escape into
|
|
84
|
+
# the customer's thread (rule 9).
|
|
85
|
+
"unnamed"
|
|
86
|
+
end
|
|
87
|
+
return noop_tracer.in_span(span_name, &block) if helpers_disabled?
|
|
88
|
+
|
|
89
|
+
get_tracer.in_span(span_name, attributes: stringify(attributes), &block)
|
|
56
90
|
end
|
|
57
91
|
|
|
58
92
|
# Records an exception on the active span (status ERROR) when one is
|
|
59
93
|
# recording. Accepts customer error classes; NEVER notifies a vendor
|
|
60
94
|
# tracker. Deduped per span. Never raises.
|
|
61
95
|
def record_exception(error)
|
|
96
|
+
return nil if helpers_disabled?
|
|
97
|
+
|
|
62
98
|
Errors.record_once(OpenTelemetry::Trace.current_span, error)
|
|
63
99
|
nil
|
|
64
100
|
end
|
|
65
101
|
|
|
66
102
|
def set_attribute(key, value)
|
|
103
|
+
return false if helpers_disabled?
|
|
104
|
+
|
|
67
105
|
span = OpenTelemetry::Trace.current_span
|
|
68
106
|
return false unless span.respond_to?(:recording?) && span.recording?
|
|
69
107
|
|
|
70
|
-
|
|
108
|
+
# Capture-time key pass (rule 18 C.3): when the customer configured
|
|
109
|
+
# redact_keys/redact_pii_keys the value lands ALREADY-MASKED, so tenant
|
|
110
|
+
# processors and inert-mode providers only ever see the redacted view of
|
|
111
|
+
# helper-set data. The exporter pass re-applies (idempotent). With no
|
|
112
|
+
# keys configured this is a pass-through — the value is captured RAW.
|
|
113
|
+
# scrub_utf8 is encoding hygiene, not masking: an invalid-UTF-8 value
|
|
114
|
+
# would make the upstream OTLP encoder drop the WHOLE batch (rule 15).
|
|
115
|
+
span.set_attribute(key.to_s, Redaction.mask_one(key.to_s, Redaction.scrub_utf8(value), active_config))
|
|
71
116
|
true
|
|
72
|
-
rescue StandardError
|
|
117
|
+
rescue StandardError, SystemStackError
|
|
118
|
+
# SystemStackError is not a StandardError: a poisoned key/value (e.g. a
|
|
119
|
+
# recursive #to_s/#hash) must never crash the caller's thread (rule 9).
|
|
73
120
|
false
|
|
74
121
|
end
|
|
75
122
|
|
|
76
123
|
def set_attributes(attributes)
|
|
124
|
+
return false if helpers_disabled?
|
|
125
|
+
|
|
77
126
|
span = OpenTelemetry::Trace.current_span
|
|
78
127
|
return false unless span.respond_to?(:recording?) && span.recording?
|
|
79
128
|
|
|
80
129
|
span.add_attributes(stringify(attributes))
|
|
81
130
|
true
|
|
82
|
-
rescue StandardError
|
|
131
|
+
rescue StandardError, SystemStackError
|
|
83
132
|
false
|
|
84
133
|
end
|
|
85
134
|
|
|
86
135
|
def add_event(name, attributes: nil)
|
|
136
|
+
return false if helpers_disabled?
|
|
137
|
+
|
|
87
138
|
span = OpenTelemetry::Trace.current_span
|
|
88
139
|
return false unless span.respond_to?(:recording?) && span.recording?
|
|
89
140
|
|
|
90
141
|
span.add_event(name.to_s, attributes: stringify(attributes))
|
|
91
142
|
true
|
|
92
|
-
rescue StandardError
|
|
143
|
+
rescue StandardError, SystemStackError
|
|
93
144
|
false
|
|
94
145
|
end
|
|
95
146
|
|
|
@@ -101,7 +152,7 @@ module Foam
|
|
|
101
152
|
return nil if ctx.nil? || !ctx.valid?
|
|
102
153
|
|
|
103
154
|
{ trace_id: ctx.hex_trace_id, span_id: ctx.hex_span_id }
|
|
104
|
-
rescue StandardError
|
|
155
|
+
rescue StandardError, SystemStackError
|
|
105
156
|
nil
|
|
106
157
|
end
|
|
107
158
|
|
|
@@ -128,15 +179,17 @@ module Foam
|
|
|
128
179
|
# is an OTel severity NUMBER (1..24) or a level symbol (:info, :warn,
|
|
129
180
|
# :error, :debug, :fatal, :trace). Safe pre-init (no-op). Never raises.
|
|
130
181
|
def log(severity, body, attributes: nil)
|
|
182
|
+
return true if helpers_disabled? # disabled means fully dark — accepted, silently dropped
|
|
183
|
+
|
|
131
184
|
get_logger.on_emit(
|
|
132
185
|
severity_number: severity_number(severity),
|
|
133
186
|
severity_text: severity_text(severity),
|
|
134
|
-
body: body,
|
|
187
|
+
body: mask_log_body(body),
|
|
135
188
|
attributes: stringify(attributes),
|
|
136
189
|
context: OpenTelemetry::Context.current
|
|
137
190
|
)
|
|
138
191
|
true
|
|
139
|
-
rescue StandardError
|
|
192
|
+
rescue StandardError, SystemStackError
|
|
140
193
|
false
|
|
141
194
|
end
|
|
142
195
|
|
|
@@ -153,7 +206,10 @@ module Foam
|
|
|
153
206
|
def flush
|
|
154
207
|
(@flushables || []).dup.each do |flushable|
|
|
155
208
|
flushable.force_flush
|
|
156
|
-
rescue StandardError
|
|
209
|
+
rescue StandardError, SystemStackError
|
|
210
|
+
# SystemStackError is not a StandardError: a poisoned (e.g. cyclic)
|
|
211
|
+
# payload deep in a pipeline must still never escape into the
|
|
212
|
+
# customer's thread (rule 9).
|
|
157
213
|
nil
|
|
158
214
|
end
|
|
159
215
|
nil
|
|
@@ -165,16 +221,19 @@ module Foam
|
|
|
165
221
|
def shutdown
|
|
166
222
|
@instance&.shutdown
|
|
167
223
|
nil
|
|
168
|
-
rescue StandardError
|
|
224
|
+
rescue StandardError, SystemStackError
|
|
169
225
|
nil
|
|
170
226
|
end
|
|
171
227
|
|
|
172
228
|
# Restart foam's PeriodicMetricReader export threads after a fork (the
|
|
173
229
|
# trace/log batch processors self-heal on their own — GOTCHAS R1).
|
|
174
|
-
#
|
|
175
|
-
#
|
|
230
|
+
# Walks BOTH registries: foam's own door-1 readers AND the door-2
|
|
231
|
+
# @ingest_taps registry (kept separate so a post-fork re-init that
|
|
232
|
+
# retires foam's pipelines never orphans a tap riding the CUSTOMER'S
|
|
233
|
+
# pipeline — GOTCHAS R7). Called automatically by foam's Process._fork
|
|
234
|
+
# hook; also safe to call from Puma on_worker_boot / Unicorn after_fork.
|
|
176
235
|
def after_fork!
|
|
177
|
-
(@metric_readers || []).each do |reader|
|
|
236
|
+
((@metric_readers || []) + (@ingest_taps || [])).each do |reader|
|
|
178
237
|
reader.after_fork if reader.respond_to?(:after_fork)
|
|
179
238
|
rescue StandardError
|
|
180
239
|
nil
|
|
@@ -201,10 +260,59 @@ module Foam
|
|
|
201
260
|
LEVEL_TEXT[severity.to_s.downcase.to_sym]
|
|
202
261
|
end
|
|
203
262
|
|
|
263
|
+
# Stringify keys, then apply the customer's key lists AT CAPTURE (rule 18
|
|
264
|
+
# C.3): when redact_keys/redact_pii_keys are configured, helper-set
|
|
265
|
+
# attributes are ALREADY-MASKED when they reach the span/log record, so a
|
|
266
|
+
# tenant processor (or a foreign provider in inert mode) only ever sees the
|
|
267
|
+
# redacted view of foam-helper data. The exporter-boundary pass re-applies
|
|
268
|
+
# — masking is idempotent. With no keys configured this is a pass-through
|
|
269
|
+
# (raw capture). NEVER throws (rule 9): non-enumerable garbage degrades to
|
|
270
|
+
# nil (attributes dropped, telemetry still flows).
|
|
204
271
|
def stringify(attributes)
|
|
205
272
|
return nil if attributes.nil?
|
|
206
273
|
|
|
207
|
-
|
|
274
|
+
# scrub_utf8 per value: encoding hygiene at capture (see set_attribute
|
|
275
|
+
# — one invalid-UTF-8 value kills its whole OTLP batch upstream).
|
|
276
|
+
stringified = attributes.each_with_object({}) { |(k, v), out| out[k.to_s] = Redaction.scrub_utf8(v) }
|
|
277
|
+
Redaction.mask_attributes(stringified, active_config)
|
|
278
|
+
rescue StandardError, SystemStackError
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The customer key pass over a log BODY at capture (same rationale as
|
|
283
|
+
# stringify). When keys are configured mask_body is fail-closed and
|
|
284
|
+
# depth-guarded, so a cyclic structure is bounded before it can reach any
|
|
285
|
+
# encoder; with no keys configured the body is captured RAW.
|
|
286
|
+
def mask_log_body(body)
|
|
287
|
+
return body if body.nil?
|
|
288
|
+
|
|
289
|
+
Redaction.mask_body(Redaction.scrub_utf8(body), active_config)
|
|
290
|
+
rescue StandardError, SystemStackError
|
|
291
|
+
# Fail CLOSED on a masking failure: in inert mode (a foreign SDK owns
|
|
292
|
+
# logs) the capture-time pass is the ONLY pass on foam-helper log
|
|
293
|
+
# bodies, so returning the raw String after a mask error would ship a
|
|
294
|
+
# half-processed body — REDACTED for every body type instead.
|
|
295
|
+
REDACTED
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# section 0: "enabled: false beats everything — including inert-mode
|
|
299
|
+
# emission. Disabled means fully dark." Helpers consult the resolved
|
|
300
|
+
# config once foam is initialized; before init the proxy providers
|
|
301
|
+
# already make them silent no-ops (and a foreign pre-init provider is
|
|
302
|
+
# the pre-init passthrough contract, unchanged).
|
|
303
|
+
def helpers_disabled?
|
|
304
|
+
@initialized && !active_config.enabled
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# The API's no-op tracer: in_span still yields a (non-recording) span
|
|
308
|
+
# and re-raises only the block's own error — nothing is emitted.
|
|
309
|
+
def noop_tracer
|
|
310
|
+
@noop_tracer ||= OpenTelemetry::Trace::Tracer.new
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The API's no-op logger (see get_logger / GOTCHAS L1).
|
|
314
|
+
def noop_logger
|
|
315
|
+
@noop_logger ||= OpenTelemetry::Logs::Logger.new
|
|
208
316
|
end
|
|
209
317
|
end
|
|
210
318
|
end
|