Class: LaunchDarklyObservability::OtelLogBridge

Inherits:
Logger
  • Object
show all
Defined in:
lib/launchdarkly_observability/otel_log_bridge.rb

Overview

A Logger that forwards messages to the OpenTelemetry Logs pipeline.

When used as a broadcast target (Rails), pass only the logger_provider. When used standalone (Sinatra, plain Ruby), pass ‘io:` to also write to a local destination such as $stdout.

Examples:

Standalone usage (non-Rails)

logger = LaunchDarklyObservability.logger          # writes to $stdout + OTel

Manually attaching (the Railtie does this automatically)

bridge = LaunchDarklyObservability::OtelLogBridge.new(logger_provider)
Rails.logger.broadcast_to(bridge)   # Rails >= 7.1

Constant Summary collapse

SEVERITY_NUMBER =

OpenTelemetry severity numbers (base value per level). See: opentelemetry.io/docs/specs/otel/logs/data-model/#severity-fields

{
  ::Logger::DEBUG   => 5,
  ::Logger::INFO    => 9,
  ::Logger::WARN    => 13,
  ::Logger::ERROR   => 17,
  ::Logger::FATAL   => 21,
  ::Logger::UNKNOWN => 0
}.freeze
SEVERITY_TEXT =
{
  ::Logger::DEBUG   => 'DEBUG',
  ::Logger::INFO    => 'INFO',
  ::Logger::WARN    => 'WARN',
  ::Logger::ERROR   => 'ERROR',
  ::Logger::FATAL   => 'FATAL',
  ::Logger::UNKNOWN => 'UNKNOWN'
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(logger_provider, io: nil) ⇒ OtelLogBridge

Returns a new instance of OtelLogBridge.

Parameters:

  • logger_provider (OpenTelemetry::SDK::Logs::LoggerProvider)
  • io (IO, nil) (defaults to: nil)

    Optional IO for local output (e.g. $stdout). When nil the bridge only emits to OTel (suitable for broadcast).



41
42
43
44
45
46
47
48
# File 'lib/launchdarkly_observability/otel_log_bridge.rb', line 41

def initialize(logger_provider, io: nil)
  super(File::NULL)
  @otel_logger = logger_provider.logger(
    name: 'launchdarkly-observability-ruby',
    version: LaunchDarklyObservability::VERSION
  )
  @local_logger = io ? ::Logger.new(io) : nil
end

Instance Method Details

#add(severity, message = nil, progname = nil) ⇒ Object

Core method that debug/info/warn/error/fatal all delegate to.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/launchdarkly_observability/otel_log_bridge.rb', line 63

def add(severity, message = nil, progname = nil)
  severity ||= ::Logger::UNKNOWN
  return true if severity < level

  if message.nil?
    if block_given?
      message = yield
    else
      message = progname
      progname = nil
    end
  end

  return true if message.nil?

  attributes = {}
  if message.is_a?(Hash)
    attributes = message.each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
    body = message.inspect
  else
    body = message.to_s
  end

  begin
    @otel_logger.on_emit(
      body: body,
      severity_number: SEVERITY_NUMBER.fetch(severity, 0),
      severity_text: SEVERITY_TEXT.fetch(severity, 'UNKNOWN'),
      timestamp: Time.now,
      context: OpenTelemetry::Context.current,
      attributes: attributes
    )
  rescue StandardError
    # OTel export failures must not suppress local IO output.
  end

  begin
    @local_logger&.add(severity, message, progname)
  rescue StandardError
    # Local IO failures must not propagate.
  end

  true
end

#formatter=(formatter) ⇒ Object

Propagate formatter changes to the local logger.



57
58
59
60
# File 'lib/launchdarkly_observability/otel_log_bridge.rb', line 57

def formatter=(formatter)
  super
  @local_logger&.formatter = formatter
end

#level=(severity) ⇒ Object

Propagate level changes to the local logger so filtering stays in sync.



51
52
53
54
# File 'lib/launchdarkly_observability/otel_log_bridge.rb', line 51

def level=(severity)
  super
  @local_logger&.level = severity
end