Class: Foam::Otel::RedactingSpanExporter
- Inherits:
-
Object
- Object
- Foam::Otel::RedactingSpanExporter
- Defined in:
- lib/foam/otel/redacting_exporter.rb
Overview
The central enforcement point: the ALWAYS-ON CREDENTIAL FLOOR (constants.rb / redaction.rb — fleet ruling 2026-07-26) plus the customer's opt-in key pass (redact_keys / redact_pii_keys) run HERE, at the exporter boundary, on door 1 AND door 2. Why not a SpanProcessor: the Ruby SDK freezes a span's attributes at finish, BEFORE any on_finish processor runs (opentelemetry-sdk span.rb — GOTCHAS entry), so no processor can mask them. SpanData / LogRecordData / Event are mutable Structs the exporter can rebuild, so masking happens on the copy foam serializes. The pass runs UNCONDITIONALLY — the floor has no off switch; values under unmatched names ship RAW. (Foam's own HELPERS additionally apply the same pass at capture — api.rb stringify / set_attribute — and the tenant seam hands additional_* processors a masked VIEW — pipelines.rb — so tenants only ever see masked data.)
These wrap (never subclass) the real OTLP exporter by composition, so they are robust across exporter versions — they only rely on the public export/force_flush/shutdown contract the batch processors call.
FAIL CLOSED: once a key pass has begun, Redaction.mask_* return NIL on a hard failure — a nil entry is DROPPED with a loud [foam] warning (never a half-masked record) and the batch reports FAILURE. If masking somehow raises past its own guards, the whole batch is dropped. SystemStackError is rescued explicitly (it is not a StandardError): a poisoned payload must kill neither the batch thread nor the caller (rule 9 / rule 15).
The F1 tenant divergence is CLOSED (1.3.0, credential-floor design §5.4): attributes still freeze at finish, so they cannot be masked IN PLACE before a tenant additional_* processor — instead the guarded tenant wrappers (pipelines.rb) hand tenants a read-only MASKED VIEW built through the same Redaction pass these exporters apply, third-party instrumentation attributes included.
Instance Method Summary collapse
- #export(span_data, timeout: nil) ⇒ Object
- #force_flush(timeout: nil) ⇒ Object
-
#initialize(inner, config) ⇒ RedactingSpanExporter
constructor
A new instance of RedactingSpanExporter.
- #shutdown(timeout: nil) ⇒ Object
Constructor Details
#initialize(inner, config) ⇒ RedactingSpanExporter
Returns a new instance of RedactingSpanExporter.
40 41 42 43 |
# File 'lib/foam/otel/redacting_exporter.rb', line 40 def initialize(inner, config) @inner = inner @config = config end |
Instance Method Details
#export(span_data, timeout: nil) ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/foam/otel/redacting_exporter.rb', line 45 def export(span_data, timeout: nil) batch = span_data.to_a masked = batch.filter_map { |sd| Redaction.mask_span_data(sd, @config) } dropped = batch.length - masked.length if dropped.positive? Diagnostics.warn("redaction failed for #{dropped} span(s) — dropped rather than exported " \ "unmasked (rule 14 fail-closed)") @inner.export(masked, timeout: timeout) unless masked.empty? return OpenTelemetry::SDK::Trace::Export::FAILURE end @inner.export(masked, timeout: timeout) rescue StandardError, SystemStackError # Fail closed: if masking somehow raises past its own guards, drop the # batch rather than export unmasked. Never break the caller — and never # SILENTLY (rule 15): the rarest, most alarming drop must still be loud. Diagnostics.warn("span redaction raised past its guards — batch dropped (fail-closed, rule 14/15)") OpenTelemetry::SDK::Trace::Export::FAILURE end |
#force_flush(timeout: nil) ⇒ Object
64 |
# File 'lib/foam/otel/redacting_exporter.rb', line 64 def force_flush(timeout: nil) = @inner.force_flush(timeout: timeout) |
#shutdown(timeout: nil) ⇒ Object
65 |
# File 'lib/foam/otel/redacting_exporter.rb', line 65 def shutdown(timeout: nil) = @inner.shutdown(timeout: timeout) |