Skip to content
macnodPublic

About

A tiny Common Lisp library for logging

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

p-log

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.

Quick start

;; 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)

Severity

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.

Multiple sinks

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)                 ; => 3

If no sinks are open, the logging functions are no-ops that return 0.

Output formats

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

Conventions

  • The :in key 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 :error entry complaining about it.
  • Reserved keys. :message, :timestamp, and :severity are stripped from your plist (with an :error entry noting it), since the library owns them.
  • Malformed plists (odd length, non-keyword keys) are not logged; an :error entry 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.txt rather 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 to close-log-stream that you gave make-log-stream.

API reference

make-log-stream name file-or-stream &key log-format append severity-threshold

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 by close-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*), or NIL (defaults to *standard-output*).
  • log-format — :jsonl (default) or :plain.
  • append — default T; if NIL, 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.

close-log-stream name

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.

close-log-streams

Closes all open sinks and returns the number that were closed.

list-log-streams

Returns a list of the names of all open sinks.

plog severity plist

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.

pdebug / pinfo / pwarn / perror &rest plist

Convenience wrappers around plog for each severity:

(pinfo :in "load-config" :path path :size size)

pversion

Returns the library version string (currently "0.1").

Dependencies

  • SBCL (sb-thread for the log mutex)
  • dc-ds for JSON serialization
  • dc-time for timestamps

Tests are in p-log-tests.lisp (FiveAM).

About

A tiny Common Lisp library for logging

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages