Class: Hyperion::Logger

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

Overview

Structured logger.

Usage:

logger = Hyperion::Logger.new
logger.info  { { message: 'listening', host: '127.0.0.1', port: 9292 } }
logger.warn  { { message: 'parse error', error: e.message, error_class: e.class.name } }
logger.error 'plain string also works for legacy callers'

Level is set from:

1. The `level:` constructor kwarg (highest precedence).
2. ENV['HYPERION_LOG_LEVEL'] if set.
3. Defaults to :info.

Format is :text (key=value), :json (JSONL), or :auto (default — picks the right one based on the runtime environment, see #pick_format below).

Each log line is prefixed with timestamp + level + a ‘hyperion’ tag so operators can grep multi-process worker output. When the resolved format is :text and the underlying IO is a TTY, level names are ANSI-coloured for readability.

Constant Summary collapse

LEVELS =
{ debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }.freeze
LEVEL_COLORS =
{
  debug: "\e[90m", # bright black / grey
  info: "\e[32m",  # green
  warn: "\e[33m",  # yellow
  error: "\e[31m",  # red
  fatal: "\e[35m"   # magenta
}.freeze
COLOR_RESET =

magenta

"\e[0m"
PRODUCTION_ENVS =
%w[production staging].freeze
ERROR_LEVELS =

Levels at WARN or higher are routed to the error stream (stderr by default). info / debug go to the regular stream (stdout by default). 12-factor: app logs to stdout, errors to stderr.

%i[warn error fatal].freeze
ACCESS_FLUSH_BYTES =

Per-thread access-log buffer flush threshold. ~32 average-size lines per write(2) call, well under PIPE_BUF (4096) so writes stay atomic. Larger = fewer syscalls but higher latency-to-disk (up to ~32 reqs of delay before the line shows up in the log file). 4 KiB is a good balance: a 16-thread fleet at 24k r/s flushes ~750 buffers/sec total vs ~24 000 syscalls/sec without buffering.

4096

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(out: $stdout, err: $stderr, io: nil, level: nil, format: nil) ⇒ Logger

Returns a new instance of Logger.



49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/hyperion/logger.rb', line 49

def initialize(out: $stdout, err: $stderr, io: nil, level: nil, format: nil)
  # `io:` is a back-compat alias for tests / single-IO use cases — it
  # routes both streams to the same target (e.g. a StringIO in specs).
  @out = io || out
  @err = io || err
  @level = parse_level(level || ENV.fetch('HYPERION_LOG_LEVEL', 'info'))
  requested = format || ENV['HYPERION_LOG_FORMAT']
  @format = pick_format(requested)
  # Colorize when format is text AND the destination is a TTY. We only
  # check the regular stream here — colored text is for humans.
  @colorize = @format == :text && tty?(@out)
end

Instance Attribute Details

#formatObject (readonly)

Returns the value of attribute format.



42
43
44
# File 'lib/hyperion/logger.rb', line 42

def format
  @format
end

#levelObject

Returns the value of attribute level.



42
43
44
# File 'lib/hyperion/logger.rb', line 42

def level
  @level
end

Instance Method Details

#access(method, path, query, status, duration_ms, remote_addr, http_version) ⇒ Object

Hot-path access-log emitter — bypasses the generic format_text / format_json kvs.join + hash#map allocations. The whole line is built via a single interpolation, the timestamp is cached per-thread per millisecond, and we batch lines into a per-thread buffer that flushes when full (lock-free emit; POSIX write(2) is atomic for writes <= PIPE_BUF / 4096 bytes).

Returns silently on any IO error — logging must never crash the server.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/hyperion/logger.rb', line 98

def access(method, path, query, status, duration_ms, remote_addr, http_version)
  return unless emit?(:info)

  ts = cached_timestamp
  line = if @format == :json
           build_access_json(ts, method, path, query, status, duration_ms, remote_addr, http_version)
         else
           build_access_text(ts, method, path, query, status, duration_ms, remote_addr, http_version)
         end

  buf = (Thread.current[:__hyperion_access_buf__] ||= +'')
  buf << line
  return if buf.bytesize < ACCESS_FLUSH_BYTES

  @out.write(buf)
  buf.clear
rescue StandardError
  # Swallow logger failures — never let logging crash the server.
end

#flush_access_bufferObject

Flush this thread’s buffered access-log lines. Called by the connection loop when a connection closes (so log lines from a closing keep-alive session don’t get stuck behind the buffer until the next connection).



121
122
123
124
125
126
127
128
129
# File 'lib/hyperion/logger.rb', line 121

def flush_access_buffer
  buf = Thread.current[:__hyperion_access_buf__]
  return if buf.nil? || buf.empty?

  @out.write(buf)
  buf.clear
rescue StandardError
  # Swallow logger failures — never let logging crash the server.
end

#io_for(lvl) ⇒ Object

Pick the destination IO for a given level. warn / error / fatal → @err (stderr default). debug / info → @out (stdout default).



74
75
76
# File 'lib/hyperion/logger.rb', line 74

def io_for(lvl)
  ERROR_LEVELS.include?(lvl) ? @err : @out
end