Project Awesome project awesome

sravioli/log.wz

Tagged logging library with pluggable sinks and severity thresholds.

Package 1 stars GitHub

log.wz

Tests Lint Coverage

Logging library for WezTerm plugins and configuration code.

  • Tagged logger instances with per-instance enable/disable
  • Global threshold filtering (DEBUG, INFO, WARN, ERROR)
  • Pluggable sinks: WezTerm native, JSON, file, in-memory ring buffer
  • File sink auto-resolves a safe log directory outside config_dir
  • Sink errors isolated with pcall; format-string errors caught gracefully
  • Lazy-loaded sink modules with no-op fallbacks
  • Full LuaLS type annotations for IDE autocompletion and type checking

Installation

local wezterm = require "wezterm"

-- from git
local log = wezterm.plugin.require "https://github.com/sravioli/log.wz"

-- from a local checkout
local log = wezterm.plugin.require("file:///" .. wezterm.config_dir .. "/plugins/log.wz")

Usage

log:setup { threshold = "INFO" }

local logger = log.new "wezterm.lua"
logger:warn "Configuration loaded"
logger:info("Window opacity = %s", 0.95)

message uses string.format placeholders. Non-string arguments are stringified automatically (userdata via tostring, others via wezterm.to_string when available). Malformed format strings emit the raw message instead of crashing.

Output is prefixed as [tag] message.

Configuration

Call setup before creating loggers. Both log.setup(t) and log:setup(t) work.

log:setup {
  enabled = true,         -- global on/off
  threshold = "INFO",     -- DEBUG | INFO | WARN | ERROR (or 0..3)
  sinks = {
    default_enabled = true, -- prepend built-in WezTerm sink to every logger
  },
}
Field Type Default Description
enabled boolean true Global on/off.
threshold string | number "WARN" Minimum level. Invalid values become WARN.
sinks.default_enabled boolean true Auto-prepend the WezTerm sink.

Only keys present in the defaults are accepted; unknown keys are silently ignored. The sinks sub-table is merged one level deep.

Existing loggers keep their original threshold and sinks. The global enabled flag takes effect immediately.

The current configuration can be read with log.config.get(). It returns a reference to the live config table.

Logger

local logger = log.new(tag?, enabled?, sinks?)
Param Type Default Notes
tag string? "Log" Prefix shown in output.
enabled boolean? true Per-instance toggle.
sinks Log.Sink[]? {} Shallow-copied, never mutated.

When sinks.default_enabled is true the WezTerm sink is prepended automatically. The logger's threshold is taken from the global config at creation time.

Methods

Method Description
logger:debug(message, ...) DEBUG level. Prepends "DEBUG: ".
logger:info(message, ...) INFO level.
logger:warn(message, ...) WARN level.
logger:error(message, ...) ERROR level.
logger:log(level, msg, ...) Arbitrary level (string or integer).
logger:add_sink(sink) Append a sink after creation.

A message is emitted only when all three conditions hold: config.enabled is true, logger.enabled is true, and the resolved level is at or above the logger's threshold.

Levels

Name Value
DEBUG 0
INFO 1
WARN 2
ERROR 3

Access the enum via log.levels.levels and the reverse map via log.levels.names. Use log.levels.normalize(level) to convert a string or number into a numeric level (case-insensitive). Returns nil for unrecognised inputs; arbitrary numeric values pass through unchanged.

Events are emitted when event.level >= logger.threshold. Unrecognised levels are silently dropped.

Event

Every sink receives a table with these fields:

Field Type Description
timestamp integer Unix epoch seconds.
datetime string %Y-%m-%d %H:%M:%S%.3f local.
level integer Numeric severity.
level_name string "DEBUG", "INFO", etc.
tag string Logger tag.
message string Formatted message with tag.
raw_message string Message before formatting.

Timestamps use wezterm.time.now() when available, falling back to os.time().

Sinks

A sink is a function or callable table that receives a Log.Event.

Kind What How to use
Stateless wz, json Pass directly: { log.sinks.json }
Stateful memory, file Call to create: { log.sinks.file() }

Stateful modules return callable instances. Pass them straight into the sinks array.

local logger = log.new("tag", true, {
  log.sinks.json,
  log.sinks.file { format = "text" },
})

Each sink runs inside pcall. A failing sink is logged to the WezTerm debug overlay and does not affect other sinks.

Sink modules are lazy-loaded on first access. If a module fails to load, a no-op fallback is returned and an error is logged via wezterm.log_error.


log.sinks.wz

Default sink. Forwards to WezTerm's native logging.

Level Calls
DEBUG, INFO wezterm.log_info
WARN wezterm.log_warn
ERROR wezterm.log_error

Unknown levels are silently ignored.


log.sinks.json

Callable sink. Encodes events as JSON and emits them through wezterm.log_info. Uses wezterm.serde internally. Errors if wezterm.serde is unavailable.

local logger = log.new("app", true, { log.sinks.json })

Also exposes utility functions:

Function Description
log.sinks.json.encode(v) Encode a Lua value to a JSON string.
log.sinks.json.decode(s) Decode a JSON string back to a Lua value.
log.sinks.json.write(evt) Encode event as JSON and log via log_info.

log.sinks.memory

In-memory ring buffer. Call the module to create an instance.

local mem = log.sinks.memory()                      -- default: 10 000 entries
local mem = log.sinks.memory { max_entries = 500 }   -- custom cap
local mem = log.sinks.memory { max_entries = 0 }     -- unlimited

local logger = log.new("test", true, { mem })
logger:info("hello %s", "world")

mem:count()        -- 1
mem:get_entries()  -- shallow copy of stored events
mem:to_string()    -- "[INFO] [test] hello world"
mem:clear()
Method Returns Description
write(event) nil Store event. Evicts oldest when full.
clear() nil Remove all stored entries.
get_entries() Log.Event[] Shallow copy of stored events.
count() integer Number of stored entries.
to_string() string Entries formatted as [LEVEL] message.

log.sinks.file

Appends one line per event to a file. Call the module to create an instance.

local f = log.sinks.file()                                  -- default path, JSON
local f = log.sinks.file { format = "text" }                -- default path, plain text
local f = log.sinks.file { path = "/tmp/wz.log" }          -- explicit path
local f = log.sinks.file {                                  -- custom formatter
  formatter = function(e)
    return ("%s | %s | %s"):format(e.datetime, e.level_name, e.message)
  end,
}

local logger = log.new("app", true, { f })

Options

Field Type Default Description
path string? auto File path. Resolved automatically if nil.
format "json" | "text" "json" Line format.
formatter fun(event): string Custom formatter. Overrides format.

Path handling

path Behaviour
nil / omitted Uses platform default directory, file log.wz.log.
Inside wezterm.config_dir Relocated to the default directory with a warning. Writing inside config_dir causes an infinite reload loop.
Anything else Used as-is. Parent directories are not auto-created for explicit paths.

Default directory:

OS Path
Windows %LOCALAPPDATA%\wezterm (fallback %APPDATA%\wezterm)
Linux / macOS $XDG_DATA_HOME/wezterm (fallback ~/.local/share/wezterm)

The default directory is created automatically if it doesn't exist.

Output formats

JSON (default):

{
  "timestamp": 1234567890,
  "datetime": "2025-01-01 00:00:00.000",
  "level": 2,
  "level_name": "WARN",
  "tag": "MyTag",
  "message": "[MyTag] Hello",
  "raw_message": "Hello"
}

Text:

2025-01-01 00:00:00.000 [WARN] [MyTag] Hello
Method Returns Description
write(event) nil Serialize and append event to file.
serialize(event) boolean, string Serialize event without writing.
append(payload) boolean, string? Append raw text to the file.

Examples

Log to both WezTerm and a file (default sink enabled):

local logger = log.new("wezterm.lua", true, { log.sinks.file() })
logger:warn "starting up"

Log only to a file:

log:setup { sinks = { default_enabled = false } }
local logger = log.new("wezterm.lua", true, { log.sinks.file { format = "text" } })

Capture in memory:

log:setup { threshold = "DEBUG" }
local mem = log.sinks.memory { max_entries = 100 }
local logger = log.new("test", true, { mem })
logger:debug "step 1"
assert(mem:count() == 1)

Multiple sinks at once:

local mem = log.sinks.memory()
local logger = log.new("app", true, {
  log.sinks.json,
  log.sinks.file { format = "text" },
  mem,
})
logger:info("started with %s sinks", #logger.sinks)

License

Code is licensed under the GNU General Public License v2. Documentation is licensed under Creative Commons Attribution-NonCommercial 4.0 International.

Back to WezTerm