Module: Foam::Otel::LoggerBridge

Defined in:
lib/foam/otel/logger_bridge.rb

Overview

The stdlib Logger bridge (console-logs ruling 2026-07-26: packages own every safe log seam; Ruby's is ::Logger). Every message an app logger actually emits (severity >= the logger's level) is ALSO emitted as an OTel log record on foam's log pipeline — body RAW (opt-in redaction only), trace-correlated via the current context, severity mapped to the OTel numbers. Rails.logger, Sidekiq's logger, and every hand-rolled Logger.new($stdout) all funnel through Logger#add, so one prepend covers the ecosystem.

Activation is gated on foam OWNING the logs slot (init.rb — foam never adds producers to a foreign pipeline, rule 18 B) and the patch body re-checks at emit time, so a bridge installed in the parent stays correct across re-inits.

LOOP GUARDS (both required):

1. Re-entrancy: emitting a log record can itself log (the OTLP
 exporter and the SDK narrate failures through a Logger) — a
 thread-local flag makes any Logger#add reached FROM the bridge's
 own emit a plain passthrough.
2. The OTel SDK's internal logger (`OpenTelemetry.logger`) is skipped
 by identity: bridging it would turn every export failure into a
 new log record that itself must be exported — unbounded growth
 that the per-stack re-entrancy flag cannot see (the batch thread
 is a different stack).

Never raises into the host logger (rule 9): every bridge failure degrades to the stdlib call alone.

Defined Under Namespace

Modules: Patch

Constant Summary collapse

SEVERITIES =

Logger::Severity → [OTel severity_number, severity_text]. UNKNOWN (stdlib's "always logged" catch-all) has no OTel level semantics — it rides as INFO-weight with its own text, never dropped.

{
  0 => [5, "DEBUG"].freeze,   # Logger::DEBUG
  1 => [9, "INFO"].freeze,    # Logger::INFO
  2 => [13, "WARN"].freeze,   # Logger::WARN
  3 => [17, "ERROR"].freeze,  # Logger::ERROR
  4 => [21, "FATAL"].freeze,  # Logger::FATAL
  5 => [9, "UNKNOWN"].freeze, # Logger::UNKNOWN
}.freeze
REENTRANCY_KEY =
:__foam_otel_logger_bridge

Class Method Summary collapse

Class Method Details

.emit(logger, severity, message, progname) ⇒ Object

Emit one OTel log record for a message the host logger accepted. Body rides RAW; a non-String message (Exception, Object) degrades to inspect — never a mutation of what the host logger wrote.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/foam/otel/logger_bridge.rb', line 83

def emit(logger, severity, message, progname)
  return if skip_logger?(logger)

  Thread.current[REENTRANCY_KEY] = true
  severity_number, severity_text = SEVERITIES.fetch(severity, SEVERITIES[5])
  attributes = {}
  attributes["logger.name"] = Redaction.scrub_utf8(progname.to_s) if progname && !progname.to_s.empty?
  # scrub_utf8 is encoding hygiene, not masking (the same capture-time
  # pass api.rb's log() applies): host loggers routinely carry binary
  # junk, and ONE invalid-UTF-8 body makes the upstream OTLP encoder
  # drop the WHOLE log batch (rule 15 — the silent-loss shape the
  # scrub exists to prevent). Content is otherwise untouched: raw
  # capture stands.
  Foam::Otel.get_logger("foam-logger-bridge").on_emit(
    severity_number: severity_number,
    severity_text: severity_text,
    body: Redaction.scrub_utf8(body_string(message)),
    attributes: attributes,
    context: OpenTelemetry::Context.current
  )
rescue StandardError, SystemStackError
  nil # the host's own log line already happened; the bridge is best-effort
ensure
  Thread.current[REENTRANCY_KEY] = false
end

.emit?Boolean

The emit-time gate: foam initialized, enabled, and OWNING the logs slot (never a producer on a foreign pipeline — rule 18 B).

Returns:

  • (Boolean)


71
72
73
74
75
76
77
78
# File 'lib/foam/otel/logger_bridge.rb', line 71

def emit?
  return false unless @installed
  return false if Thread.current[REENTRANCY_KEY]

  Foam::Otel.send(:foam_exports_signal?, :logs) && !Foam::Otel.send(:helpers_disabled?)
rescue StandardError
  false
end

.install!Object

Idempotent prepend onto ::Logger. Called from init's activation path only when the logs slot is foam-owned; a process where stdlib Logger was never loaded (require "logger" is transitive of the SDK today, but never assumed) stays untouched.



56
57
58
59
60
61
62
63
64
65
# File 'lib/foam/otel/logger_bridge.rb', line 56

def install!
  return false unless defined?(::Logger)
  return true if @installed

  ::Logger.prepend(Patch)
  @installed = true
rescue StandardError => e
  Diagnostics.warn("stdlib Logger bridge failed to install: #{e.class}: #{e.message}")
  false
end

.installed?Boolean

Returns:

  • (Boolean)


67
# File 'lib/foam/otel/logger_bridge.rb', line 67

def installed? = @installed