{"id":47724189,"url":"https://github.com/sravioli/log.wz","last_synced_at":"2026-04-02T20:04:26.111Z","repository":{"id":345377062,"uuid":"1185680647","full_name":"sravioli/log.wz","owner":"sravioli","description":"🪵 lightweight, pluggable logging library for WezTerm","archived":false,"fork":false,"pushed_at":"2026-03-31T13:15:54.000Z","size":88,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-31T15:18:13.746Z","etag":null,"topics":["logger","logging","lua","plugin","wezterm","wezterm-plugin"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sravioli.png","metadata":{"files":{"readme":".github/readme.md","changelog":null,"contributing":".github/contributing.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":".github/code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":"sravioli","buy_me_a_coffee":null,"custom":["https://www.paypal.me/SatansRavioli"]}},"created_at":"2026-03-18T20:50:18.000Z","updated_at":"2026-03-31T13:15:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sravioli/log.wz","commit_stats":null,"previous_names":["sravioli/log.wz"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/sravioli/log.wz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sravioli%2Flog.wz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sravioli%2Flog.wz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sravioli%2Flog.wz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sravioli%2Flog.wz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sravioli","download_url":"https://codeload.github.com/sravioli/log.wz/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sravioli%2Flog.wz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31314843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["logger","logging","lua","plugin","wezterm","wezterm-plugin"],"created_at":"2026-04-02T20:04:22.121Z","updated_at":"2026-04-02T20:04:26.104Z","avatar_url":"https://github.com/sravioli.png","language":"Lua","readme":"# log.wz\n\n[![Tests](https://img.shields.io/github/actions/workflow/status/sravioli/log.wz/tests.yaml?label=Tests\u0026logo=Lua)](https://github.com/sravioli/log.wz/actions?workflow=tests)\n[![Lint](https://img.shields.io/github/actions/workflow/status/sravioli/log.wz/lint.yaml?label=Lint\u0026logo=Lua)](https://github.com/sravioli/log.wz/actions?workflow=lint)\n[![Coverage](https://img.shields.io/coverallsCoverage/github/sravioli/log.wz?label=Coverage\u0026logo=coveralls)](https://coveralls.io/github/sravioli/log.wz)\n\nLogging library for [WezTerm](https://wezfurlong.org/wezterm/) plugins and\nconfiguration code.\n\n- Tagged logger instances with per-instance enable/disable\n- Global threshold filtering (`DEBUG`, `INFO`, `WARN`, `ERROR`)\n- Pluggable sinks: WezTerm native, JSON, file, in-memory ring buffer\n- File sink auto-resolves a safe log directory outside `config_dir`\n- Sink errors isolated with `pcall`; format-string errors caught gracefully\n- Lazy-loaded sink modules with no-op fallbacks\n- Full LuaLS type annotations for IDE autocompletion and type checking\n\n## Installation\n\n```lua\nlocal wezterm = require \"wezterm\"\n\n-- from git\nlocal log = wezterm.plugin.require \"https://github.com/sravioli/log.wz\"\n\n-- from a local checkout\nlocal log = wezterm.plugin.require(\"file:///\" .. wezterm.config_dir .. \"/plugins/log.wz\")\n```\n\n## Usage\n\n```lua\nlog:setup { threshold = \"INFO\" }\n\nlocal logger = log.new \"wezterm.lua\"\nlogger:warn \"Configuration loaded\"\nlogger:info(\"Window opacity = %s\", 0.95)\n```\n\n`message` uses `string.format` placeholders. Non-string arguments are\nstringified automatically (`userdata` via `tostring`, others via\n`wezterm.to_string` when available). Malformed format strings emit the raw\nmessage instead of crashing.\n\nOutput is prefixed as `[tag] message`.\n\n## Configuration\n\nCall `setup` before creating loggers. Both `log.setup(t)` and `log:setup(t)`\nwork.\n\n```lua\nlog:setup {\n  enabled = true,         -- global on/off\n  threshold = \"INFO\",     -- DEBUG | INFO | WARN | ERROR (or 0..3)\n  sinks = {\n    default_enabled = true, -- prepend built-in WezTerm sink to every logger\n  },\n}\n```\n\n| Field                   | Type             | Default  | Description                                  |\n| ----------------------- | ---------------- | -------- | -------------------------------------------- |\n| `enabled`               | boolean          | `true`   | Global on/off.                               |\n| `threshold`             | string \\| number | `\"WARN\"` | Minimum level. Invalid values become `WARN`. |\n| `sinks.default_enabled` | boolean          | `true`   | Auto-prepend the WezTerm sink.               |\n\nOnly keys present in the defaults are accepted; unknown keys are silently\nignored. The `sinks` sub-table is merged one level deep.\n\nExisting loggers keep their original threshold and sinks. The global\n`enabled` flag takes effect immediately.\n\nThe current configuration can be read with `log.config.get()`. It returns a\nreference to the live config table.\n\n## Logger\n\n```lua\nlocal logger = log.new(tag?, enabled?, sinks?)\n```\n\n| Param     | Type        | Default | Notes                          |\n| --------- | ----------- | ------- | ------------------------------ |\n| `tag`     | string?     | `\"Log\"` | Prefix shown in output.        |\n| `enabled` | boolean?    | `true`  | Per-instance toggle.           |\n| `sinks`   | Log.Sink[]? | `{}`    | Shallow-copied, never mutated. |\n\nWhen `sinks.default_enabled` is true the WezTerm sink is prepended\nautomatically. The logger's threshold is taken from the global config at\ncreation time.\n\n### Methods\n\n| Method                        | Description                          |\n| ----------------------------- | ------------------------------------ |\n| `logger:debug(message, ...)`  | DEBUG level. Prepends `\"DEBUG: \"`.   |\n| `logger:info(message, ...)`   | INFO level.                          |\n| `logger:warn(message, ...)`   | WARN level.                          |\n| `logger:error(message, ...)`  | ERROR level.                         |\n| `logger:log(level, msg, ...)` | Arbitrary level (string or integer). |\n| `logger:add_sink(sink)`       | Append a sink after creation.        |\n\nA message is emitted only when all three conditions hold: `config.enabled` is\ntrue, `logger.enabled` is true, and the resolved level is at or above the\nlogger's threshold.\n\n## Levels\n\n| Name    | Value |\n| ------- | ----- |\n| `DEBUG` | 0     |\n| `INFO`  | 1     |\n| `WARN`  | 2     |\n| `ERROR` | 3     |\n\nAccess the enum via `log.levels.levels` and the reverse map via\n`log.levels.names`. Use `log.levels.normalize(level)` to convert a string or\nnumber into a numeric level (case-insensitive). Returns `nil` for unrecognised\ninputs; arbitrary numeric values pass through unchanged.\n\nEvents are emitted when `event.level \u003e= logger.threshold`. Unrecognised levels\nare silently dropped.\n\n## Event\n\nEvery sink receives a table with these fields:\n\n| Field         | Type    | Description                    |\n| ------------- | ------- | ------------------------------ |\n| `timestamp`   | integer | Unix epoch seconds.            |\n| `datetime`    | string  | `%Y-%m-%d %H:%M:%S%.3f` local. |\n| `level`       | integer | Numeric severity.              |\n| `level_name`  | string  | `\"DEBUG\"`, `\"INFO\"`, etc.      |\n| `tag`         | string  | Logger tag.                    |\n| `message`     | string  | Formatted message with tag.    |\n| `raw_message` | string  | Message before formatting.     |\n\nTimestamps use `wezterm.time.now()` when available, falling back to\n`os.time()`.\n\n## Sinks\n\nA sink is a function or callable table that receives a `Log.Event`.\n\n| Kind      | What             | How to use                             |\n| --------- | ---------------- | -------------------------------------- |\n| Stateless | `wz`, `json`     | Pass directly: `{ log.sinks.json }`    |\n| Stateful  | `memory`, `file` | Call to create: `{ log.sinks.file() }` |\n\nStateful modules return callable instances. Pass them straight into the sinks\narray.\n\n```lua\nlocal logger = log.new(\"tag\", true, {\n  log.sinks.json,\n  log.sinks.file { format = \"text\" },\n})\n```\n\nEach sink runs inside `pcall`. A failing sink is logged to the WezTerm debug\noverlay and does not affect other sinks.\n\nSink modules are lazy-loaded on first access. If a module fails to load, a\nno-op fallback is returned and an error is logged via `wezterm.log_error`.\n\n---\n\n### `log.sinks.wz`\n\nDefault sink. Forwards to WezTerm's native logging.\n\n| Level       | Calls               |\n| ----------- | ------------------- |\n| DEBUG, INFO | `wezterm.log_info`  |\n| WARN        | `wezterm.log_warn`  |\n| ERROR       | `wezterm.log_error` |\n\nUnknown levels are silently ignored.\n\n---\n\n### `log.sinks.json`\n\nCallable sink. Encodes events as JSON and emits them through\n`wezterm.log_info`. Uses `wezterm.serde` internally. Errors if\n`wezterm.serde` is unavailable.\n\n```lua\nlocal logger = log.new(\"app\", true, { log.sinks.json })\n```\n\nAlso exposes utility functions:\n\n| Function                    | Description                                  |\n| --------------------------- | -------------------------------------------- |\n| `log.sinks.json.encode(v)`  | Encode a Lua value to a JSON string.         |\n| `log.sinks.json.decode(s)`  | Decode a JSON string back to a Lua value.    |\n| `log.sinks.json.write(evt)` | Encode event as JSON and log via `log_info`. |\n\n---\n\n### `log.sinks.memory`\n\nIn-memory ring buffer. Call the module to create an instance.\n\n```lua\nlocal mem = log.sinks.memory()                      -- default: 10 000 entries\nlocal mem = log.sinks.memory { max_entries = 500 }   -- custom cap\nlocal mem = log.sinks.memory { max_entries = 0 }     -- unlimited\n\nlocal logger = log.new(\"test\", true, { mem })\nlogger:info(\"hello %s\", \"world\")\n\nmem:count()        -- 1\nmem:get_entries()  -- shallow copy of stored events\nmem:to_string()    -- \"[INFO] [test] hello world\"\nmem:clear()\n```\n\n| Method          | Returns       | Description                             |\n| --------------- | ------------- | --------------------------------------- |\n| `write(event)`  | nil           | Store event. Evicts oldest when full.   |\n| `clear()`       | nil           | Remove all stored entries.              |\n| `get_entries()` | `Log.Event[]` | Shallow copy of stored events.          |\n| `count()`       | integer       | Number of stored entries.               |\n| `to_string()`   | string        | Entries formatted as `[LEVEL] message`. |\n\n---\n\n### `log.sinks.file`\n\nAppends one line per event to a file. Call the module to create an instance.\n\n```lua\nlocal f = log.sinks.file()                                  -- default path, JSON\nlocal f = log.sinks.file { format = \"text\" }                -- default path, plain text\nlocal f = log.sinks.file { path = \"/tmp/wz.log\" }          -- explicit path\nlocal f = log.sinks.file {                                  -- custom formatter\n  formatter = function(e)\n    return (\"%s | %s | %s\"):format(e.datetime, e.level_name, e.message)\n  end,\n}\n\nlocal logger = log.new(\"app\", true, { f })\n```\n\n#### Options\n\n| Field       | Type                 | Default  | Description                               |\n| ----------- | -------------------- | -------- | ----------------------------------------- |\n| `path`      | string?              | auto     | File path. Resolved automatically if nil. |\n| `format`    | `\"json\"` \\| `\"text\"` | `\"json\"` | Line format.                              |\n| `formatter` | `fun(event): string` | —        | Custom formatter. Overrides `format`.     |\n\n#### Path handling\n\n| `path`                      | Behaviour                                                                                                      |\n| --------------------------- | -------------------------------------------------------------------------------------------------------------- |\n| nil / omitted               | Uses platform default directory, file `log.wz.log`.                                                            |\n| Inside `wezterm.config_dir` | Relocated to the default directory with a warning. Writing inside `config_dir` causes an infinite reload loop. |\n| Anything else               | Used as-is. Parent directories are **not** auto-created for explicit paths.                                    |\n\nDefault directory:\n\n| OS            | Path                                                         |\n| ------------- | ------------------------------------------------------------ |\n| Windows       | `%LOCALAPPDATA%\\wezterm` (fallback `%APPDATA%\\wezterm`)      |\n| Linux / macOS | `$XDG_DATA_HOME/wezterm` (fallback `~/.local/share/wezterm`) |\n\nThe default directory is created automatically if it doesn't exist.\n\n#### Output formats\n\n**JSON** (default):\n\n```json\n{\n  \"timestamp\": 1234567890,\n  \"datetime\": \"2025-01-01 00:00:00.000\",\n  \"level\": 2,\n  \"level_name\": \"WARN\",\n  \"tag\": \"MyTag\",\n  \"message\": \"[MyTag] Hello\",\n  \"raw_message\": \"Hello\"\n}\n```\n\n**Text**:\n\n```\n2025-01-01 00:00:00.000 [WARN] [MyTag] Hello\n```\n\n| Method             | Returns            | Description                         |\n| ------------------ | ------------------ | ----------------------------------- |\n| `write(event)`     | nil                | Serialize and append event to file. |\n| `serialize(event)` | `boolean, string`  | Serialize event without writing.    |\n| `append(payload)`  | `boolean, string?` | Append raw text to the file.        |\n\n## Examples\n\nLog to both WezTerm and a file (default sink enabled):\n\n```lua\nlocal logger = log.new(\"wezterm.lua\", true, { log.sinks.file() })\nlogger:warn \"starting up\"\n```\n\nLog only to a file:\n\n```lua\nlog:setup { sinks = { default_enabled = false } }\nlocal logger = log.new(\"wezterm.lua\", true, { log.sinks.file { format = \"text\" } })\n```\n\nCapture in memory:\n\n```lua\nlog:setup { threshold = \"DEBUG\" }\nlocal mem = log.sinks.memory { max_entries = 100 }\nlocal logger = log.new(\"test\", true, { mem })\nlogger:debug \"step 1\"\nassert(mem:count() == 1)\n```\n\nMultiple sinks at once:\n\n```lua\nlocal mem = log.sinks.memory()\nlocal logger = log.new(\"app\", true, {\n  log.sinks.json,\n  log.sinks.file { format = \"text\" },\n  mem,\n})\nlogger:info(\"started with %s sinks\", #logger.sinks)\n```\n\n## License\n\nCode is licensed under the [GNU General Public License v2](../LICENSE). Documentation\nis licensed under [Creative Commons Attribution-NonCommercial 4.0 International](../LICENSE-DOCS).\n","funding_links":["https://polar.sh/sravioli","https://www.paypal.me/SatansRavioli"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsravioli%2Flog.wz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsravioli%2Flog.wz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsravioli%2Flog.wz/lists"}