foam-otel 1.9.0 → 2.0.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/README.md +29 -25
- data/lib/foam/otel/before_send.rb +36 -0
- data/lib/foam/otel/version.rb +20 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 767e21dce7e53eb8a74e430c736f2ba94e22c882022733fff7fb8edb058c118e
|
|
4
|
+
data.tar.gz: 4d9e7b6789d79daec4eb0482a22ac68520263c0a267e1b4e4def7eae97d9e642
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4512202149fa0ee2b50093b7485b07373a09ed29315b6489d3e59d0c111ecf3d023e77e49f9af271ca97df33210e2028ba3376e4a43516284bd6f0fe195a8627
|
|
7
|
+
data.tar.gz: 9d15c2c183dfdac3ca20f0a3eccae5c6f2e45134206a697604a1b70e7a9046c775d0b5e805662a162aee2b0d89e62f810cdea8e43a72b9f15db783f1b07ccb0d
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Foam's OpenTelemetry core for Ruby services. A thin, safe wrapper over the
|
|
4
4
|
official OpenTelemetry libraries: foam owns the pipeline (providers, batch
|
|
5
|
-
processors, OTLP export to the foam
|
|
5
|
+
processors, OTLP export to the foam endpoint), turns on automatic
|
|
6
6
|
tier-1/2 instrumentation, and hands you a small set of never-throw helpers —
|
|
7
7
|
and nothing else. **Redaction is fully opt-in above two always-on exceptions:
|
|
8
8
|
by default foam captures every value RAW** (no PII preset) **except (1) the
|
|
@@ -42,7 +42,8 @@ gem "opentelemetry-instrumentation-graphql" # e.g. GraphQL
|
|
|
42
42
|
gem "opentelemetry-instrumentation-resque" # e.g. Resque
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
**Supported versions** (tested in CI, `spec/version_spec.rb`): Ruby **>= 3.
|
|
45
|
+
**Supported versions** (tested in CI, `spec/version_spec.rb`): Ruby **>= 3.4**
|
|
46
|
+
(the version the CI matrix runs — the floor follows the tested matrix);
|
|
46
47
|
OpenTelemetry API **`opentelemetry-api` ~> 1.1** (1.x). The metrics and logs
|
|
47
48
|
SDKs are pre-1.0 upstream (metrics alpha, logs development) and are pinned with
|
|
48
49
|
pessimistic constraints; foam wraps them behind stable helpers so app code never
|
|
@@ -69,7 +70,7 @@ Foam::Otel.init(
|
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
**Outcome**: foam registers the tracer/meter/logger providers, exports OTLP to
|
|
72
|
-
the foam
|
|
73
|
+
the foam endpoint, and auto-activates the **universal floor** — the
|
|
73
74
|
official rack/rails instrumentation plus the bundled HTTP (Net::HTTP/Faraday),
|
|
74
75
|
datastore (pg/mysql2/redis/mongo), and Sidekiq instrumentations — each
|
|
75
76
|
presence-gated, and gated on foam owning the traces slot (a foreign-owned
|
|
@@ -111,7 +112,7 @@ turns keep their tool names/arguments and tool-result turns keep their
|
|
|
111
112
|
`tool_call_id` linkage inside the message content. `gen_ai.operation.name`
|
|
112
113
|
(and the `{operation} {model}` span name) follows the semconv operation for
|
|
113
114
|
each API shape — `chat` for the OpenAI/Anthropic/ruby_llm chat seams,
|
|
114
|
-
`generate_content` for Gemini's generateContent,
|
|
115
|
+
`generate_content` for Gemini's generateContent, identical to foam's
|
|
115
116
|
js/python Gemini paths (**wire change in 1.2.1**: Gemini spans previously
|
|
116
117
|
said `chat`). Covered: the official `openai` SDK
|
|
117
118
|
(chat.completions.create + responses.create), the official `anthropic` SDK
|
|
@@ -148,8 +149,8 @@ listed request and response headers as
|
|
|
148
149
|
> **Honest limit (deliberate divergence from foam's js/python cores):**
|
|
149
150
|
> Ruby inbound captures the **named default list below, NOT all headers** —
|
|
150
151
|
> the official gem's options are an enumerated allowlist with no
|
|
151
|
-
> capture-all form, and that official option is the chosen seam
|
|
152
|
-
>
|
|
152
|
+
> capture-all form, and that official option is the chosen seam.
|
|
153
|
+
> A custom header outside the list is **not captured** unless
|
|
153
154
|
> you extend the list (recipe below).
|
|
154
155
|
|
|
155
156
|
The default lists (`Foam::Otel::HeaderCapture::DEFAULT_REQUEST_HEADERS` /
|
|
@@ -321,7 +322,7 @@ not captured — inbound only.
|
|
|
321
322
|
| `name:` | String | yes | — | `service.name`. Blank → raises at boot. |
|
|
322
323
|
| `environment:` | String | yes | — | `deployment.environment.name`, exported verbatim. A value outside `{production, staging, development, test}` warns but is never rewritten. Blank → raises. |
|
|
323
324
|
| `enabled:` | Boolean | yes | — | The config switch. `false` = fully inert (no SDK, no providers, no network, helpers no-op). `true` = export, in EVERY environment. No default — you write the logic. |
|
|
324
|
-
| `token:` | String | when enabled | — | `Authorization: Bearer` for export. Required (and validated) only when `enabled: true`; wired explicitly from your secret source — never an env fallback read by the gem. **
|
|
325
|
+
| `token:` | String | when enabled | — | `Authorization: Bearer` for export. Required (and validated) only when `enabled: true`; wired explicitly from your secret source — never an env fallback read by the gem. **The convention across all foam SDKs: read it from the `FOAM_OTEL_TOKEN` env var.** Blank while enabled → raises at boot. |
|
|
325
326
|
| `version:` | String | no | nil | `service.version`, verbatim (git SHA recommended). Missing → warns and continues. Never detected at runtime. |
|
|
326
327
|
| `redact:` | Hash | no | nil | The grouped redaction object (1.5.0, the preferred surface): `{ secrets: [...], pii: [...], detect: [...] }` — symbol or string keys. `secrets:` has `redact_keys` semantics (tail mask) and `pii:` has `redact_pii_keys` semantics (full `[REDACTED]`); when both a legacy option and its `redact` field are set, the lists UNION. `detect:` is the opt-in PII detection tier's entity list — value-shape detection with typed placeholders (see "PII detection (opt-in)" below). An unknown field, a non-Array value, or an unknown `detect` entity name raises at boot. Absent (the default) → behavior byte-identical to before. |
|
|
327
328
|
| `redact_keys:` | Array<String> | no | `[]` | Alias — the preferred spelling is `redact: { secrets: [...] }` (same semantics; the lists union when both are set). The ONLY field names tail-masked (shape-preserving, e.g. `********f456`). Opt-in and ADDITIVE on top of the always-on credential floor — empty (the default) means no masking beyond the floor. Matches by case-insensitive substring, including keys nested in structured values and query-string keys. A key that is also on the floor stays fully `[REDACTED]` (the floor wins; it is never downgraded to a tail). |
|
|
@@ -432,8 +433,7 @@ override is the operator-level `OTEL_EXPORTER_OTLP_ENDPOINT` env var (below).
|
|
|
432
433
|
|
|
433
434
|
## The default credential denylist (the always-on floor)
|
|
434
435
|
|
|
435
|
-
|
|
436
|
-
`docs/decisions/credential-denylist-design.md`; frozen fixture:
|
|
436
|
+
Always on in every foam SDK, identical everywhere (frozen fixture:
|
|
437
437
|
`contract/credential-denylist.json`). This is the ONE exception to foam's
|
|
438
438
|
raw-by-default capture, and it has **no off switch** — no init option, no env
|
|
439
439
|
var, and no door-2 parameter can disable, shrink, or re-spell it.
|
|
@@ -482,13 +482,13 @@ byte-identical raw, exactly as documented everywhere else in this README.
|
|
|
482
482
|
|
|
483
483
|
**Why.** Coverage-over-masking remains the mission, but raw `Authorization`
|
|
484
484
|
headers, cookies, and API keys on the wire are a breach in waiting for every
|
|
485
|
-
customer at once —
|
|
486
|
-
this list) as non-negotiable. The list is byte-identical in every foam
|
|
487
|
-
and gate-checked in CI against the
|
|
485
|
+
customer at once — foam carves out exactly this list (and only
|
|
486
|
+
this list) as non-negotiable. The list is byte-identical in every foam SDK
|
|
487
|
+
and gate-checked in CI against the shipped fixture
|
|
488
488
|
(`spec/credential_floor_spec.rb`).
|
|
489
489
|
|
|
490
|
-
**
|
|
491
|
-
concept in every
|
|
490
|
+
**Param names across the foam SDKs** (the customer options above the floor,
|
|
491
|
+
identical concept in every SDK — the floor itself has NO init surface anywhere):
|
|
492
492
|
|
|
493
493
|
| Concept | js/otel | js/browser | python | ruby | java |
|
|
494
494
|
|---|---|---|---|---|---|
|
|
@@ -517,8 +517,7 @@ The credential floor masks by NAME and is deliberately value-shape-blind — a
|
|
|
517
517
|
live AWS key under the field name `note` would sail past it. Foam telemetry is
|
|
518
518
|
read downstream by LLMs, so a leaked credential is exfiltratable by prompt
|
|
519
519
|
injection; "no leakage at all" is the bar. The value-pattern secret layer is
|
|
520
|
-
the second always-on control
|
|
521
|
-
`docs/decisions/security-fixes-design.md`):
|
|
520
|
+
the second always-on control:
|
|
522
521
|
|
|
523
522
|
**What it masks.** Credential-SHAPED value spans, regardless of the field
|
|
524
523
|
name they hide under: AWS access-key ids and keyword-anchored AWS secrets,
|
|
@@ -561,8 +560,8 @@ value rather than shipping it raw.
|
|
|
561
560
|
|
|
562
561
|
**Provenance.** The ruleset derives from MIT/Apache-licensed OSS secret
|
|
563
562
|
scanners (gitleaks, detect-secrets, secretlint) — see `THIRD-PARTY-NOTICES`.
|
|
564
|
-
Rules are frozen module constants, byte-consistent across
|
|
565
|
-
|
|
563
|
+
Rules are frozen module constants, byte-consistent across all foam SDKs —
|
|
564
|
+
not configurable per app.
|
|
566
565
|
|
|
567
566
|
> **Migration note — 1.4.0 (minor): value-pattern secret masking + redaction
|
|
568
567
|
> coverage fix + TLS pin.** foam now masks credential-SHAPED values
|
|
@@ -589,9 +588,9 @@ The floor and the secret layer protect credentials. PII in VALUES — an email
|
|
|
589
588
|
address inside a log line, a card number inside an exception message — still
|
|
590
589
|
exports RAW by default, because coverage-over-masking is the mission and only
|
|
591
590
|
you know your privacy posture. As of 1.5.0 you can opt into value-shape PII
|
|
592
|
-
detection per entity, via `redact: { detect: [...] }` (
|
|
593
|
-
|
|
594
|
-
placeholders are byte-identical in every foam
|
|
591
|
+
detection per entity, via `redact: { detect: [...] }` (frozen fixture:
|
|
592
|
+
`contract/pii-detect.json` — the entity names and
|
|
593
|
+
placeholders are byte-identical in every foam SDK and gate-checked in CI by
|
|
595
594
|
`spec/pii_detect_spec.rb`). Nothing detects unless you list the entity —
|
|
596
595
|
an empty/absent `detect` list is exactly today's behavior.
|
|
597
596
|
|
|
@@ -672,6 +671,11 @@ The per-record contract (Sentry `beforeSend` semantics):
|
|
|
672
671
|
in place, they are shared structures. Foam re-normalizes the record's
|
|
673
672
|
`total_recorded_*` bookkeeping after your hooks run, so adding/deleting
|
|
674
673
|
attributes never corrupts the wire's dropped-count accounting.
|
|
674
|
+
`record.resource` and `record.instrumentation_scope` are **identity, not
|
|
675
|
+
payload**: read them freely, but edits and replacements are discarded —
|
|
676
|
+
the original identity objects always ship (they are process-global and
|
|
677
|
+
the encoder groups by them; a writable resource would also bypass
|
|
678
|
+
redaction, which never masks resource attributes).
|
|
675
679
|
* **return `nil`** → the record is dropped, silently (that is the filter
|
|
676
680
|
mechanism, not an error).
|
|
677
681
|
* **raise, or return a foreign object** → that record is dropped
|
|
@@ -691,7 +695,7 @@ at-exit flush. Anything a hook needs from the request/job thread
|
|
|
691
695
|
attribute at capture time (see the `additional_span_processors` stamper
|
|
692
696
|
pattern); thread-locals are gone by the time the hook runs.
|
|
693
697
|
|
|
694
|
-
Boot validation is strict
|
|
698
|
+
Boot validation is strict: a non-callable — or a **lambda/Method
|
|
695
699
|
whose signature cannot accept one positional argument** (e.g. a Sentry-port
|
|
696
700
|
`->(event, hint) { ... }` with two required params, or a required keyword)
|
|
697
701
|
— raises `ArgumentError` at `init`, because it would otherwise fault on
|
|
@@ -958,7 +962,7 @@ which every tap applies with zero configuration; `diagnostics:` is tap-scoped
|
|
|
958
962
|
narration). Construction
|
|
959
963
|
validates loudly at boot and NEVER
|
|
960
964
|
throws afterwards — a failure inside a tap can never break their pipeline.
|
|
961
|
-
|
|
965
|
+
Naming across the foam SDKs (pinned by `spec/export_surface_spec.rb`):
|
|
962
966
|
`create_ingest_span_processor` = `createFoamIngestSpanProcessor`,
|
|
963
967
|
`create_ingest_log_record_processor` = `createFoamIngestLogRecordProcessor`,
|
|
964
968
|
`create_ingest_metric_reader` = `createFoamIngestMetricReader`.
|
|
@@ -1093,8 +1097,8 @@ end
|
|
|
1093
1097
|
|
|
1094
1098
|
| Var | Posture |
|
|
1095
1099
|
| --- | --- |
|
|
1096
|
-
| `OTEL_SDK_DISABLED=true` | HONORED — full kill switch, supersedes `enabled: true` (matched trimmed + case-insensitive, so `TRUE` / ` true ` also kill —
|
|
1097
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT` | HONORED — the one operator-level override of the pinned
|
|
1100
|
+
| `OTEL_SDK_DISABLED=true` | HONORED — full kill switch, supersedes `enabled: true` (matched trimmed + case-insensitive, so `TRUE` / ` true ` also kill — this one emergency switch behaves the same in every foam SDK). Door-2 ingest entries honor it too: construction returns an inert instance (read once at construction; changing it implies a restart). |
|
|
1101
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | HONORED — the one operator-level override of the pinned foam endpoint (for foam's own conformance rig / enterprise egress). Active → loud `[foam]` warning naming the destination. Applies to door-2 taps identically (and moves the host the required loop step must name). |
|
|
1098
1102
|
| `OTEL_PROPAGATORS=none` | HONORED — turns trace propagation OFF (links lost) while telemetry keeps flowing; warns. Any other value warns and is ignored (foam's propagator set is fixed: W3C tracecontext + baggage). |
|
|
1099
1103
|
| `OTEL_BSP_SCHEDULE_DELAY` / `OTEL_BLRP_SCHEDULE_DELAY` / `OTEL_METRIC_EXPORT_INTERVAL` | HONORED — batch cadence, read natively by the upstream SDK. |
|
|
1100
1104
|
| `FOAM_CAPTURE_PAYLOADS` | HONORED — the operator clamp over the `capture_payloads:` init option (`off`/`errors`/`always`, case-insensitive), overriding it in BOTH directions with one loud `[foam]` line when it changes the mode. An invalid value warns and falls back to the init option (never crashes a boot). This is the ONE foam-named env var the gem reads — an override valve over an init-declared option, never an on/off switch, token, or config fallback (those still arrive only through `init`'s explicit arguments). Read once at init; changing it implies a restart. |
|
|
@@ -62,6 +62,7 @@ module Foam
|
|
|
62
62
|
# flood an adversary-shaped record stream controls — rule 15's
|
|
63
63
|
# "loud but never a flood" posture).
|
|
64
64
|
def run(hooks, record, signal, faults)
|
|
65
|
+
identity = identity_of(record)
|
|
65
66
|
current, counters = thaw(record)
|
|
66
67
|
hooks.each do |hook|
|
|
67
68
|
result = hook.call(current)
|
|
@@ -74,6 +75,7 @@ module Foam
|
|
|
74
75
|
current = result
|
|
75
76
|
end
|
|
76
77
|
restore_counters!(current, counters)
|
|
78
|
+
restore_identity!(current, identity)
|
|
77
79
|
current
|
|
78
80
|
rescue StandardError, SystemStackError => e
|
|
79
81
|
faults << e.class.name
|
|
@@ -110,9 +112,43 @@ module Foam
|
|
|
110
112
|
copy.attributes = attributes.nil? ? {} : attributes.transform_values { |v| thaw_value(v) }
|
|
111
113
|
end
|
|
112
114
|
copy.body = thaw_body(copy.body) if copy.respond_to?(:body) && copy.respond_to?(:body=)
|
|
115
|
+
# The scope handed to the hook is a COPY: InstrumentationScope is a
|
|
116
|
+
# mutable Struct, and an in-place `scope.name =` on the shared
|
|
117
|
+
# object would corrupt process-global identity (see identity_of).
|
|
118
|
+
if copy.respond_to?(:instrumentation_scope) && copy.respond_to?(:instrumentation_scope=)
|
|
119
|
+
copy.instrumentation_scope = copy.instrumentation_scope&.dup
|
|
120
|
+
end
|
|
113
121
|
[copy, sdk_drop_counters(record)]
|
|
114
122
|
end
|
|
115
123
|
|
|
124
|
+
# resource / instrumentation_scope are process-global IDENTITY objects
|
|
125
|
+
# shared by every record the tracer/logger emits — and Ruby's
|
|
126
|
+
# InstrumentationScope is a plain MUTABLE Struct. A hook that renames
|
|
127
|
+
# the scope would corrupt the identity of EVERY subsequent span/log
|
|
128
|
+
# from that instrument (proven live in the 2026-07-29 retro-probe),
|
|
129
|
+
# and a wholesale `record.resource =` replacement would ship
|
|
130
|
+
# hook-authored resource attributes RAW — redaction never masks
|
|
131
|
+
# resource, so that path could widen what ships. Both are closed the
|
|
132
|
+
# way the Go port does it: identity always comes from the ORIGINAL
|
|
133
|
+
# record — captured here, force-restored onto the survivor after the
|
|
134
|
+
# pipeline (hook edits to identity are discarded; the encoder's
|
|
135
|
+
# group-by-identity stays intact). Documented in the README:
|
|
136
|
+
# resource/instrumentation_scope are informational, not hook-writable.
|
|
137
|
+
def identity_of(record)
|
|
138
|
+
{
|
|
139
|
+
resource: (record.resource if record.respond_to?(:resource)),
|
|
140
|
+
scope: (record.instrumentation_scope if record.respond_to?(:instrumentation_scope)),
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def restore_identity!(record, identity)
|
|
145
|
+
record.resource = identity[:resource] if record.respond_to?(:resource=)
|
|
146
|
+
if record.respond_to?(:instrumentation_scope=)
|
|
147
|
+
record.instrumentation_scope = identity[:scope]
|
|
148
|
+
end
|
|
149
|
+
record
|
|
150
|
+
end
|
|
151
|
+
|
|
116
152
|
def thaw_value(value)
|
|
117
153
|
case value
|
|
118
154
|
when String then value.dup
|
data/lib/foam/otel/version.rb
CHANGED
|
@@ -175,6 +175,25 @@ module Foam
|
|
|
175
175
|
# case) install NOTHING. Proven against the real agents in
|
|
176
176
|
# test-apps/ruby-coexistence (six vendors, both patch mechanisms,
|
|
177
177
|
# patched-client probe REQUIRED green).
|
|
178
|
-
|
|
178
|
+
# 1.9.1: before_send IDENTITY RESTORE (retro-probe class 14; PATCH —
|
|
179
|
+
# bugfix-only). resource / instrumentation_scope are process-global
|
|
180
|
+
# identity objects, and InstrumentationScope is a MUTABLE Struct: a
|
|
181
|
+
# hook renaming it corrupted the identity of every subsequent
|
|
182
|
+
# span/log from that instrument (proven live), and a wholesale
|
|
183
|
+
# `record.resource =` replacement shipped hook-authored resource
|
|
184
|
+
# attributes RAW (redaction never masks resource — the one
|
|
185
|
+
# can-never-widen gap). Hooks now receive a COPIED scope, and the
|
|
186
|
+
# ORIGINAL resource/scope are force-restored onto every survivor
|
|
187
|
+
# after the pipeline (identity is not hook-writable — the Go port's
|
|
188
|
+
# contract; README updated).
|
|
189
|
+
# 2.0.0: RUNTIME FLOOR RAISE (label-follows-tested-matrix ruling
|
|
190
|
+
# 2026-07-29). required_ruby_version >= 3.1 -> >= 3.4 — the version the
|
|
191
|
+
# CI matrix actually runs. The 3.1 floor was an unproven published
|
|
192
|
+
# promise: no CI leg ran below 3.4, and on 3.1–3.2 (both EOL) Bundler
|
|
193
|
+
# resolves an OLDER upstream otel gem set than the proven lockfile —
|
|
194
|
+
# a combination never tested. MAJOR: narrowing the supported-runtime
|
|
195
|
+
# label is breaking for installs on the dropped versions. No wire/API
|
|
196
|
+
# change — every 1.x contract above still holds.
|
|
197
|
+
VERSION = "2.0.0"
|
|
179
198
|
end
|
|
180
199
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foam-otel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foam
|
|
@@ -422,7 +422,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
422
422
|
requirements:
|
|
423
423
|
- - ">="
|
|
424
424
|
- !ruby/object:Gem::Version
|
|
425
|
-
version: '3.
|
|
425
|
+
version: '3.4'
|
|
426
426
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
427
427
|
requirements:
|
|
428
428
|
- - ">="
|