simple logger egg for CHICKEN scheme
Find a file
2026-05-08 09:20:46 -07:00
tests Add atomic multi-process log writes with optional advisory lock 2026-05-08 09:20:46 -07:00
.gitignore first commit 2026-01-18 11:11:52 -08:00
LICENSE first commit 2026-01-18 11:11:52 -08:00
logger.egg Add atomic multi-process log writes with optional advisory lock 2026-05-08 09:20:46 -07:00
logger.release-info Add atomic multi-process log writes with optional advisory lock 2026-05-08 09:20:46 -07:00
logger.scm Add atomic multi-process log writes with optional advisory lock 2026-05-08 09:20:46 -07:00
README.md Add atomic multi-process log writes with optional advisory lock 2026-05-08 09:20:46 -07:00

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_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:

csi -s tests/run-tests.scm

License

BSD 3-Clause - see LICENSE file