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
-
.emit(logger, severity, message, progname) ⇒ Object
Emit one OTel log record for a message the host logger accepted.
-
.emit? ⇒ Boolean
The emit-time gate: foam initialized, enabled, and OWNING the logs slot (never a producer on a foreign pipeline — rule 18 B).
-
.install! ⇒ Object
Idempotent prepend onto ::Logger.
- .installed? ⇒ Boolean
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, , 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()), 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).
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.}") false end |
.installed? ⇒ Boolean
67 |
# File 'lib/foam/otel/logger_bridge.rb', line 67 def installed? = @installed |