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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d27bb6b38712364503503fecd37ea383f9a7e3fb3648da8ef41524c575ef9302
4
- data.tar.gz: 0c9fee962999043120904c8ffe85621e32ae04fcb300280ec8af0b2c77291a0d
3
+ metadata.gz: 767e21dce7e53eb8a74e430c736f2ba94e22c882022733fff7fb8edb058c118e
4
+ data.tar.gz: 4d9e7b6789d79daec4eb0482a22ac68520263c0a267e1b4e4def7eae97d9e642
5
5
  SHA512:
6
- metadata.gz: 8bdcb84ca5d9becc44e9a181512ac19c9c8c24f5035e7da9da09a1ab59a4c09cd96b9cde80a8b5d073001ae6f36d9f4e1b3e1a3e640c4eafa6d30ac34800e47c
7
- data.tar.gz: b09d59343870e8d16ef9e4c0794e7ec78486d5ab9c3eea339d99c4f8b4fa46f58ca5246ca3df70ed8a82c795fd4108c296c3bb19e720640c5acce080f0e90a98
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 fleet endpoint), turns on automatic
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.1**;
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 fleet endpoint, and auto-activates the **universal floor** — the
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, fleet-identical with the
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 (ruling
152
- > 2026-07-28). A custom header outside the list is **not captured** unless
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. **Fleet convention: read it from the `FOAM_OTEL_TOKEN` env var.** Blank while enabled → raises at boot. |
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
- **Fleet ruling 2026-07-26** (binding design:
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 — the fleet ruling carves out exactly this list (and only
486
- this list) as non-negotiable. The list is byte-identical in every foam core
487
- and gate-checked in CI against the fleet fixture
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
- **Fleet param-name canon** (the customer options above the floor, identical
491
- concept in every core — the floor itself has NO init surface anywhere):
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 (fleet ruling 2026-07-27,
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 the foam cores,
565
- changed only by fleet ruling.
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: [...] }` (fleet ruling
593
- 2026-07-28; frozen fixture: `contract/pii-detect.json` — the entity names and
594
- placeholders are byte-identical in every foam core and gate-checked in CI by
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 (rule 10): a non-callable — or a **lambda/Method
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
- Fleet canon mapping (pinned by `spec/export_surface_spec.rb`):
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 — the fleet's one emergency switch behaves the same across languages). Door-2 ingest entries honor it too: construction returns an inert instance (read once at construction; changing it implies a restart). |
1097
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | HONORED — the one operator-level override of the pinned fleet 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). |
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
@@ -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
- VERSION = "1.9.0"
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: 1.9.0
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.1'
425
+ version: '3.4'
426
426
  required_rubygems_version: !ruby/object:Gem::Requirement
427
427
  requirements:
428
428
  - - ">="