- Scheme 100%
| tests | ||
| .gitignore | ||
| LICENSE | ||
| logger.egg | ||
| logger.release-info | ||
| logger.scm | ||
| README.md | ||
logger
Simple structured logging for CHICKEN Scheme with per-module level control.
Usage
(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:
(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
;; 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
;; 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:
(logger/format 'json)
(logger/i "user logged in" '((user-id . 123) (ip . "127.0.0.1")))
Output:
{"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:
(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:
(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
(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:
(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_APPENDfor best results; without it, two processes can race on the file offset. - Locking adds two extra syscalls per record, so leave
logger/lockat#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:
csi -s tests/run-tests.scm
License
BSD 3-Clause - see LICENSE file