Logging¶
A program reports on itself through a logger, apart from its result.
import logging provides one. Logger(endpoint, Format, name = …) names a
producer and the destination it writes to:
import logging
log = Logger(stderr, Text, name = "club")
unread = ["The Hobbit", "Leaves of Grass", "The Common Reader"]
log | info("selected", {books: unread | size})
log | debug("not shown")
unread | size
INFO club selected {books: 3}
3
The event goes to standard error and the result to standard output. A logger
has five operations, one per level, ordered from least to most severe:
trace, debug, info, warn and error. Each takes a message and,
optionally, structured data, and is called through a pipe or directly:
info(log, "x") is log | info("x"). Each gives Unit, so a log call never
becomes part of a result.
The threshold¶
A logger writes events at its threshold or above, Info unless level says
otherwise, so the debug call above writes nothing. A level is a type, and
the threshold is passed as one:
import logging
log = Logger(stdout, Text, name = "club", level = Warn)
log | info("quiet")
log | warn("loud")
1
WARN club loud
1
--log-level overrides every threshold without changing the program:
$ hql --log-level debug eval 'import logging
log = Logger(stderr, Text, name = "club")
log | debug("now shown")
1'
DEBUG club now shown
1
The event and its format¶
A logger writes as sink would: one document per event, in the format
given. In Text an event is one line: the level in capitals, the logger's
name, the message, then the data unless it is empty. In Json each event is
one document with four keys:
import logging
log = Logger(stdout, Json, name = "club")
log | warn("low stock", {title: "Dune"})
1
{"data":{"title":"Dune"},"level":"Warn","logger":"club","message":"low stock"}
1
A log file is appended to by naming its end,
Logger(file("app.log").end, Text, name = "club"); a logger on file(p)
replaces the file with this run's events, by the rules of sink.
Logs as values¶
A logger whose destination is a Channel<LogEvent> takes no format and keeps
its events as values, which the rest of the program can query. A level is a
type, so it is tested with is:
import logging
events = Channel<LogEvent>("events")
log = Logger(events, name = "club", level = Debug)
log | debug("loaded")
log | warn("low stock", {title: "Dune"})
log | error("missing", {title: "Ulysses"})
source(events)
| filter(e => (e.level | is(Warn)) or (e.level | is(Error)))
| map(e => {level: e.level, message: e.message})
[{level: "Warn", message: "low stock"}, {level: "Error", message: "missing"}]
Inside a document, the host binds cell, the cell being rendered.
Logger(cell.logs, name = …) writes to that cell's own channel, whose events
an editor shows beside the cell's result. They are never saved into the
document: the cell's result is its value, unchanged by anything it logged.
Constructing a logger is pure. What a logger writes to is checked by the
host's policy as a sink to that endpoint, so logging grants nothing a
program could not do with sink (effects and what a host allows).