simple log to store.

This commit is contained in:
2026-08-03 11:11:20 +02:00
parent 5869df9f59
commit 318620c854
4 changed files with 308 additions and 101 deletions
+24 -1
View File
@@ -1,3 +1,26 @@
# simple-log # simple-log
Extends on racket logging to make it more simple `simple-log` is a small convenience layer on top of Racket's logging system.
It provides generated procedures for the standard log levels and supports
logging to the display, a file, custom callbacks, and a filterable in-memory
store.
```racket
#lang racket
(require simple-log)
(sl-def-log example)
(define logs (sl-log-to-store 1000))
(sl-log-to-display)
(info-example "Started with ~a items" 3)
(warn-example "An example warning")
(sync-log-example)
(sl-store->display
(sl-store-grep-level logs 'warning))
```
The Scribble manual documents logger definitions, destinations, log levels,
synchronization, and all in-memory store operations.
+2 -2
View File
@@ -1,8 +1,8 @@
#lang info #lang info
(define pkg-authors '(hnmdijkema)) (define pkg-authors '(hnmdijkema))
(define version "0.1.2") (define version "0.2.1")
(define license 'Apache-2.0) (define license 'MIT)
(define collection "simple-log") (define collection "simple-log")
(define pkg-desc "simple-log - A simple wrapper around the racket logging system") (define pkg-desc "simple-log - A simple wrapper around the racket logging system")
+1 -1
View File
@@ -82,7 +82,7 @@
(if (> (queue-length queue) ml) (if (> (queue-length queue) ml)
(begin (begin
(dequeue! queue) (dequeue! queue)
(set-sl-store-length! store (- sl-store-length store) 1) (set-sl-store-length! store (- sl-store-length store 1))
(loop)) (loop))
#t)) #t))
) )
+281 -97
View File
@@ -1,157 +1,341 @@
#lang scribble/manual #lang scribble/manual
@(require (for-label racket/base)) @(require (for-label racket/base
racket/contract
simple-log))
@title{simple-log} @title{simple-log}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
A small logging layer on top of Rackets logger system. A log definition @defmodule[simple-log]
creates a logger plus five convenience procedures. Messages are formatted
with @racket[format], timestamped, and dispatched asynchronously to the
registered callbacks.
@section{API} A small logging layer on top of Racket's logger system. A log definition
creates a logger and convenience procedures for the standard log levels.
Messages are formatted with @racket[format], timestamped, and dispatched
asynchronously to one or more registered destinations.
@defmodule["simple-log"] @section{Quick start}
@defform*[((sl-def-log id)
(sl-def-log id name)
(sl-def-log id name parent))]{
Defines a logger with topic @racket['id] and creates:
@itemlist[#:style 'compact
@item{@racket[dbg-id]}
@item{@racket[info-id]}
@item{@racket[warn-id]}
@item{@racket[err-id]}
@item{@racket[fatal-id]}
@item{@racket[sync-id]}
]
Note. If name is given, id @racket[dbg-prefix], etc. will be generated instead of @racket[dbg-id], etc.
Each procedure has shape:
@racketblock[ @racketblock[
(proc msg arg ...) (require simple-log)
(sl-def-log example)
(sl-log-to-display)
(info-example "Started with ~a items" 3)
(warn-example "This is only an example")
] ]
The message is formatted via @racket[format] and emitted with a timestamp A log line has the following format:
(@litchar{YYYY-MM-DDTHH:MM:SS}) and topic @racket['id].
If @racket[parent] is omitted, the @racket[#f] is used as "parent logger". @verbatim{example:info:2026-08-03T10:30:00:Started with 3 items}
A background thread is started that receives log events and forwards them @section{Defining a logger}
to the registered callbacks.
@defform*[((sl-def-log id)
(sl-def-log id prefix)
(sl-def-log id prefix parent))]{
Defines a logger with topic @racket['id]. The generated procedure names use
@racket[id] as their prefix unless an explicit @racket[prefix] is supplied.
For example:
@racketblock[
(sl-def-log player)
]
creates @racket[dbg-player], @racket[info-player], @racket[warn-player],
@racket[err-player], @racket[fatal-player], and @racket[sync-log-player].
With an explicit prefix:
@racketblock[
(sl-def-log media-renderer renderer)
]
creates the same procedures with @racket[renderer] in their names, while the
log topic remains @racket['media-renderer].
If @racket[parent] is omitted, @racket[#f] is used as the parent logger.
} }
@section{Log destinations}
@defform[(sl-log-to name callback)]{ @defform[(sl-log-to name callback)]{
Registers @racket[callback] under the symbolic name derived from Registers @racket[callback] under the symbolic name derived from
@racket[name]. @racket[name]. The name is an identifier, not a runtime value. Registering
another callback with the same name replaces the previous callback.
@racket[name] is an identifier (not a runtime value). It is converted at The callback is invoked as:
macro expansion time to a symbol and used as key in the callback registry.
Invocation shape:
@racketblock[ @racketblock[
(callback topic level timestamp message) (callback topic level timestamp message)
] ]
with: The arguments are a topic symbol, a level symbol, an ISO-like timestamp in
@litchar{YYYY-MM-DDTHH:MM:SS} form, and the formatted message string.
@itemlist[#:style 'compact For example:
@item{@racket[topic] — logger topic (symbol)}
@item{@racket[level] — level (symbol)}
@item{@racket[timestamp] — @litchar{YYYY-MM-DDTHH:MM:SS}}
@item{@racket[message] — formatted string}
]
An existing callback with the same name is replaced.
}
@defproc[(sl-log-to-file [filename path-string?]) void?]{
Registers a callback that writes log lines to @racket[filename]. The file
is opened with @racket['replace].
Format:
@racketblock[ @racketblock[
"<topic>:<level>:<timestamp>:<message>" (sl-log-to collect-errors
(lambda (topic level timestamp message)
(when (memq level '(error fatal))
(displayln (list timestamp topic message)))))
] ]
} }
@defproc[(sl-log-to-display) void?]{ @defproc[(sl-log-to-display) void?]{
Registers a callback that writes log lines to the current output port Registers a destination that writes log lines to the current output port with
using @racket[displayln]. @racket[displayln]. Calling the procedure again replaces the existing display
destination.
}
Format: @defproc[(sl-log-to-file [filename path-string?]) void?]{
@racketblock[ Registers a destination that writes log lines to @racket[filename]. The file is
"<topic>:<level>:<timestamp>:<message>" opened with @racket['replace], and the output is flushed after every line.
] Calling the procedure again replaces the existing file destination.
} }
@defproc[(sl-log-to-file&display [filename path-string?]) void?]{ @defproc[(sl-log-to-file&display [filename path-string?]) void?]{
Equivalent to combining @racket[sl-log-to-display] and Enables both the display and file destinations.
@racket[sl-log-to-file].
} }
@defproc[(sl-log-to-store [max-length exact-nonnegative-integer? 1000]) any/c]{
Creates an in-memory log store, registers it as the current store destination,
and returns it. At most @racket[max-length] entries are retained; older entries
are removed when the limit is exceeded.
Only one destination named @racket[store] is active. A later call to
@racket[sl-log-to-store] replaces the previous store destination, but does not
modify the previously returned store.
Logging is asynchronous. Use the generated synchronization procedure before
reading the store when all previously submitted messages must be present.
@racketblock[
(sl-def-log worker)
(define logs (sl-log-to-store 200))
(info-worker "Starting job ~a" 42)
(warn-worker "Job ~a is slow" 42)
(sync-log-worker)
(sl-store->display logs)
]
}
@section{Log level}
@defproc[(sl-set-log-level @defproc[(sl-set-log-level
[l (or/c 'debug 'dbg [level (or/c 'debug 'dbg
'info 'info
'warning 'warn 'warning 'warn
'error 'err 'error 'err
'fatal)]) 'fatal)])
symbol?]{ symbol?]{
Sets the module-wide log level and returns the normalized symbol. Sets the module-wide minimum log level and returns its normalized symbol.
The aliases @racket['dbg], @racket['warn], and @racket['err] are normalized to
Aliases: @racket['debug], @racket['warning], and @racket['error]. Other values raise an
exception.
@itemlist[#:style 'compact
@item{@racket['dbg] → @racket['debug]}
@item{@racket['warn] → @racket['warning]}
@item{@racket['err] → @racket['error]}
]
Other values raise an exception.
Note: this value is stored globally. The receiver installed by
@racket[sl-def-log] itself operates at level @racket['debug].
} }
@defproc[(sl-log-level) symbol?]{ @defproc[(sl-log-level) symbol?]{
Returns the current module-wide log level (default @racket['debug]). Returns the current module-wide minimum log level. The default is
@racket['debug].
} }
@section{Synchronization}
@defproc[(sl-sync [logger logger?] [topic symbol?]) void?]{
Submits a synchronization event to @racket[logger] and waits until the
simple-log receiver has processed it. Normally this lower-level procedure is
not needed directly; use the @racket[sync-log-prefix] procedure generated by
@racket[sl-def-log].
}
Synchronization is useful before inspecting an in-memory store or before a
program exits immediately after its final log message.
@section{In-memory stores}
A store contains log entries in chronological order. Every entry consists of:
@racketblock[
(list topic level timestamp message)
]
The filtering procedures return new stores and leave the original store
unchanged. The new store has the same maximum length as the original.
@defproc[(make-sl-store [max-length exact-nonnegative-integer? 1000]) any/c]{
Creates an empty store without registering it as a log destination. Most code
uses @racket[sl-log-to-store] instead.
}
@defproc[(sl-store-enqueue! [store any/c]
[topic symbol?]
[level (or/c 'debug 'dbg
'info
'warning 'warn
'error 'err
'fatal)]
[timestamp string?]
[message string?])
void?]{
Adds an entry to @racket[store]. When the maximum length is exceeded, entries
are removed from the beginning of the store.
}
@defproc[(sl-store-length [store any/c]) exact-nonnegative-integer?]{
Returns the current number of entries in @racket[store].
}
@defproc[(sl-store-max-length [store any/c]) exact-nonnegative-integer?]{
Returns the configured maximum number of entries in @racket[store].
}
@defproc[(sl-store->list [store any/c]) list?]{
Returns the entries as a list in chronological order. Each entry is a list of
topic, level, timestamp, and message.
}
@defproc[(sl-store->display [store any/c]) void?]{
Writes all entries to the current output port in the same colon-separated
format used by the display and file destinations.
}
@defproc[(sl-store-grep-message [store any/c] [regexp regexp?]) any/c]{
Returns a store containing entries whose message matches @racket[regexp].
}
@defproc[(sl-store-grep-topic [store any/c]
[topic-or-regexp (or/c symbol? regexp?)])
any/c]{
With a symbol, returns entries whose topic is exactly that symbol. With a
regular expression, matches against the string form of the topic.
}
@defproc[(sl-store-grep-level [store any/c]
[level-or-regexp
(or/c regexp?
'debug 'dbg
'info
'warning 'warn
'error 'err
'fatal)])
any/c]{
Returns entries at the selected level or a more severe level. A regular
expression is matched against the available level names and selects the first
matching level.
@racketblock[
(sl-store-grep-level logs 'warning)
(sl-store-grep-level logs #rx"err(or)?")
]
}
@defproc[(sl-store-grep [store any/c]
[value (or/c symbol? regexp?)])
any/c]{
Performs a combined search.
When @racket[value] is a log-level symbol, the result contains entries at that
level or a more severe level. Any other symbol is treated as an exact topic.
A regular expression is matched against both topic and message. If that same
regular expression also identifies a log level, entries below that level are
excluded.
}
@defproc[(sl-store-tail [store any/c]
[count exact-nonnegative-integer?])
any/c]{
Returns a store containing the last @racket[count] entries. If
@racket[count] is greater than the store length, all entries are returned.
}
@section{Store examples}
@racketblock[
(sl-def-log player)
(define logs (sl-log-to-store 500))
(info-player "Opening ~a" "album.flac")
(warn-player "No duration available")
(err-player "Renderer returned status ~a" 701)
(sync-log-player)
(define warnings-and-errors
(sl-store-grep-level logs 'warning))
(define renderer-errors
(sl-store-grep
(sl-store-grep-topic logs 'player)
#rx"renderer|status"))
(sl-store->display (sl-store-tail warnings-and-errors 20))
]
@section{Generated procedures} @section{Generated procedures}
A call to @racket[sl-def-log] creates five procedures. For example: For a definition such as:
@racketblock[ @racketblock[
(sl-def-log my-module) (sl-def-log my-module)
] ]
creates: simple-log creates the following procedures:
@defproc[#:link-target? #f @defproc[#:link-target? #f
(dbg-my-module [msg string?] [arg any/c] ...) void?]{Debug log.} (dbg-my-module [message string?] [argument any/c] ...) void?]{
@defproc[#:link-target? #f Emits a debug message.
(info-my-module [msg string?] [arg any/c] ...) void?]{Info log.} }
@defproc[#:link-target? #f
(warn-my-module [msg string?] [arg any/c] ...) void?]{Warning log.}
@defproc[#:link-target? #f
(err-my-module [msg string?] [arg any/c] ...) void?]{Error log.}
@defproc[#:link-target? #f
(fatal-my-module [msg string?] [arg any/c] ...) void?]{Fatal log.}
@defproc[#:link-target? #f
(sync-log-my-module) void?]{Puts a sync message to the logger and waits for the receiver until it logs this synchronization event.}
All, except for @racket[sync-log-my-module] use @racket[format] and emit asynchronously. @defproc[#:link-target? #f
(info-my-module [message string?] [argument any/c] ...) void?]{
Emits an informational message.
}
@defproc[#:link-target? #f
(warn-my-module [message string?] [argument any/c] ...) void?]{
Emits a warning message.
}
@defproc[#:link-target? #f
(err-my-module [message string?] [argument any/c] ...) void?]{
Emits an error message.
}
@defproc[#:link-target? #f
(fatal-my-module [message string?] [argument any/c] ...) void?]{
Emits a fatal message.
}
@defproc[#:link-target? #f
(sync-log-my-module) void?]{
Waits until the simple-log receiver has processed all earlier messages from
this logger.
}
The message procedures pass @racket[message] and @racket[argument] values to
@racket[format]. Log delivery to the registered destinations is asynchronous.