A tiny Common Lisp logging library. Log entries are plists sent to any number of log sinks (files or streams), each with its own format (JSONL or plain text) and severity threshold. Requires SBCL.
;; Log everything to the console as plain text...
(make-log-stream "console" nil :log-format :plain)
;; ...and warnings-and-up to a JSONL file.
(make-log-stream "app-log" "/var/log/myapp/app.log"
:severity-threshold :warn)
(pinfo :in "start-server" :port 8080)
(pdebug :in "handle-request" :method "GET" :path "/users")
(perror :in "handle-request" :status "db down" :error err)This is how the data-ui project uses it — one sink set up at startup,
then pdebug/perror calls sprinkled through the code:
(defun init ()
(when *log-file*
(pl:make-log-stream "data-ui" *log-file*)))
(pl:pdebug :in "be-insert" :step 1
:type-key type-key :status "validated")
(pl:perror :in "be-insert"
:status "write-through failed"
:type-key type-key :field-key field-key :error e)From lowest to highest: :debug, :info, :warn, :error.
A log entry is written to a sink only when the entry's severity is
greater than or equal to that sink's :severity-threshold. A
:debug sink gets everything; an :error-threshold sink only gets
:error entries.
You can have any number of open sinks (files and/or streams) at once. Every logging call fans out to all sinks whose threshold admits the entry, and returns the number of sinks actually written to:
(make-log-stream "all" nil :log-format :plain) ; threshold :debug
(make-log-stream "err" *error-output*) ; threshold :debug
(make-log-stream "boom" "/var/log/boom.log"
:severity-threshold :error)
(list-log-streams) ; => ("boom" "err" "all")
(pwarn :in "x" :msg "meh") ; => 2 (all, err; boom filters it out)
(perror :in "y" :msg "kaboom") ; => 3 (all sinks)
(close-log-streams) ; => 3If no sinks are open, the logging functions are no-ops that return 0.
:plain produces traditional log lines:
2025-01-15T14:32:07.123456-05:00 [INFO] in=start-server; port=8080
2025-01-15T14:32:08.001234-05:00 [ERROR] in=handle-request; status=db down; error=#<DB-ERROR ...>
:jsonl (the default) produces one JSON object per line, with the
plist nested under "message" (illustrative):
{"timestamp":"2025-01-15T14:32:07.123456-05:00","severity":"INFO","message":{"in":"start-server","port":"8080"}}
In both formats the :timestamp and :severity keys are supplied by
the library and values are stringified. Nested plists become JSON
maps; other lists become arrays.
- The
:inkey is mandatory. Its value should be the lower-case name of the function doing the logging. If it's missing, the entry is still logged with:in "(missing)", followed by an extra:errorentry complaining about it. - Reserved keys.
:message,:timestamp, and:severityare stripped from your plist (with an:errorentry noting it), since the library owns them. - Malformed plists (odd length, non-keyword keys) are not logged;
an
:errorentry describing the problem is written instead. - Output is flushed (
force-output) after each entry. If logging itself signals an error, the condition is appended to/tmp/p-log-error.txtrather than propagated. - Timestamps are UTC. Each entry is written to each sink under the package's log mutex, so concurrent threads won't interleave lines.
- Log names are compared with
equal— pass the exact same string toclose-log-streamthat you gavemake-log-stream.
Opens a log sink and adds it to the sink list. Returns the new sink
record on success, NIL on failure (bad arguments or unopenable
file).
name— string uniquely identifying the sink, used byclose-log-stream. Duplicate names and already-registered streams are reported as errors.file-or-stream— a string or pathname (opened as a log file), a stream (used directly, e.g.*standard-output*), orNIL(defaults to*standard-output*).log-format—:jsonl(default) or:plain.append— defaultT; ifNIL, an existing file is truncated. Only meaningful for file sinks.severity-threshold—:debug(default),:info,:warn, or:error. Dictates the minimum severity an entry needs to reach this sink.
Closes the sink named name: file-backed streams are closed, the
sink is removed from the sink list, and T is returned. If no sink
with that name exists, does nothing and returns NIL.
Closes all open sinks and returns the number that were closed.
Returns a list of the names of all open sinks.
Logs one entry with severity severity (:debug, :info, :warn,
or :error) and the key/value pairs in plist (keyword keys; values
are stringified). See Conventions above for the :in key and
reserved keys. Returns the number of sinks the entry was written to.
Convenience wrappers around plog for each severity:
(pinfo :in "load-config" :path path :size size)Returns the library version string (currently "0.1").
- SBCL (
sb-threadfor the log mutex) dc-dsfor JSON serializationdc-timefor timestamps
Tests are in p-log-tests.lisp (FiveAM).