chickadee » logger

logger

Simple structured logging for CHICKEN Scheme with per-module level control.

Description

A lightweight logging library that provides:

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:

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.

Contents »