From 318620c85458f3832254e977ce196f954a5a83c5 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Mon, 3 Aug 2026 11:11:20 +0200 Subject: [PATCH] simple log to store. --- README.md | 25 ++- info.rkt | 4 +- private/store.rkt | 2 +- scrbl/simple-log.scrbl | 378 ++++++++++++++++++++++++++++++----------- 4 files changed, 308 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 2812d65..d534697 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ # simple-log -Extends on racket logging to make it more simple \ No newline at end of file +`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. diff --git a/info.rkt b/info.rkt index 0d59aa9..a536bc8 100644 --- a/info.rkt +++ b/info.rkt @@ -1,8 +1,8 @@ #lang info (define pkg-authors '(hnmdijkema)) -(define version "0.1.2") -(define license 'Apache-2.0) +(define version "0.2.1") +(define license 'MIT) (define collection "simple-log") (define pkg-desc "simple-log - A simple wrapper around the racket logging system") diff --git a/private/store.rkt b/private/store.rkt index c1d6341..cb43d8a 100644 --- a/private/store.rkt +++ b/private/store.rkt @@ -82,7 +82,7 @@ (if (> (queue-length queue) ml) (begin (dequeue! queue) - (set-sl-store-length! store (- sl-store-length store) 1) + (set-sl-store-length! store (- sl-store-length store 1)) (loop)) #t)) ) diff --git a/scrbl/simple-log.scrbl b/scrbl/simple-log.scrbl index faa7258..1320ecd 100644 --- a/scrbl/simple-log.scrbl +++ b/scrbl/simple-log.scrbl @@ -1,157 +1,341 @@ #lang scribble/manual -@(require (for-label racket/base)) +@(require (for-label racket/base + racket/contract + simple-log)) @title{simple-log} @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] -A small logging layer on top of Racket’s logger system. A log definition -creates a logger plus five convenience procedures. Messages are formatted -with @racket[format], timestamped, and dispatched asynchronously to the -registered callbacks. +@defmodule[simple-log] -@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"] - -@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: +@section{Quick start} @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 -(@litchar{YYYY-MM-DDTHH:MM:SS}) and topic @racket['id]. +A log line has the following format: -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 -to the registered callbacks. +@section{Defining a logger} + +@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)]{ 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 -macro expansion time to a symbol and used as key in the callback registry. - -Invocation shape: +The callback is invoked as: @racketblock[ (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 - @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: +For example: @racketblock[ -":::" +(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?]{ -Registers a callback that writes log lines to the current output port -using @racket[displayln]. +Registers a destination that writes log lines to the current output port with +@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 +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?]{ -Equivalent to combining @racket[sl-log-to-display] and -@racket[sl-log-to-file]. +Enables both the display and file destinations. } +@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 - [l (or/c 'debug 'dbg - 'info - 'warning 'warn - 'error 'err - 'fatal)]) + [level (or/c 'debug 'dbg + 'info + 'warning 'warn + 'error 'err + 'fatal)]) symbol?]{ -Sets the module-wide log level and returns the normalized symbol. - -Aliases: - -@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]. +Sets the module-wide minimum log level and returns its normalized symbol. +The aliases @racket['dbg], @racket['warn], and @racket['err] are normalized to +@racket['debug], @racket['warning], and @racket['error]. Other values raise an +exception. } @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} -A call to @racket[sl-def-log] creates five procedures. For example: +For a definition such as: @racketblock[ (sl-def-log my-module) ] -creates: +simple-log creates the following procedures: @defproc[#:link-target? #f - (dbg-my-module [msg string?] [arg any/c] ...) void?]{Debug log.} -@defproc[#:link-target? #f - (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.} + (dbg-my-module [message string?] [argument any/c] ...) void?]{ +Emits a debug message. +} -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.