Class: Foam::Otel::RedactingSpanExporter

Inherits:
Object
  • Object
show all
Defined in:
lib/foam/otel/redacting_exporter.rb

Overview

The always-on secrets floor runs HERE, at the exporter boundary (BASE_PACKAGE_SPEC rule 14). 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. (Foam's own HELPERS additionally mask at capture — api.rb stringify / set_attribute — so the tenant seam sees helper-set data already masked; this floor is the unconditional backstop for everything else.)

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 (rule 14): Redaction.mask_* return NIL on a hard failure — a nil entry is DROPPED with a loud [foam] warning (never exported raw) 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, TODO pcga11): 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 (masked at capture) and the redact() helper, but foam cannot mask third-party instrumentation attributes before the tenant processor in Ruby. Foam's own export is always masked.

Instance Method Summary collapse

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)