logger
Simple structured logging for CHICKEN Scheme with per-module level control.
TOC »
Description
A lightweight logging library that provides:
- Four log levels: debug, info, warn, error
- Per-module log level control
- Module-local logging functions via logger/install macro
- Text and JSON output formats
- Structured fields (alists) merged into JSON records
- Configurable output port
- ISO 8601 timestamps
- Optional hook for warn/error messages
- Atomic record writes, with an optional advisory file lock for multi-process logging
- Works on CHICKEN 5 and CHICKEN 6 from the same source
Author
Rolando Abarca
Repository
https://forgejo.rolando.cl/cpm/logger-egg
Requirements
Documentation
Quick Start
(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...")))Output:
2026-01-18T19:07:51Z [INFO] [my-app] doing stuff 2026-01-18T19:07:51Z [DEBUG] [my-app] details...
Structured Fields
Any logging function accepts one final alist argument as structured fields. In JSON format, the fields are merged into the record:
(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 record is written with json-write from srfi-180, so field values must be valid srfi-180 JSON values. In particular, JSON arrays must be vectors, not lists:
(logger/i "batch done" `((ids . ,(vector 1 2 3))))The rest argument is treated as structured fields only when there is exactly one extra argument and it is shaped like an alist, so normal message concatenation still works:
(logger/i "user " user-id " logged in")In text format, the fields are written after the message, separated by 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))
Concurrency
Each logging call builds the whole record, including the trailing newline, as a single string and writes it with one display followed by flush-output. On Linux and macOS local filesystems, when the destination file is opened in append mode, this keeps short records intact even with concurrent writers.
For multi-process safety with longer lines, set logger/lock to an acquire/release pair. logger/make-flock-lock builds one from 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) can 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) for single-process programs.
API Reference
Parameters
- logger/levelparameter
Global log level. Messages below this priority are suppressed. Default: 'debug.
Valid values: 'debug, 'info, 'warn, 'error, 'none
;; Set global level to info (hides debug messages) (logger/level 'info) ;; Get current level (logger/level) ; => 'info
- logger/outputparameter
Output port for log messages. Default: (current-output-port).
(import chicken.file.posix) ;; Log to a file (define log-port (open-output-file "app.log")) (logger/output log-port) ;; Log to stderr (import chicken.port) (logger/output (current-error-port))
- logger/formatparameter
Output format. Default: 'text.
- 'text
- Human-readable format: TIMESTAMP [LEVEL] [MODULE] message
- 'json
- JSON format: {"ts":N,"level":"...","module":"...","message":"..."}
;; Text format (default) (logger/format 'text) ;; Output: 2026-01-18T19:07:51Z [INFO] [GLOBAL] message ;; JSON format (logger/format 'json) ;; Output: {"ts":1737226071,"level":"info","module":"GLOBAL","message":"message"}
- logger/module-levelsparameter
Alist of per-module log levels. Use logger/set-module-level! to modify.
- logger/error-hookparameter
Optional procedure called after every warn or error record is written. Default: #f (disabled).
The hook is called as (hook message level), where message is a string of the form "[MODULE] message" (structured fields are not included) and level is 1 for warnings and 2 for errors. It is only called for messages that pass the level filter.
(logger/error-hook (lambda (message level) (send-alert (if (= level 2) 'error 'warning) message)))
- logger/lockparameter
Optional cross-process lock used around each write. Default: #f (no locking).
When set, it must be a pair (acquire-thunk . release-thunk). The thunks are called around each write with dynamic-wind. See Concurrency and logger/make-flock-lock.
Procedures
- logger/log module-name level msg #!rest restprocedure
Core logging function. Logs a message if level meets or exceeds the effective level for module-name.
- module-name
- Symbol identifying the module.
- level
- Log level symbol: 'debug, 'info, 'warn, or 'error.
- msg
- Message string or value.
- rest
- Additional values to concatenate to the message, or a single alist of structured fields (see Structured Fields).
(logger/log 'my-module 'info "Processing item " item-id) (logger/log 'my-module 'info "Processed item" `((item-id . ,item-id)))
- logger/d msg #!rest restprocedure
- logger/i msg #!rest restprocedure
- logger/w msg #!rest restprocedure
- logger/e msg #!rest restprocedure
Convenience functions for logging at debug, info, warn, and error levels respectively. Uses 'GLOBAL as the module name.
(logger/d "debug details") (logger/i "informational message") (logger/w "warning!") (logger/e "error occurred: " error-msg)
- logger/set-module-level! module-name levelprocedure
Set the log level for a specific module. This overrides the global logger/level for that module.
;; Only show warnings and errors from noisy-module (logger/set-module-level! 'noisy-module 'warn) ;; Show all messages from important-module even if global level is higher (logger/set-module-level! 'important-module 'debug)
- logger/disable-module! module-nameprocedure
Disable all logging for a specific module. Equivalent to (logger/set-module-level! module-name 'none).
(logger/disable-module! 'noisy-module)
- logger/make-flock-lock pathprocedure
Returns an (acquire-thunk . release-thunk) pair, suitable for logger/lock, that takes an advisory POSIX lock on the file at path. It uses chicken.file.posix, which is fcntl-based on CHICKEN 5 and flock-based on CHICKEN 6. The lock is released by the OS if the process dies.
The lockfile is created if needed, opened on the first acquire, and kept open for the life of the process.
(logger/lock (logger/make-flock-lock "app.log.lock"))
Macros
- (logger/install module-name)syntax
Installs module-local logging functions d, i, w, and e that automatically tag messages with module-name.
Must be called at the top level of a module, after imports.
(module my-component (initialize shutdown) (import scheme chicken.base logger) (logger/install my-component) (define (initialize) (i "initializing...") (d "loading config")) (define (shutdown) (i "shutting down")))
Log Levels
Levels from lowest to highest priority:
- debug
- Detailed information for debugging. Priority: 0
- info
- General informational messages. Priority: 1
- warn
- Warning conditions. Priority: 2
- error
- Error conditions. Priority: 3
- none
- Disables logging entirely. Priority: 999
Messages are logged only if their level priority is greater than or equal to the effective level for the module.
Examples
Application with Multiple Modules
;;; main.scm (import scheme chicken.base logger) (import my-db my-http) ;; Set global level to info (logger/level 'info) ;; But show debug messages from database module (logger/set-module-level! 'my-db 'debug) ;; Suppress http module unless errors (logger/set-module-level! 'my-http 'error) (db-connect "localhost") (http-serve 8080)
;;; my-db.scm (module my-db (db-connect) (import scheme chicken.base logger) (logger/install my-db) (define (db-connect host) (i "connecting to " host) (d "using default port 5432") ;; ... (i "connected")))
;;; my-http.scm (module my-http (http-serve) (import scheme chicken.base logger) (logger/install my-http) (define (http-serve port) (i "starting server on port " port) ;; suppressed (d "debug info") ;; suppressed ;; ... ))
JSON Logging for Production
(import logger chicken.file.posix) ;; Configure for production (logger/format 'json) (logger/level 'info) (logger/output (open-output-file "/var/log/myapp.log")) (logger/i "application started") ;; {"ts":1737226071,"level":"info","module":"GLOBAL","message":"application started"}
License
BSD-3-Clause
Version History
- 0.0.7
- CHICKEN 6 support (the same source builds on CHICKEN 5 and 6). Replaced medea with srfi-180 for JSON output: list-valued structured fields must now be vectors. logger/make-flock-lock uses flock on CHICKEN 6 (fcntl on CHICKEN 5).
- 0.0.6
- Atomic per-record writes; added logger/lock and logger/make-flock-lock for multi-process logging.
- 0.0.5
- Structured fields are rendered in text output.
- 0.0.4
- Structured fields (alist) in JSON output.
- 0.0.3
- Added logger/error-hook.
- 0.0.2
- Initial release.