foam-otel 1.7.0 → 1.8.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 +67 -0
- data/lib/foam/otel/before_send.rb +153 -0
- data/lib/foam/otel/config.rb +37 -4
- data/lib/foam/otel/init.rb +6 -2
- data/lib/foam/otel/pipelines.rb +8 -0
- data/lib/foam/otel/version.rb +16 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e034361554ad48530554a1bdc62d75adf1899e3ef57104a367abd2c412d2f93
|
|
4
|
+
data.tar.gz: 40b6db9d7f11ed9105b00c9998b87da51e0822480d97da52be766ddea24435ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c71df1b9ce568dabd996b659d13e0bc05e93a81e070376a77e8c09ccd6a6fc51dcaa028c19a904e78d38daae532c6bc75077e3fa5331047aa1c64e08a9609ee
|
|
7
|
+
data.tar.gz: 90623d934372eb908d8c9d13e5ddd4108faad54bea4c5a647e7934959562ff130c2cdfdc2f0c5c5cc00a1a86eb5fcc4ee03c745c69c8c258fced2caf6ceb2f31
|
data/README.md
CHANGED
|
@@ -331,6 +331,7 @@ not captured — inbound only.
|
|
|
331
331
|
| `additional_metric_readers:` | Array | no | `[]` | Tenant seam, metrics. |
|
|
332
332
|
| `additional_instrumentations:` | Array | no | `[]` | Constructed tier-2 instrumentation instances to register (fault-isolated: one that throws is skipped with a `[foam]` warning). |
|
|
333
333
|
| `ignored_outbound_hosts:` | Array<String> | no | `[]` | Hosts whose outbound calls produce no spans — EXTENDS the built-in export-loop guard. For a co-resident agent's intake or a tenant exporter's endpoint. Applies to the clients whose official instrumentation supports host suppression: **Net::HTTP and Excon**. Faraday/HTTP (httprb)/HTTPX have no upstream `untraced_hosts` option — foam warns loudly at init when one of those is bundled (GOTCHAS G13); foam's own export loop is guarded for every client regardless. |
|
|
334
|
+
| `before_send:` | Proc/Array | no | `[]` | Export hook (1.8.0): one callable — or an Array run as a pipeline — called with every **span and log record** at the export boundary, after batching and BEFORE foam's redaction pass and serialization, so nothing a hook drops ever leaves the process. Return the record to export it (mutating it in place is supported — `record.attributes` arrives unfrozen), or `nil` to drop it. A hook that raises (or returns anything else) drops **that record** loudly and the batch reports failure — fail-closed, never a raise into your threads. Runs BEFORE redaction, so the credential floor and your redact lists still apply to whatever the hook returns. Spans and logs only (metrics are aggregated state, not records). A non-callable raises at boot. Process-global: a second `init` never changes it. See "The `before_send` hook" below. |
|
|
334
335
|
| `capture_payloads:` | Symbol/String | no | `:off` | Inbound HTTP **body** capture on the rack server span (see "Payload (body) capture"). `:off` = zero teeing, zero middleware (the default); `:errors` = tee per request, attach only on exception/5xx; `:always` = attach on every request. Equivalent strings accepted; anything else raises at boot. Overridden in both directions by the `FOAM_CAPTURE_PAYLOADS` env clamp (env table below). Textual payloads only, 8192-char cap + `…[truncated]`, true `.size` attributes; sizes-only for binary/compressed. |
|
|
335
336
|
| `diagnostics:` | Boolean | no | false | Verbose `[foam]` self-reporting of init/health. Warnings and errors are always loud regardless. |
|
|
336
337
|
| `secret_heuristics:` | Boolean | no | true | The value-pattern secret layer's HEURISTIC tier (generic keyword+entropy detection, see "The value-pattern secret layer"). `false` disables ONLY the heuristics — the named provider patterns, the credential floor and the redaction-coverage contract have no off switch. Disabling logs one loud `[foam]` line at init (an explicit, audited decision for telemetry whose legitimate values collide with the heuristics). |
|
|
@@ -388,6 +389,24 @@ Foam::Otel.init(name: "checkout-api", environment: env, enabled: true, token: EN
|
|
|
388
389
|
ignored_outbound_hosts: ["ingest.eval-tool.example"]) # the tenant loop guard — required
|
|
389
390
|
```
|
|
390
391
|
|
|
392
|
+
```ruby
|
|
393
|
+
# The before_send export hook (1.8.0): transform or drop spans/logs right
|
|
394
|
+
# before they are exported to foam — after batching, BEFORE redaction and
|
|
395
|
+
# serialization (a dropped record never leaves the process). Return the
|
|
396
|
+
# record (mutate in place freely) or nil to drop; a raising hook drops ONLY
|
|
397
|
+
# its record, loudly, and never raises into your threads (fail-closed).
|
|
398
|
+
Foam::Otel.init(name: "checkout-api", environment: env, enabled: true, token: ENV.fetch("FOAM_OTEL_TOKEN"),
|
|
399
|
+
before_send: lambda { |record|
|
|
400
|
+
next nil if record.respond_to?(:name) && record.name == "GET /healthz" # drop noise
|
|
401
|
+
|
|
402
|
+
record.attributes.delete("internal.debug.blob") # trim before it ships
|
|
403
|
+
record
|
|
404
|
+
})
|
|
405
|
+
# Or an Array pipeline — hooks run in order, each receiving the previous
|
|
406
|
+
# hook's return; the first nil drops the record and short-circuits:
|
|
407
|
+
# Foam::Otel.init(..., before_send: [DropHealthchecks, TrimDebugAttrs, StampTeam])
|
|
408
|
+
```
|
|
409
|
+
|
|
391
410
|
```ruby
|
|
392
411
|
# Verbose self-reporting while wiring foam up (warnings are loud regardless):
|
|
393
412
|
Foam::Otel.init(name: "checkout-api", environment: "development", enabled: true,
|
|
@@ -616,6 +635,54 @@ processor = Foam::Otel.create_ingest_span_processor(
|
|
|
616
635
|
|
|
617
636
|
---
|
|
618
637
|
|
|
638
|
+
## The `before_send` hook
|
|
639
|
+
|
|
640
|
+
`before_send:` is the last word your code gets on every span and log record
|
|
641
|
+
before it is exported to foam. It runs at the **export boundary** — after the
|
|
642
|
+
batch processor hands foam the finished records, immediately **before**
|
|
643
|
+
foam's redaction pass and the OTLP serialization — so a record your hook
|
|
644
|
+
drops is never serialized and never leaves the process. (This is the same
|
|
645
|
+
seam redaction lives at, and for the same Ruby SDK reason: a span's
|
|
646
|
+
attributes freeze at finish before any `on_finish` processor runs, so a
|
|
647
|
+
`SpanProcessor` could neither mutate nor drop. The export boundary is the
|
|
648
|
+
first point where the record is a mutable struct.)
|
|
649
|
+
|
|
650
|
+
The pipeline order is pinned:
|
|
651
|
+
|
|
652
|
+
```
|
|
653
|
+
BatchProcessor → before_send hooks → redaction (floor + your lists) → OTLP wire
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Your hooks run **first**, on the raw record (it is your own data, still
|
|
657
|
+
in-process), and redaction runs on whatever they return — so `before_send`
|
|
658
|
+
can never widen what ships: an attribute a hook adds is masked by the
|
|
659
|
+
credential floor and your redact lists exactly like one an instrumentation
|
|
660
|
+
set.
|
|
661
|
+
|
|
662
|
+
The per-record contract (Sentry `beforeSend` semantics):
|
|
663
|
+
|
|
664
|
+
* **return the record** → it continues to the next hook / to export.
|
|
665
|
+
Mutating it in place is supported — `record.attributes` arrives as an
|
|
666
|
+
unfrozen copy, so `record.attributes["k"] = v` and
|
|
667
|
+
`record.attributes.delete("k")` just work.
|
|
668
|
+
* **return `nil`** → the record is dropped, silently (that is the filter
|
|
669
|
+
mechanism, not an error).
|
|
670
|
+
* **raise, or return anything else** → that record is dropped **loudly**
|
|
671
|
+
(`[foam]` warning) and the batch reports failure — fail-closed: a faulted
|
|
672
|
+
hook never ships a record you may have meant to scrub, and never raises
|
|
673
|
+
into your application or export threads. Healthy records in the same batch
|
|
674
|
+
still export.
|
|
675
|
+
|
|
676
|
+
Scope: **spans and log records** (the two record-shaped signals — the hook
|
|
677
|
+
receives `SpanData` or `LogRecordData`; distinguish them with `is_a?` or
|
|
678
|
+
`respond_to?`). Metrics are aggregated state, not discrete records, and are
|
|
679
|
+
deliberately not routed through `before_send`. Door-2 ingest taps are
|
|
680
|
+
unchanged. Like `enabled:` and the endpoint, `before_send` is process-global:
|
|
681
|
+
the exporter chain is built at the first `init`, and a second `init` never
|
|
682
|
+
changes it.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
619
686
|
## The helpers
|
|
620
687
|
|
|
621
688
|
All helpers never raise, and no-op silently before `init` and when disabled.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "diagnostics"
|
|
4
|
+
|
|
5
|
+
module Foam
|
|
6
|
+
module Otel
|
|
7
|
+
# The customer's `before_send:` hook(s) (1.8.0), run at the EXPORT
|
|
8
|
+
# BOUNDARY — after batching, immediately BEFORE foam's redaction pass and
|
|
9
|
+
# the OTLP serialization, so a record a hook drops is never serialized
|
|
10
|
+
# and never leaves the process. Why not a SpanProcessor: the same Ruby
|
|
11
|
+
# SDK constraint that forced redaction to the exporter boundary — a span
|
|
12
|
+
# freezes its attributes at finish BEFORE any on_finish processor runs
|
|
13
|
+
# (GOTCHAS F1), so a processor could neither mutate nor reliably drop.
|
|
14
|
+
# The mutable SpanData / LogRecordData Structs the batch processors hand
|
|
15
|
+
# the exporter are the first (and last) point where per-record transform
|
|
16
|
+
# + drop is possible before the wire.
|
|
17
|
+
#
|
|
18
|
+
# Composition order (pinned, pipelines.rb):
|
|
19
|
+
# BatchProcessor -> BeforeSend::*Exporter -> Redacting*Exporter -> OTLP
|
|
20
|
+
# The hooks run FIRST so the credential floor, the value-pattern secret
|
|
21
|
+
# layer and the customer's key lists still apply to whatever a hook
|
|
22
|
+
# returns — before_send can never widen what ships past redaction (an
|
|
23
|
+
# attribute a hook ADDS is masked exactly like one an instrumentation
|
|
24
|
+
# set).
|
|
25
|
+
#
|
|
26
|
+
# Contract per record (Sentry beforeSend semantics — the spec's named
|
|
27
|
+
# fail-closed precedent, BASE_PACKAGE_SPEC D6):
|
|
28
|
+
# * return the record (mutating it in place is fine) → next hook /
|
|
29
|
+
# export;
|
|
30
|
+
# * return nil → record DROPPED (the intentional filter mechanism —
|
|
31
|
+
# success, not a failure);
|
|
32
|
+
# * raise, or return a foreign object → record DROPPED, loudly, and
|
|
33
|
+
# the batch reports FAILURE (fail-closed: a hook fault must never
|
|
34
|
+
# ship a record the customer may have meant to scrub, and must never
|
|
35
|
+
# raise into the export thread — rules 9/14/15). SystemStackError is
|
|
36
|
+
# rescued explicitly (not a StandardError): a hook recursing on a
|
|
37
|
+
# poisoned payload kills neither the batch thread nor the caller.
|
|
38
|
+
# The record reaches the first hook as a shallow dup with an UNFROZEN
|
|
39
|
+
# attributes copy (span attributes freeze at finish — GOTCHAS F1), so
|
|
40
|
+
# `record.attributes["k"] = v` just works; the dup also keeps the
|
|
41
|
+
# customer's mutations off the struct any other consumer might hold.
|
|
42
|
+
#
|
|
43
|
+
# Scope: spans and logs (the two record-shaped signals). Metric data is
|
|
44
|
+
# aggregated state, not records — deliberately NOT routed through
|
|
45
|
+
# before_send (documented in the README).
|
|
46
|
+
module BeforeSend
|
|
47
|
+
# Internal sentinel distinguishing a FAULTED drop (counts toward batch
|
|
48
|
+
# FAILURE) from an intentional nil drop. A hook can never return it
|
|
49
|
+
# legitimately — any non-record return is itself classified a fault.
|
|
50
|
+
ERROR = Object.new.freeze
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
# Run the hook pipeline over one record: each hook receives the
|
|
55
|
+
# previous hook's return. Returns the surviving record, nil for an
|
|
56
|
+
# intentional drop, or ERROR for a faulted one.
|
|
57
|
+
def run(hooks, record, signal)
|
|
58
|
+
current = thaw(record)
|
|
59
|
+
hooks.each do |hook|
|
|
60
|
+
result = hook.call(current)
|
|
61
|
+
return nil if result.nil?
|
|
62
|
+
|
|
63
|
+
unless result.is_a?(record.class)
|
|
64
|
+
Diagnostics.warn("before_send returned a #{result.class} for a #{signal} record — record " \
|
|
65
|
+
"dropped (return the record, a replacement of the same type, or nil to drop; " \
|
|
66
|
+
"fail-closed, rule 14)")
|
|
67
|
+
return ERROR
|
|
68
|
+
end
|
|
69
|
+
current = result
|
|
70
|
+
end
|
|
71
|
+
current
|
|
72
|
+
rescue StandardError, SystemStackError => e
|
|
73
|
+
Diagnostics.warn("before_send raised for a #{signal} record — record dropped, never exported " \
|
|
74
|
+
"half-transformed (fail-closed, rule 14): #{e.class}: #{e.message}")
|
|
75
|
+
ERROR
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A shallow dup with an unfrozen attributes copy — the hook gets its
|
|
79
|
+
# own mutable record (span attributes arrive frozen; GOTCHAS F1).
|
|
80
|
+
def thaw(record)
|
|
81
|
+
copy = record.dup
|
|
82
|
+
if copy.respond_to?(:attributes) && copy.respond_to?(:attributes=)
|
|
83
|
+
attributes = copy.attributes
|
|
84
|
+
copy.attributes = attributes.nil? ? {} : attributes.dup
|
|
85
|
+
end
|
|
86
|
+
copy
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The span-side wrapper (composition over the redacting exporter, same
|
|
90
|
+
# wrap-never-subclass posture as redacting_exporter.rb: only the public
|
|
91
|
+
# export/force_flush/shutdown contract the batch processors call).
|
|
92
|
+
class SpanExporter
|
|
93
|
+
def initialize(inner, config)
|
|
94
|
+
@inner = inner
|
|
95
|
+
@hooks = config.before_send
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def export(span_data, timeout: nil)
|
|
99
|
+
batch = span_data.to_a
|
|
100
|
+
faulted = 0
|
|
101
|
+
kept = batch.filter_map do |sd|
|
|
102
|
+
result = BeforeSend.run(@hooks, sd, :span)
|
|
103
|
+
if result.equal?(ERROR)
|
|
104
|
+
faulted += 1
|
|
105
|
+
nil
|
|
106
|
+
else
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
status = kept.empty? ? OpenTelemetry::SDK::Trace::Export::SUCCESS : @inner.export(kept, timeout: timeout)
|
|
111
|
+
faulted.positive? ? OpenTelemetry::SDK::Trace::Export::FAILURE : status
|
|
112
|
+
rescue StandardError, SystemStackError
|
|
113
|
+
# Fail closed past the per-record guards, loudly (rule 15) —
|
|
114
|
+
# never raise into the batch thread.
|
|
115
|
+
Diagnostics.warn("before_send pass raised past its guards — span batch dropped (fail-closed, rule 14/15)")
|
|
116
|
+
OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def force_flush(timeout: nil) = @inner.force_flush(timeout: timeout)
|
|
120
|
+
def shutdown(timeout: nil) = @inner.shutdown(timeout: timeout)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class LogRecordExporter
|
|
124
|
+
def initialize(inner, config)
|
|
125
|
+
@inner = inner
|
|
126
|
+
@hooks = config.before_send
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def export(log_record_data, timeout: nil)
|
|
130
|
+
batch = log_record_data.to_a
|
|
131
|
+
faulted = 0
|
|
132
|
+
kept = batch.filter_map do |lrd|
|
|
133
|
+
result = BeforeSend.run(@hooks, lrd, :log)
|
|
134
|
+
if result.equal?(ERROR)
|
|
135
|
+
faulted += 1
|
|
136
|
+
nil
|
|
137
|
+
else
|
|
138
|
+
result
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
status = kept.empty? ? OpenTelemetry::SDK::Logs::Export::SUCCESS : @inner.export(kept, timeout: timeout)
|
|
142
|
+
faulted.positive? ? OpenTelemetry::SDK::Logs::Export::FAILURE : status
|
|
143
|
+
rescue StandardError, SystemStackError
|
|
144
|
+
Diagnostics.warn("before_send pass raised past its guards — log batch dropped (fail-closed, rule 14/15)")
|
|
145
|
+
OpenTelemetry::SDK::Logs::Export::FAILURE
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def force_flush(timeout: nil) = @inner.force_flush(timeout: timeout)
|
|
149
|
+
def shutdown(timeout: nil) = @inner.shutdown(timeout: timeout)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/foam/otel/config.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Foam
|
|
|
13
13
|
:name, :environment, :version, :enabled,
|
|
14
14
|
:redact_keys, :redact_pii_keys, :redact_detect, :ignored_outbound_hosts,
|
|
15
15
|
:diagnostics, :endpoint, :secret_heuristics, :capture_payloads,
|
|
16
|
+
:before_send,
|
|
16
17
|
keyword_init: true
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -40,7 +41,8 @@ module Foam
|
|
|
40
41
|
redact_detect: [].freeze,
|
|
41
42
|
ignored_outbound_hosts: [].freeze,
|
|
42
43
|
diagnostics: false, endpoint: FOAM_OTEL_ENDPOINT,
|
|
43
|
-
secret_heuristics: true, capture_payloads: :off
|
|
44
|
+
secret_heuristics: true, capture_payloads: :off,
|
|
45
|
+
before_send: [].freeze
|
|
44
46
|
).freeze
|
|
45
47
|
end
|
|
46
48
|
|
|
@@ -53,7 +55,7 @@ module Foam
|
|
|
53
55
|
def resolve_config(name:, environment:, version:, enabled:,
|
|
54
56
|
redact_keys:, redact_pii_keys:, ignored_outbound_hosts:,
|
|
55
57
|
diagnostics:, endpoint:, secret_heuristics: true, redact: nil,
|
|
56
|
-
capture_payloads: :off)
|
|
58
|
+
capture_payloads: :off, before_send: nil)
|
|
57
59
|
keys, pii, detect = merged_redact_lists(redact_keys, redact_pii_keys, redact)
|
|
58
60
|
# capture_payloads is validated LOUDLY at boot (rule 10, exactly like
|
|
59
61
|
# the redact: object): a value outside CAPTURE_PAYLOAD_MODES (or its
|
|
@@ -89,12 +91,39 @@ module Foam
|
|
|
89
91
|
# — zero teeing, zero middleware) | :errors | :always, canonical
|
|
90
92
|
# symbol form. init.rb applies the FOAM_CAPTURE_PAYLOADS operator
|
|
91
93
|
# env clamp BEFORE this resolves.
|
|
92
|
-
capture_payloads: capture_mode
|
|
94
|
+
capture_payloads: capture_mode,
|
|
95
|
+
# The customer's export hook(s) (1.8.0): one callable or an Array
|
|
96
|
+
# pipeline, validated LOUDLY at boot (rule 10 — a non-callable is a
|
|
97
|
+
# programming error). Stored as a frozen Array; empty (the default)
|
|
98
|
+
# means the exporter chain gains no before_send stage at all.
|
|
99
|
+
before_send: validate_before_send!(before_send)
|
|
93
100
|
).freeze
|
|
94
101
|
end
|
|
95
102
|
|
|
96
103
|
private
|
|
97
104
|
|
|
105
|
+
# ---- the before_send option (1.8.0) ------------------------------------
|
|
106
|
+
# Loud at boot (rule 10, same posture as capture_payloads and the
|
|
107
|
+
# redact: object): before_send must be one callable (responds to
|
|
108
|
+
# #call — a lambda, a Proc, a Method, or any hook object) or an Array
|
|
109
|
+
# of callables run as a pipeline. Anything else — including a nil or
|
|
110
|
+
# non-callable INSIDE the array — raises here, never a silent no-op
|
|
111
|
+
# that leaves the customer believing their filter runs.
|
|
112
|
+
def validate_before_send!(value, context: "Foam::Otel.init")
|
|
113
|
+
hooks = if value.nil?
|
|
114
|
+
[]
|
|
115
|
+
else
|
|
116
|
+
value.is_a?(Array) ? value : [value]
|
|
117
|
+
end
|
|
118
|
+
hooks.each_with_index do |hook, index|
|
|
119
|
+
next if hook.respond_to?(:call)
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "#{context} before_send: must be a callable (responds to #call) or an " \
|
|
122
|
+
"Array of callables, got #{hook.class} at position #{index}"
|
|
123
|
+
end
|
|
124
|
+
hooks.freeze
|
|
125
|
+
end
|
|
126
|
+
|
|
98
127
|
# ---- the capture_payloads option (payload-capture mandate 2026-07-28)
|
|
99
128
|
# Canonicalize a mode value: the three symbols, or their string
|
|
100
129
|
# spellings trimmed + case-insensitive, map to the canonical symbol;
|
|
@@ -212,7 +241,11 @@ module Foam
|
|
|
212
241
|
# the first init: middleware insertion already happened (or
|
|
213
242
|
# deliberately did not — an :off boot shipped none), so a second
|
|
214
243
|
# init cannot meaningfully flip it. Carried forward unchanged.
|
|
215
|
-
capture_payloads: existing.capture_payloads
|
|
244
|
+
capture_payloads: existing.capture_payloads,
|
|
245
|
+
# before_send is process-global too: the exporter chain was built
|
|
246
|
+
# (with or without its before_send stage) at the first init and a
|
|
247
|
+
# second init never rebuilds pipelines. Carried forward unchanged.
|
|
248
|
+
before_send: existing.before_send
|
|
216
249
|
).freeze
|
|
217
250
|
end
|
|
218
251
|
end
|
data/lib/foam/otel/init.rb
CHANGED
|
@@ -59,7 +59,8 @@ module Foam
|
|
|
59
59
|
ignored_outbound_hosts: nil,
|
|
60
60
|
diagnostics: false,
|
|
61
61
|
secret_heuristics: true,
|
|
62
|
-
capture_payloads: :off
|
|
62
|
+
capture_payloads: :off,
|
|
63
|
+
before_send: nil)
|
|
63
64
|
# Identity is validated at boot (rule 10): a blank required value is a
|
|
64
65
|
# programming error the engineer must catch on their machine.
|
|
65
66
|
validate_present!(:name, name)
|
|
@@ -96,7 +97,10 @@ module Foam
|
|
|
96
97
|
# FOAM_CAPTURE_PAYLOADS operator env clamp applied first (a valid
|
|
97
98
|
# env value overrides the option in BOTH directions; an invalid
|
|
98
99
|
# one warns + falls back — never a crashed boot).
|
|
99
|
-
capture_payloads: resolve_capture_payloads(capture_payloads)
|
|
100
|
+
capture_payloads: resolve_capture_payloads(capture_payloads),
|
|
101
|
+
# The customer's export hook(s): a non-callable raises INSIDE
|
|
102
|
+
# resolve_config, loudly, at boot (rule 10).
|
|
103
|
+
before_send: before_send
|
|
100
104
|
)
|
|
101
105
|
# The heuristic-tier opt-out is an explicit, audited customer decision
|
|
102
106
|
# (security-fixes-design §V.8) — always one loud line, never silent.
|
data/lib/foam/otel/pipelines.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "delegate"
|
|
|
8
8
|
require "openssl"
|
|
9
9
|
|
|
10
10
|
require_relative "redacting_exporter"
|
|
11
|
+
require_relative "before_send"
|
|
11
12
|
require_relative "session_stitching"
|
|
12
13
|
|
|
13
14
|
module Foam
|
|
@@ -33,6 +34,11 @@ module Foam
|
|
|
33
34
|
ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER
|
|
34
35
|
)
|
|
35
36
|
exporter = RedactingSpanExporter.new(inner, config)
|
|
37
|
+
# The customer's before_send hooks (1.8.0) wrap OUTSIDE redaction:
|
|
38
|
+
# batch → before_send → redaction → OTLP, so the hooks see the record
|
|
39
|
+
# first and the credential floor still masks whatever they return
|
|
40
|
+
# (before_send.rb). No hooks configured → no stage at all.
|
|
41
|
+
exporter = BeforeSend::SpanExporter.new(exporter, config) unless config.before_send.empty?
|
|
36
42
|
# Pin the sampler explicitly: the TracerProvider constructor otherwise
|
|
37
43
|
# reads OTEL_TRACES_SAMPLER, which an operator (or a stale fleet-wide
|
|
38
44
|
# var) could set to always_off and silently drop EVERY foam span
|
|
@@ -87,6 +93,8 @@ module Foam
|
|
|
87
93
|
ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER # F-RB1 pin — see setup_traces
|
|
88
94
|
)
|
|
89
95
|
exporter = RedactingLogRecordExporter.new(inner, config)
|
|
96
|
+
# before_send stage, same shape and ordering as setup_traces above.
|
|
97
|
+
exporter = BeforeSend::LogRecordExporter.new(exporter, config) unless config.before_send.empty?
|
|
90
98
|
# Pin the log-record limits at the SDK defaults — LogRecordLimits
|
|
91
99
|
# otherwise reads OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT /
|
|
92
100
|
# OTEL_(LOGRECORD_)ATTRIBUTE_VALUE_LENGTH_LIMIT (same env family as
|
data/lib/foam/otel/version.rb
CHANGED
|
@@ -109,6 +109,21 @@ module Foam
|
|
|
109
109
|
# customer redact lists reach inside the body JSON). Rails auto-wiring
|
|
110
110
|
# (Railtie + init fallback) inserts the middleware ONLY when the mode
|
|
111
111
|
# is not :off; plain Rack adds one `use` line.
|
|
112
|
-
|
|
112
|
+
# 1.8.0: `before_send:` EXPORT HOOK (MINOR — one additive init option,
|
|
113
|
+
# default absent, zero default-path behavior change). One callable or
|
|
114
|
+
# an Array pipeline, run per record at the EXPORT BOUNDARY — after
|
|
115
|
+
# batching, BEFORE foam's redaction pass and the OTLP serialization —
|
|
116
|
+
# so a dropped record never leaves the process, and whatever a hook
|
|
117
|
+
# returns still rides the full credential floor / value layer /
|
|
118
|
+
# customer-key pass (before_send can never widen what ships). Spans
|
|
119
|
+
# and logs only (metrics are aggregated state, not records). Return
|
|
120
|
+
# the record (in-place mutation supported — attributes arrive
|
|
121
|
+
# unfrozen) to keep it, nil to drop it; a hook that raises or returns
|
|
122
|
+
# a foreign object drops the record LOUDLY and the batch reports
|
|
123
|
+
# FAILURE (fail-closed, Sentry beforeSend precedent, spec D6). A
|
|
124
|
+
# non-callable raises at boot; process-global like enabled/endpoint
|
|
125
|
+
# (a second init never changes it). Door-1 only — the door-2 ingest
|
|
126
|
+
# taps are unchanged.
|
|
127
|
+
VERSION = "1.8.0"
|
|
113
128
|
end
|
|
114
129
|
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.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foam
|
|
@@ -378,6 +378,7 @@ files:
|
|
|
378
378
|
- lib/foam-otel.rb
|
|
379
379
|
- lib/foam/otel.rb
|
|
380
380
|
- lib/foam/otel/api.rb
|
|
381
|
+
- lib/foam/otel/before_send.rb
|
|
381
382
|
- lib/foam/otel/classifier.rb
|
|
382
383
|
- lib/foam/otel/config.rb
|
|
383
384
|
- lib/foam/otel/constants.rb
|