# logger Simple structured logging for CHICKEN Scheme with per-module level control. ## Usage ```scheme (import logger) ;; Basic logging (uses GLOBAL module name) (logger/d "debug message") (logger/i "info message") (logger/w "warning message") (logger/e "error message") ;; Messages can be concatenated (logger/i "user " user-id " logged in") ``` Output: ``` 2026-01-18T19:07:51Z [INFO] [GLOBAL] info message ``` ### Per-module logging Use `logger/install` inside a module to create local `d`, `i`, `w`, `e` functions that automatically tag logs with the module name: ```scheme (module my-app (do-stuff) (import scheme chicken.base logger) (logger/install my-app) (define (do-stuff) (i "doing stuff") ;; tagged as [my-app] (d "details..."))) ``` ### Log levels Levels from lowest to highest priority: `debug`, `info`, `warn`, `error`, `none` ```scheme ;; Set global level (default: debug) (logger/level 'info) ;; hides debug messages ;; Set level for specific module (logger/set-module-level! 'noisy-module 'warn) ;; Disable a module entirely (logger/disable-module! 'noisy-module) ``` ### Output format ```scheme ;; Text format (default) (logger/format 'text) ;; 2026-01-18T19:07:51Z [INFO] [GLOBAL] message ;; JSON format (logger/format 'json) ;; {"ts":1737226071,"level":"info","module":"GLOBAL","message":"message"} ``` ### Structured JSON fields JSON logs can include extra fields by passing one final alist argument to any logging function: ```scheme (logger/format 'json) (logger/i "user logged in" '((user-id . 123) (ip . "127.0.0.1"))) ``` Output: ```json {"ts":1737226071,"level":"info","module":"GLOBAL","message":"user logged in","user-id":123,"ip":"127.0.0.1"} ``` The structured fields are merged into the object passed to `write-json`. The logger only treats `rest` as structured fields when it receives exactly one extra argument shaped like an alist, so normal message concatenation still works: ```scheme (logger/i "user " user-id " logged in") ``` In text output, structured fields are rendered after the message by applying `->string` to the `rest` value and separating it with a space: ```scheme (logger/format 'text) (logger/i "user logged in" '((user-id . 123) (ip . "127.0.0.1"))) ;; 2026-01-18T19:07:51Z [INFO] [GLOBAL] user logged in ((user-id . 123) (ip . "127.0.0.1")) ``` ### Custom output port ```scheme (import chicken.file.posix) (call-with-output-file "app.log" (lambda (port) (logger/output port) ;; logs now go to app.log )) ``` ### Concurrency Each call to a logging function assembles the full record (including the trailing newline) into a single string and emits it with one `display` followed by `flush-output`. On Linux/macOS local filesystems, when the destination file is opened in append mode, this is enough to keep records intact for short lines from concurrent writers. For multi-process safety with longer lines, set `logger/lock` to an acquire/release pair. The egg ships with `logger/make-flock-lock`, which builds an advisory POSIX lock pair (via `fcntl` through `chicken.file.posix`) over a shared lockfile path: ```scheme (import logger chicken.file.posix chicken.bitwise) (let* ((fd (file-open "app.log" (bitwise-ior open/wronly open/creat open/append) #o644)) (out (open-output-file* fd))) (logger/output out) (logger/lock (logger/make-flock-lock "app.log.lock")) (logger/i "this line is safe across processes")) ``` Notes: - All participating processes must use the **same** lockfile path. - POSIX file locks are advisory: only writers that go through this egg are serialized. Foreign writers (`echo >> app.log`, log-rotation tools) will still interleave. - Open the destination file with `O_APPEND` for best results; without it, two processes can race on the file offset. - Locking adds two extra syscalls per record, so leave `logger/lock` at `#f` (the default) when single-process. The multi-process stress test in `tests/concurrency-test.scm` forks several writers and asserts that no line is spliced. ## Tests Run the test suite from the repository root: ```sh csi -s tests/run-tests.scm ``` ## License BSD 3-Clause - see LICENSE file