Class: PostHog::Rails::Logs::Appender Private

Inherits:
Logger
  • Object
show all
Defined in:
lib/posthog/rails/logs/appender.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

A ‘Logger`-compatible sink that forwards each log record to an OpenTelemetry logger as an OTLP log record.

It is designed to be broadcast alongside the app’s existing ‘Rails.logger` so that ordinary `Rails.logger.info(…)` calls flow to PostHog Logs in addition to the normal output. Each record is stamped with the request-scoped PostHog identity captured by RequestContext.

Thread-safety: intentionally lock-free apart from the optional rate limiter’s counter. Emitting touches no shared mutable state (‘@otel_logger` is assigned once, attributes are built per call, and `Internal::Context.current` is thread/fiber-local), and the OTel BatchLogRecordProcessor synchronizes its buffer internally — the same split as stdlib `Logger`, which locks in `LogDevice`, not `Logger#add`. A mutex around emit would serialize all app logging needlessly.

Constant Summary collapse

SELF_LOG_PREFIX =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

'[posthog-ruby]'
SELF_LOG_PROGNAME =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

'PostHog'
REQUEST_ATTRIBUTE_NAMES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Maps PostHog event-property names (as stored in Internal::Context) to the OTel semantic-convention attribute names used on log records, matching the web SDK so one filter works across SDKs.

{
  '$current_url' => 'url.full',
  '$request_method' => 'http.request.method',
  '$request_path' => 'url.path'
}.freeze
REENTRANCY_KEY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Re-entrancy guard key. Fiber-local (Thread.current[]), which is what recursion needs: if anything inside #add logs through a broadcast that includes this appender (e.g. a logs_before_send callback calling Rails.logger), the nested call would recurse until SystemStackError —which, as an Exception, escapes the rescue below and breaks the app.

:posthog_rails_logs_emitting

Instance Method Summary collapse

Constructor Details

#initialize(otel_logger, level: nil, rate_limiter: nil, before_send: nil) ⇒ Appender

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Appender.

Parameters:

  • otel_logger (#on_emit)

    An OpenTelemetry logger.

  • level (Integer, nil) (defaults to: nil)

    Minimum severity to forward.

  • rate_limiter (PostHog::Rails::Logs::RateLimiter, nil) (defaults to: nil)

    Optional cap on forwarded records, protecting the ingestion quota from runaway log volume.

  • before_send (#call, nil) (defaults to: nil)

    Optional callback invoked with each record hash (:timestamp, :severity, :body, :attributes — where :severity is a symbol such as :warn) before it is emitted. Return a (possibly modified) hash to send, or nil to drop — useful for scrubbing PII. If the callback raises, the record is dropped.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/posthog/rails/logs/appender.rb', line 52

def initialize(otel_logger, level: nil, rate_limiter: nil, before_send: nil)
  super(nil)
  @otel_logger = otel_logger
  @rate_limiter = rate_limiter
  @before_send = before_send
  # The forwarding threshold deliberately does NOT live in Logger#level.
  # Rails 7.1+ BroadcastLogger computes #level as the min and #debug?
  # etc. as the any? across sinks, so storing it there would widen the
  # app-wide predicates (logs_level = :debug would flip
  # Rails.logger.debug? true and make e.g. ActiveRecord start
  # generating SQL debug lines), and a broadcast-wide
  # `Rails.logger.level =` would clobber the configured logs_level.
  # Pinning the inherited level to UNKNOWN keeps this sink invisible
  # to those calculations; filtering happens against @threshold in #add.
  @threshold = level || ::Logger::DEBUG
  self.level = ::Logger::UNKNOWN
end

Instance Method Details

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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Mirrors ‘Logger#add` message/progname resolution, then emits to OTel instead of writing to a log device.

Returns:

  • (Boolean)

    Always true so it composes with broadcast loggers.



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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/posthog/rails/logs/appender.rb', line 81

def add(severity, message = nil, progname = nil)
  return true if Thread.current[REENTRANCY_KEY]

  begin
    Thread.current[REENTRANCY_KEY] = true

    severity ||= ::Logger::UNKNOWN
    return true if severity < @threshold

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

    return true if message.nil?
    return true if self_log?(message, progname)

    record = apply_before_send(build_record(severity, message, progname))
    return true if record.nil?

    case @rate_limiter&.record
    when :reject
      return true
    when :reject_first
      emit_rate_cap_notice
      return true
    end

    emit(record)
    true
  rescue StandardError => e
    # Never let log forwarding break the calling code path, but leave
    # one breadcrumb: a persistent emit failure would otherwise drop
    # 100% of records with no signal anywhere.
    warn_emit_error(e)
    true
  ensure
    Thread.current[REENTRANCY_KEY] = nil
  end
end