From d3a704601591efc2badd80daacdb65997eaa1078 Mon Sep 17 00:00:00 2001 From: Dexter Date: Thu, 16 Apr 2026 17:39:05 +0800 Subject: [PATCH 1/2] New basic logging module using kx.log as reference --- di/log/init.q | 4 ++ di/log/log.md | 62 +++++++++++++++++ di/log/log.q | 180 ++++++++++++++++++++++++++++++++++++++++++++++++ di/log/test.csv | 109 +++++++++++++++++++++++++++++ 4 files changed, 355 insertions(+) create mode 100644 di/log/init.q create mode 100644 di/log/log.md create mode 100644 di/log/log.q create mode 100644 di/log/test.csv diff --git a/di/log/init.q b/di/log/init.q new file mode 100644 index 00000000..0d9eaadc --- /dev/null +++ b/di/log/init.q @@ -0,0 +1,4 @@ +// simple stdout logger - default log dependency for di.* modules +\l ::log.q + +export:([trace;debug;info;warn;error;fatal;createLog]) diff --git a/di/log/log.md b/di/log/log.md new file mode 100644 index 00000000..aeb143e8 --- /dev/null +++ b/di/log/log.md @@ -0,0 +1,62 @@ +# Log + +`log.q` is the default logging implementation for `di.*` modules. It writes formatted lines to stdout and satisfies the log dependency contract expected by modules such as `di.email`. + +## Usage + +```q +log:use`di.log +log.info[`mymodule;"starting up"] +log.warn[`mymodule;"disk usage above 80%"] +log.error[`mymodule;"connection failed"] +``` + +Output format: + +``` +2026-04-09T12:00:00.000000000 [INFO] [mymodule] starting up +2026-04-09T12:00:00.001000000 [WARN] [mymodule] disk usage above 80% +2026-04-09T12:00:00.002000000 [ERROR] [mymodule] connection failed +``` + +## Injecting into other modules + +All `di.*` modules that accept a log dependency expect a dictionary with keys `` `info`warn`error ``, each a function with signature `{[ctx;msg]}`. + +```q +log:use`di.log +logdep:`info`warn`error!(log.info;log.warn;log.error) + +email:use`di.email +email.init[emailconfig;`log`send!(logdep;::)] +``` + +## API + +### `info` +Parameters: `[ctx; msg]` + +Write an info-level message to stdout. + +- `ctx` — symbol context tag (e.g. `` `mymodule ``) +- `msg` — string message + +### `warn` +Parameters: `[ctx; msg]` + +Write a warning-level message to stdout. + +### `error` +Parameters: `[ctx; msg]` + +Write an error-level message to stdout. + +## Log dependency contract + +The log dependency contract used across `di.*` modules requires a dictionary: + +```q +`info`warn`error!({[ctx;msg] ...};{[ctx;msg] ...};{[ctx;msg] ...}) +``` + +`di.log` satisfies this contract. You can also supply any custom implementation with the same signatures. diff --git a/di/log/log.q b/di/log/log.q new file mode 100644 index 00000000..88952a0c --- /dev/null +++ b/di/log/log.q @@ -0,0 +1,180 @@ +// simple stdout logger for di.* modules +// provides info, warn, error, trace, debug, fatal with signature {[ctx;msg]} +// ctx is a symbol context tag, msg is a string +// also provides createLog, a factory for rich structured logger instances + +// os-aware newline +nl:$[.z.o in `w32`w64;"\r\n";"\n"]; + +// log levels in priority order +lvls:`trace`debug`info`warn`error`fatal; + +// syslog severity per level (rfc5424) +syslogLvl:lvls!7 7 6 4 3 2i; + +// built-in format templates for createLog instances +fmts:`basic`syslog`raw!("$p $l PID[$i] HOST[$h] $m";"<$s> $m";"$m"); + +// format pattern handlers for createLog instances; each takes {[level;msg]} +// level is an uppercase string (e.g. "INFO"), msg is the formatted message string +pattern:"plihms~"!( + {[x;y] string .z.p}; + {[x;y] x}; + {[x;y] string .z.i}; + {[x;y] string .z.h}; + {[x;y] y}; + {[x;y] string .z.M.syslogLvl`$lower x}; + {[x;y] "$"}); + +// split a format template on delimiter, returning (textparts; substitutionfunctions) +// escaped delimiter (e.g. $$) is replaced by $~ which resolves to literal $ +fmtprep:{[del;rep;fmt] + fmt:ssr[fmt;del,del;del,"~"]; + parts:del vs fmt; + fns:rep@first each 1_parts; + (enlist[first parts],1_/:1_parts;fns) + }; + +// apply a prepared format returning the assembled line string +// level is an uppercase string, msg is the formatted message string +fmtapply:{[prep;level;msg] + textparts:prep 0; + fns:prep 1; + vals:fns .\:(level;msg); + raze first[textparts],vals,'1_textparts + }; + +// apply printf-style variable substitution to a message +// msg is a plain string or (fmtstring;arg1;arg2;...) +// %s converts to string, %r uses .Q.s1, %% becomes a literal percent +fmtmsg:{[msg] + $[10h=abs type msg; + msg; + [fmt:first msg; + args:1_msg; + fmt:ssr[fmt;"%%";"\000"]; + parts:"%" vs fmt; + subs:"sr"!({$[10h=abs type x;x;-11h=type x;string x;'`type]};.Q.s1); + result:first[parts],raze {[subs;args;parts;i] + part:parts[1+i]; + code:first part; + rest:1_part; + if[not code in key subs;'`$"unsupported format char: ",enlist code]; + (subs[code] args[i]),rest + }[subs;args;parts;] each til count[parts]-1; + ssr[result;"\000";"%"] + ] + ]}; + +// format and write a log line to stdout using the dependency-contract format +logline:{[level;ctx;msg] + -1 (string .z.p)," [",level,"] [",string[ctx],"] ",msg; + }; + +// simple dependency-contract-compatible log functions; each has signature {[ctx;msg]} +trace:{[ctx;msg] logline["TRACE";ctx;msg];}; +debug:{[ctx;msg] logline["DEBUG";ctx;msg];}; +info:{[ctx;msg] logline["INFO";ctx;msg];}; +warn:{[ctx;msg] logline["WARN";ctx;msg];}; +error:{[ctx;msg] logline["ERROR";ctx;msg];}; +fatal:{[ctx;msg] logline["FATAL";ctx;msg];}; + +// instance counter and per-instance state; bare names fall back to .z.M when .z.m not yet set +i:0; +inst:()!(); + +// factory helper: returns a 1-arg {[msg]} log function for the given level and instance +// each entry in sink is a (handle;sender) pair; sender is called with the formatted text +makelevel:{[id;gv;lvl] + {[id;gv;lvl;msg] + if[(lvls?lvl) Date: Thu, 16 Apr 2026 17:47:12 +0800 Subject: [PATCH 2/2] Update changes not included in testing and markdown doc for trace, debug and fatal --- di/log/log.md | 100 +++++++++++++++++++++++++++++++++++++++++++++--- di/log/test.csv | 34 ++++++++++++++++ 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/di/log/log.md b/di/log/log.md index aeb143e8..5d05cddd 100644 --- a/di/log/log.md +++ b/di/log/log.md @@ -6,17 +6,23 @@ ```q log:use`di.log +log.trace[`mymodule;"entering function"] +log.debug[`mymodule;"value is 42"] log.info[`mymodule;"starting up"] log.warn[`mymodule;"disk usage above 80%"] log.error[`mymodule;"connection failed"] +log.fatal[`mymodule;"unrecoverable error, shutting down"] ``` Output format: ``` -2026-04-09T12:00:00.000000000 [INFO] [mymodule] starting up -2026-04-09T12:00:00.001000000 [WARN] [mymodule] disk usage above 80% -2026-04-09T12:00:00.002000000 [ERROR] [mymodule] connection failed +2026-04-09T12:00:00.000000000 [TRACE] [mymodule] entering function +2026-04-09T12:00:00.001000000 [DEBUG] [mymodule] value is 42 +2026-04-09T12:00:00.002000000 [INFO] [mymodule] starting up +2026-04-09T12:00:00.003000000 [WARN] [mymodule] disk usage above 80% +2026-04-09T12:00:00.004000000 [ERROR] [mymodule] connection failed +2026-04-09T12:00:00.005000000 [FATAL] [mymodule] unrecoverable error, shutting down ``` ## Injecting into other modules @@ -31,16 +37,77 @@ email:use`di.email email.init[emailconfig;`log`send!(logdep;::)] ``` +You can extend the injected dictionary with `trace`, `debug`, and `fatal` for modules that support them: + +```q +logdep:`trace`debug`info`warn`error`fatal!(log.trace;log.debug;log.info;log.warn;log.error;log.fatal) +``` + +## createLog + +`createLog` is a factory that returns an independent logger instance with level filtering, multiple output sinks, and configurable format templates. Each call to `createLog` produces a separate instance with its own state. + +```q +log:use`di.log +mylog:log.createLog[] + +mylog.setlvl `warn / suppress trace, debug, info +mylog.info "this is suppressed" / returns () silently +mylog.warn "this appears" + +mylog.setfmt `syslog / switch to syslog format +mylog.addfmt[`compact;"$l $m"] / add a custom format +mylog.setfmt `compact + +mylog.add[2i;`error`fatal] / add stderr sink for error and fatal +mylog.remove[1i;`trace] / remove stdout from trace level +``` + +### Sinks + +A sink is a handle (integer file descriptor or function) passed to `add`. If a function is provided it is called with the formatted line string. Built-in handles follow standard q conventions: `1i` is stdout, `2i` is stderr. + +```q +buf:(); +capture:{[msg] buf,:enlist msg}; / function sink - captures output +mylog.add[capture;`info`warn`error] +mylog.info "captured" +buf / ("2026-...captured\n") +mylog.remove[capture;`info] +``` + +### Format templates + +Three built-in formats are available: + +| Name | Template | Example output | +|---|---|---| +| `basic` (default) | `$p $l PID[$i] HOST[$h] $m` | `2026-04-09T12:00:00.000000000 INFO PID[1234] HOST[myhost] message` | +| `syslog` | `<$s> $m` | `<6> message` | +| `raw` | `$m` | `message` | + +Template variables: `$p` timestamp, `$l` level, `$i` PID, `$h` hostname, `$m` message, `$s` syslog severity number. + ## API -### `info` +### `trace` Parameters: `[ctx; msg]` -Write an info-level message to stdout. +Write a trace-level message to stdout. - `ctx` — symbol context tag (e.g. `` `mymodule ``) - `msg` — string message +### `debug` +Parameters: `[ctx; msg]` + +Write a debug-level message to stdout. + +### `info` +Parameters: `[ctx; msg]` + +Write an info-level message to stdout. + ### `warn` Parameters: `[ctx; msg]` @@ -51,6 +118,29 @@ Parameters: `[ctx; msg]` Write an error-level message to stdout. +### `fatal` +Parameters: `[ctx; msg]` + +Write a fatal-level message to stdout. + +### `createLog` +Parameters: none + +Returns an independent logger instance as a dictionary of functions. Each call returns a new instance with isolated state. + +Returned keys: `` `trace`debug`info`warn`error`fatal`add`remove`setfmt`getfmt`addfmt`setlvl`getlvl `` + +| Function | Parameters | Description | +|---|---|---| +| `trace`..`fatal` | `[msg]` | Write a message at the given level (filtered by active level) | +| `setlvl` | `[lvl]` | Set minimum level; one of `` `trace`debug`info`warn`error`fatal `` | +| `getlvl` | `[_]` | Return current minimum level | +| `setfmt` | `[name]` | Switch to a named format template | +| `getfmt` | `[_]` | Return current format name | +| `addfmt` | `[name;template]` | Register a new named format template | +| `add` | `[handle;lvls]` | Add a sink for one or more levels; returns the handle | +| `remove` | `[handle;lvl]` | Remove a sink from a level | + ## Log dependency contract The log dependency contract used across `di.*` modules requires a dictionary: diff --git a/di/log/test.csv b/di/log/test.csv index a42986e5..f3012440 100644 --- a/di/log/test.csv +++ b/di/log/test.csv @@ -107,3 +107,37 @@ run,0,0,q,log1.info "plain string message",1,,instance info with plain string run,0,0,q,log1.info ("message with %s";`symbol),1,,instance info with %s substitution run,0,0,q,log1.info ("message with %r";"raw repr"),1,,instance info with %r substitution run,0,0,q,log1.info "literal 100%% complete",1,,instance info with %% escape + +/ Test 20: trace, debug and fatal accept any symbol ctx +run,0,0,q,lg.trace[`mymodule;"trace custom ctx"],1,,trace with custom module ctx +run,0,0,q,lg.debug[`mymodule;"debug custom ctx"],1,,debug with custom module ctx +run,0,0,q,lg.fatal[`mymodule;"fatal custom ctx"],1,,fatal with custom module ctx + +/ Test 21: instance trace, debug and fatal do not throw +run,0,0,q,log1.setlvl `trace,1,,lower level to trace so trace and debug are not suppressed +run,0,0,q,log1.trace "test trace",1,,instance trace accepts plain string +run,0,0,q,log1.debug "test debug",1,,instance debug accepts plain string +run,0,0,q,log1.fatal "test fatal",1,,instance fatal accepts plain string +run,0,0,q,log1.setlvl `info,1,,restore level to info + +/ Test 22: function sink captures formatted output +before,0,0,q,log3:lg.createLog[],1,,create logger for sink capture test +before,0,0,q,captured:(),1,,initialise capture buffer +before,0,0,q,"capfn:{captured::captured,enlist x}",1,,define capture function that appends to global +run,0,0,q,log3.add[capfn;`info],1,,add function sink to info level +run,0,0,q,log3.info "sink test message",1,,send message through function sink +true,0,0,q,0