log - leveled structured logging
Import with import "log.j" as log;. Leveled, structured logging: a log.Logger carries a minimum level, an output format, and a sink; log.info(logger, message, fields) (and the sibling levels) render one record - a timestamp, the level, the message, and the caller's key/value fields - and write it, dropping records below the logger's level.
import "log.j" as log;
def lg as log.Logger init log.new("info", "logfmt");
def f as map of string to string init {"user": "ada", "id": "42"};
log.info($lg, "user logged in", $f);
# time=2026-... level=info msg="user logged in" user=ada id=42Runnable: examples/modules/log_demo.j.
Loggers
A log.Logger is value-semantic; build one with a constructor that fixes the sink:
| Constructor | Sink |
|---|---|
log.new(level, format) | standard output |
log.toStderr(level, format) | standard error |
log.toFile(level, format, path) | append to a file (created if missing) |
log.toSyslog(level, address, app) | an RFC 5424 syslog server over UDP |
level is the minimum level to emit - one of "debug" < "info" < "warn" < "error" - so a logger at "info" silently drops debug records. format is "text", "logfmt", or "json" (the syslog sink uses RFC 5424 framing and ignores it).
Levels
Each level has a method taking the logger, a message, and a map of string to string of fields ({} for none):
| Method | |
|---|---|
log.debug(logger, message, fields) | verbose / development detail |
log.info(logger, message, fields) | normal operation |
log.warn(logger, message, fields) | something unexpected but handled |
log.error(logger, message, fields) | a failure |
log.fatal(logger, message, fields) | an unrecoverable failure - logs then exits |
log.at(logger, level, message, fields) | emit at a level chosen at runtime |
The level ordering is "debug" < "info" < "warn" < "error" < "fatal". A record below the logger's level is dropped before rendering, so guarded log.debug calls in hot paths cost only the level comparison. log.fatal ranks above every other level, so it is always emitted regardless of the logger's minimum level, and then terminates the whole program with exit code 1 - the statement after a log.fatal call never runs.
Context loggers
log.with(logger, fields) returns a new logger that carries fields as persistent context: every record emitted through it includes those fields, merged with any per-call fields (a per-call key of the same name wins). The parent logger is unchanged (value semantics), and with composes - calling it again merges the new fields on top of the carried ones:
def base as log.Logger init log.new("info", "logfmt");
def req as log.Logger init log.with($base, {"svc": "api", "reqId": "abc"});
log.info($req, "handling", {"port": "8080"});
# time=... level=info msg=handling svc=api reqId=abc port=8080| Function | |
|---|---|
log.with(logger, fields) | derive a child logger carrying persistent fields |
Formats
The same record - timestamp, level, message, fields - in each format:
| Format | Example |
|---|---|
text | 2026-01-02T03:04:05Z INFO user logged in user=ada id=42 |
logfmt | time=2026-01-02T03:04:05Z level=info msg="user logged in" user=ada id=42 |
json | {"time":"2026-01-02T03:04:05Z","level":"info","msg":"user logged in","user":"ada","id":"42"} |
Timestamps are RFC 3339 in UTC. A field value containing a space, a quote, or an = is double-quoted (with inner quotes escaped) in the text / logfmt forms, so pairs stay parseable; the json form is a proper object with the field keys alongside time / level / msg (a field named time / level / msg overrides the built-in key).
Sinks and portability
stdout/stderr(viaio.printf/io.eprintf) andfile(viafs.appendString) work on both binaries.syslogsends each record as an RFC 5424 datagram over UDP (net):<PRI>1 TIMESTAMP HOST APP - - - MSG, where the priority is facilityuser(1) times 8 plus the level's severity (error=3,warn=4,info=6,debug=7), the host is$HOSTNAME(or-), and the fields ride in the message as logfmt pairs. Because it usesnet, the syslog sink needs the defaultjenniferbinary - the module is therefore partial onjennifer-tiny(console and file logging work; the syslog sink returns the no-network error).
Scope
- String fields.
fieldsis amap of string to string- convert numbers / bools to strings at the call site (convert.toString). This keeps the record model simple and the value quoting predictable. - UDP syslog. No TCP / TLS syslog transport, and no local
/dev/logsocket. - No global default logger. Loggers are values you pass explicitly - no hidden singleton, no package-level state.
See also
- io.md -
printf/eprintf, the stdout / stderr sinks. - fs.md -
appendString, the file sink. - net.md - the UDP transport the syslog sink uses.
- modules/index.md - the module catalog and import rules.