Class: Foam::Otel::RedactingSpanExporter
- Inherits:
-
Object
- Object
- Foam::Otel::RedactingSpanExporter
- Defined in:
- lib/foam/otel/redacting_exporter.rb
Overview
The customer's opt-in key pass (redact_keys / redact_pii_keys) runs HERE, at the exporter boundary. 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. With NO keys configured (the default) this is a pass-through — every value ships RAW. (Foam's own HELPERS additionally apply the same key pass at capture — api.rb stringify / set_attribute — so the tenant seam sees helper-set data already masked when keys are configured.)
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).
Documented Ruby divergence (RESEARCH.md / GOTCHAS F1): because attributes freeze at finish, a tenant additional_span_processor sees THIRD-PARTY INSTRUMENTATION attributes unmasked — the "tenant sees already masked" guarantee (rule 18 C) holds for foam-captured/helper-set data and the redact() helper, but foam cannot mask third-party instrumentation attributes before the tenant processor in Ruby.
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.
37 38 39 40 |
# File 'lib/foam/otel/redacting_exporter.rb', line 37 def initialize(inner, config) @inner = inner @config = config end |
Instance Method Details
#export(span_data, timeout: nil) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/foam/otel/redacting_exporter.rb', line 42 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
61 |
# File 'lib/foam/otel/redacting_exporter.rb', line 61 def force_flush(timeout: nil) = @inner.force_flush(timeout: timeout) |
#shutdown(timeout: nil) ⇒ Object
62 |
# File 'lib/foam/otel/redacting_exporter.rb', line 62 def shutdown(timeout: nil) = @inner.shutdown(timeout: timeout) |