Module: Tempest::DebugLog

Defined in:
lib/tempest/debug_log.rb

Overview

Structured diagnostic logging for tempest.

‘Tempest::DebugLog.build` returns a `Channel` that fans messages out to one or more underlying `::Logger` instances. The format is logfmt-flavored single-line:

2026-05-18T01:23:45+09:00 level=warn module=watchdog event=stalled_detected elapsed_seconds=612.3 threshold_seconds=600

The fixed leading keys (‘level=`, `module=`, `event=`) are produced by the formatter from the level + progname + first-positional arguments, so call sites just write the variable fields as keyword arguments:

@logger.warn("watchdog", event: "stalled_detected", elapsed_seconds: 612.3, threshold_seconds: 600)

Output destinations:

* `info.log` — INFO and above, always written when logging is enabled.
* `debug.log` — DEBUG and above, written only when `--debug` (or the
  equivalent flag passed to `build(debug: true)`) is on.

Default base directory is ‘$XDG_STATE_HOME/tempest` (falling back to `~/.local/state/tempest`). Override via `TEMPEST_LOG_DIR=/path` for the whole tree, or set `TEMPEST_NO_LOG=1` to disable both files entirely (used by tests). The legacy `TEMPEST_DEBUG_LOG=/path/to/file` env var still works and routes everything (DEBUG and above) to a single file regardless of the other settings.

All file destinations use size-based rotation (5 MiB x 5 files) so a long-running session can’t fill the disk.

Defined Under Namespace

Classes: Channel

Constant Summary collapse

LEVELS =
{
  "DEBUG" => Logger::DEBUG,
  "INFO"  => Logger::INFO,
  "WARN"  => Logger::WARN,
  "ERROR" => Logger::ERROR,
  "FATAL" => Logger::FATAL,
}.freeze
DEFAULT_ROTATION_COUNT =
5
DEFAULT_ROTATION_SIZE =
5 * 1024 * 1024

Class Method Summary collapse

Class Method Details

.build(env:, debug: false) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/tempest/debug_log.rb', line 49

def build(env:, debug: false)
  loggers = []

  legacy = env["TEMPEST_DEBUG_LOG"]
  if legacy && !legacy.empty?
    loggers << build_file_logger(legacy, level: resolve_level(env["TEMPEST_DEBUG_LOG_LEVEL"]) || Logger::DEBUG)
  end

  unless env["TEMPEST_NO_LOG"] == "1"
    dir = log_dir(env)
    loggers << build_file_logger(File.join(dir, "info.log"), level: Logger::INFO)
    loggers << build_file_logger(File.join(dir, "debug.log"), level: Logger::DEBUG) if debug
  end

  Channel.new(loggers: loggers)
end

.build_file_logger(path, level:) ⇒ Object



98
99
100
101
102
103
104
105
# File 'lib/tempest/debug_log.rb', line 98

def build_file_logger(path, level:)
  path = File.expand_path(path)
  FileUtils.mkdir_p(File.dirname(path))
  logger = Logger.new(path, DEFAULT_ROTATION_COUNT, DEFAULT_ROTATION_SIZE)
  logger.level = level
  logger.formatter = formatter
  logger
end

.encode_value(value) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/tempest/debug_log.rb', line 107

def encode_value(value)
  case value
  when nil
    "nil"
  when true, false, Integer, Float, Symbol
    value.to_s
  when Time
    value.iso8601
  else
    s = value.to_s
    if s.empty?
      '""'
    elsif s.match?(/[\s"=]/)
      '"' + s.gsub('\\', '\\\\\\\\').gsub('"', '\\"') + '"'
    else
      s
    end
  end
end

.formatterObject



70
71
72
73
74
75
76
77
78
# File 'lib/tempest/debug_log.rb', line 70

def formatter
  proc do |severity, time, progname, msg|
    parts = []
    parts << "level=#{severity.downcase}"
    parts << "module=#{progname}" if progname && !progname.to_s.empty?
    parts << msg if msg && !msg.to_s.empty?
    "#{time.iso8601} #{parts.join(' ')}\n"
  end
end

.log_dir(env) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/tempest/debug_log.rb', line 85

def log_dir(env)
  override = env["TEMPEST_LOG_DIR"]
  return override if override && !override.empty?

  xdg = env["XDG_STATE_HOME"]
  base = if xdg && !xdg.empty?
    xdg
  else
    File.join(env["HOME"] || Dir.home, ".local", "state")
  end
  File.join(base, "tempest")
end

.null_channelObject



66
67
68
# File 'lib/tempest/debug_log.rb', line 66

def null_channel
  Channel.new(loggers: [])
end

.resolve_level(value) ⇒ Object



80
81
82
83
# File 'lib/tempest/debug_log.rb', line 80

def resolve_level(value)
  return nil if value.nil? || value.empty?
  LEVELS[value.to_s.upcase]
end