Class: Philiprehberger::StructuredLogger::Logger

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/structured_logger/logger.rb

Constant Summary collapse

LEVELS =
{ debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }.freeze
CORRELATION_ID_KEY =
:philiprehberger_structured_logger_correlation_id
BACKTRACE_LINE =

Regex matching a single Ruby backtrace line. Captures the file path, the line number, and (optionally) the method name. Handles both Ruby 3.4+ single-quote (‘’method’‘) and Ruby 3.3-and-earlier backtick-apostrophe (“ `method’ “) quoting.

/\A(?<file>.+?):(?<line>\d+)(?::in ['`](?<method>[^']+)')?\z/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**opts) ⇒ Logger

Returns a new instance of Logger.



21
22
23
24
25
26
27
28
29
30
# File 'lib/philiprehberger/structured_logger/logger.rb', line 21

def initialize(**opts)
  @level = opts.fetch(:level, :debug)
  @context = opts.fetch(:context, {}).freeze
  @sampling = opts.fetch(:sampling, {})
  @async = opts.fetch(:async, false)
  @buffer_size = opts.fetch(:buffer_size, 1000)
  @monitor = Monitor.new

  @outputs = OutputBuilder.call(opts, @async, @buffer_size)
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



19
20
21
# File 'lib/philiprehberger/structured_logger/logger.rb', line 19

def context
  @context
end

#levelObject

Returns the value of attribute level.



19
20
21
# File 'lib/philiprehberger/structured_logger/logger.rb', line 19

def level
  @level
end

Instance Method Details

#add_output(io, level: nil, formatter: nil) ⇒ Object



37
38
39
40
41
# File 'lib/philiprehberger/structured_logger/logger.rb', line 37

def add_output(io, level: nil, formatter: nil)
  resolved = StructuredLogger.resolve_formatter(formatter)
  wrapped = @async ? AsyncWriter.new(io, buffer_size: @buffer_size) : io
  @monitor.synchronize { @outputs << { io: wrapped, level: level, formatter: resolved } }
end

#child(**extra) ⇒ Object



43
44
45
46
47
# File 'lib/philiprehberger/structured_logger/logger.rb', line 43

def child(**extra)
  clone = self.class.allocate
  clone.send(:initialize_child, @outputs, @level, @context.merge(extra), @sampling, @monitor)
  clone
end

#closeObject



207
208
209
# File 'lib/philiprehberger/structured_logger/logger.rb', line 207

def close
  @monitor.synchronize { @outputs.each { |out| out[:io].close if out[:io].is_a?(AsyncWriter) } }
end

#flushObject



203
204
205
# File 'lib/philiprehberger/structured_logger/logger.rb', line 203

def flush
  @monitor.synchronize { @outputs.each { |out| out[:io].flush if out[:io].respond_to?(:flush) } }
end

#log_exception(exception, level: :error, structured_backtrace: false, **extra) ⇒ void

This method returns an undefined value.

Logs an exception’s message, class, and backtrace as a single structured entry.

Examples:

Default (raw string backtrace)

logger.log_exception(e)
# backtrace: ["app/foo.rb:42:in 'bar'", ...]

Structured backtrace

logger.log_exception(e, structured_backtrace: true)
# backtrace: [
#   { file: "app/foo.rb", line: 42, method: "bar" },
#   ...
# ]

Parameters:

  • exception (Exception)

    the exception to log.

  • level (Symbol) (defaults to: :error)

    the log level for the entry (default ‘:error`).

  • structured_backtrace (Boolean) (defaults to: false)

    when ‘false` (default), the backtrace is emitted as an array of raw strings (the same shape as `exception.backtrace`). When `true`, each backtrace line is parsed into a hash with `:file`, `:line` (Integer), and (when present) `:method` keys. Lines that don’t match the standard Ruby backtrace format are passed through as ‘{ raw: “<original line>” }`. The parsed form is generally easier to index in log-aggregation systems like Elasticsearch, Datadog, or Loki.

  • extra (Hash)

    additional context merged into the log entry.



142
143
144
145
146
147
148
149
# File 'lib/philiprehberger/structured_logger/logger.rb', line 142

def log_exception(exception, level: :error, structured_backtrace: false, **extra)
  bt = exception.backtrace || []
  bt = parse_backtrace(bt) if structured_backtrace
  log(level, exception.message,
      error_class: exception.class.name,
      backtrace: bt,
      **extra)
end

#measure(event_name, **context) { ... } ⇒ Object

Yields to the given block, measures its monotonic wall-clock duration, and emits a single info-level log entry describing the outcome. On success, the block’s return value is returned. On exception, the failure is logged and the original exception is re-raised.

Examples:

Measuring a database query

logger.measure('db.query', table: 'users') { User.find(1) }
# logs event: 'db.query', table: 'users', duration_ms: 12.345

Parameters:

  • event_name (String, Symbol)

    the event name to record as the ‘event` field in the log entry.

  • context (Hash)

    extra context merged into the log entry.

Yields:

  • executes the measured block.

Returns:

  • (Object)

    the block’s return value on success.

Raises:

  • re-raises any exception raised by the block.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/philiprehberger/structured_logger/logger.rb', line 167

def measure(event_name, **context)
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  begin
    result = yield
    duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0).round(3)
    log(:info, event_name.to_s, event: event_name, duration_ms: duration_ms, **context)
    result
  rescue StandardError => e
    duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0).round(3)
    log(:info, event_name.to_s,
        event: event_name,
        duration_ms: duration_ms,
        error: e.message,
        error_class: e.class.name,
        **context)
    raise
  end
end

#measure_value(event_name, **context) { ... } ⇒ Object

Variant of #measure that emits the same timing log entry but also returns the block’s return value. Captures and re-raises exceptions like #measure.

Examples:

Capturing a query result

result = logger.measure_value('db.query') { query_database }

Parameters:

  • event_name (String, Symbol)

    the event name to record as the ‘event` field in the log entry.

  • context (Hash)

    extra context merged into the log entry.

Yields:

  • executes the measured block.

Returns:

  • (Object)

    the block’s return value on success.

Raises:

  • re-raises any exception raised by the block.



199
200
201
# File 'lib/philiprehberger/structured_logger/logger.rb', line 199

def measure_value(event_name, **context, &)
  measure(event_name, **context, &)
end

#silence(temp_level = :fatal, &block) ⇒ Object



94
95
96
97
98
99
100
101
102
# File 'lib/philiprehberger/structured_logger/logger.rb', line 94

def silence(temp_level = :fatal, &block)
  @monitor.synchronize do
    original = @level
    @level = temp_level
    block.call
  ensure
    @level = original
  end
end

#with_context(**extra, &block) ⇒ Object



49
50
51
52
53
54
55
56
57
# File 'lib/philiprehberger/structured_logger/logger.rb', line 49

def with_context(**extra, &block)
  @monitor.synchronize do
    original = @context
    @context = @context.merge(extra).freeze
    block.call
  ensure
    @context = original
  end
end

#with_correlation_id(id = nil, &block) ⇒ Object



104
105
106
107
108
109
110
111
# File 'lib/philiprehberger/structured_logger/logger.rb', line 104

def with_correlation_id(id = nil, &block)
  id ||= SecureRandom.uuid
  previous = Thread.current[CORRELATION_ID_KEY]
  Thread.current[CORRELATION_ID_KEY] = id
  block.call
ensure
  Thread.current[CORRELATION_ID_KEY] = previous
end

#with_tags(*tags) {|optional| ... } ⇒ Object, Hash

Adds the given tags to the logger’s context under the ‘:tags` key, merging with any existing tags (de-duplicated, preserving insertion order). When a block is given, the previous context is restored when the block exits (even on exception). Without a block, the change persists like #with_context.

Examples:

Block form

logger.with_tags('auth', 'request') do
  logger.info('Login attempt')
  # entry includes tags: ['auth', 'request']
end

Parameters:

  • tags (Array<String, Symbol>)

    one or more tags to add.

Yields:

  • (optional)

    executes within the tagged context; the original context is restored on exit.

Returns:

  • (Object, Hash)

    the block’s return value when a block is given, otherwise the new merged context hash.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/philiprehberger/structured_logger/logger.rb', line 76

def with_tags(*tags)
  existing = @context[:tags] || []
  merged_tags = (existing + tags).uniq
  if block_given?
    @monitor.synchronize do
      original = @context
      @context = @context.merge(tags: merged_tags).freeze
      yield
    ensure
      @context = original
    end
  else
    @monitor.synchronize do
      @context = @context.merge(tags: merged_tags).freeze
    end
  end
end