Packages

log

Levelled, timestamped console logging with printf-style messages, grouping, tables, progress bars and a history you can query or export.

import "std:log"

std:log is a logger for programs that run unattended or print a lot: servers, scripts, tools. Every entry has a level (DEBUG, INFO, SUCCESS, WARN or ERROR), a time and a message. You can hide the noisy levels, keep recent entries to inspect or export, and draw tables and progress bars.

osl
import "std:log"

For quick printing while you work, the log statement (log a b) is simpler. Use this package for levels, timestamps and a record of what happened.

Levels#

Each level has a function that takes a message. Lines start with the time of day and the level, and are coloured in a terminal.

main.osl
import "std:log"

log.info("server starting")
log.success("listening on port 8080")
log.warn("cache is cold")
log.error("could not reach the database")
log.debug("this one is hidden")
Click the code to edit it

Debug entries are below the default level, so they are dropped. Output goes to standard output. Levels, from least to most severe, are DEBUG, INFO (and SUCCESS, which ranks the same), WARN and ERROR.

Choose what you see#

log.setLevel(name) sets the least severe level that is printed. log.level() tells you what it is now. Entries below the level are neither printed nor recorded.

main.osl
import "std:log"

log log.level()
log.setLevel("WARN")
log log.level()
log.info("dropped")
log.warn("kept")
log.setLevel("DEBUG")
log log.level()
log log.history().len
log log.history()[1].message
log.setLevel("LOUD") catch ( log "not a level" )
Click the code to edit it

An unknown level name is a ValueError. Turn on DEBUG while chasing a problem, and set WARN for a program that should only speak when something is wrong.

Printf-style messages#

Messages take printf verbs (%s, %d, %f, %v) filled from the arguments that follow. Leftover arguments are appended with a space, so you can also pass plain values.

main.osl
import "std:log"

log.info("%s logged in from %s (%d attempts)", "ada", "10.0.0.2", 3)
log.info("retrying in", 5, "seconds")
log.warn("disk at %.1f%%", 91.256)
for h in log.history() ( log h.level ":" h.message )
Click the code to edit it

The recorded message is the formatted text, without the time or level. The level lines themselves printed above the list, with times, so run it yourself to see them.

Plain output#

log.plain prints without a level, time or colour, and isn't recorded. log.json prints a value as one line of JSON, and log.table a text table. log.separator, log.group and log.groupEnd structure the output.

main.osl
import "std:log"

log.group("Report")
log.plain("total: %d items", 42)
log.json({ok: true, items: [1, 2]})
log.table(["name", "qty"], [["bolt", 12], ["nut", 30]])
log.groupEnd()
log.separator()
Click the code to edit it

Timestamps#

log.time(message) prefixes the time of day to the millisecond, log.timestamp(message) the unix time in microseconds, and log.withTimestamp(level, message) the full date. They are meant for measuring and correlating, and always differ between runs.

main.osl
import "std:log"

log.time("request received")
log.timestamp("tick")
log.withTimestamp("INFO", "something happened")
Click the code to edit it

Check conditions#

log.assert(condition, message) logs an error when the condition is false, and does nothing otherwise. It never stops the program: use testing when a failure must be fatal.

main.osl
import "std:log"

log.assert(1 + 1 == 2, "arithmetic works")
log.assert(1 + 1 == 3, "arithmetic is broken")
log log.countByLevel()
Click the code to edit it

The history#

The last 1000 entries are kept. log.history() returns them oldest first, each with time (unix milliseconds), level and message. log.countByLevel() counts them by level, and levels with no entries are absent.

main.osl
import "std:log"

log.info("one")
log.warn("two")
log.warn("three")
log.error("four")

log log.history().len
for h in log.history() ( log h.level h.message )
log log.countByLevel()

log.clearHistory()
log log.history().len
log.disableHistory()
log.info("not recorded")
log log.history().len
Click the code to edit it

exportHistory(path) writes the entries to a file, one per line as [HH:MM:SS] [LEVEL] message. It returns a result, because the file may not be writable.

main.osl
import "std:log"
import "std:fs"

log.info("started")
log.error("failed once")
log.exportHistory("run.log").unwrap()
for line in fs.lines("run.log").unwrap() (
  log line.len > 0 line.endsWith("started") or line.endsWith("failed once")
)
log log.exportHistory("no/such/dir/run.log").unwrapErr().kind
Click the code to edit it

Progress, spinners and countdowns#

log.progress(current, total, options) draws a bar on the current line and redraws it in place on every call, ending the line when current reaches total. Options are width and label. log.spinner(message, done) animates in the same way. log.countdown(from, message) counts down one number per second.

main.osl
import "std:log"
import "std:thread"

for i in 0 to 20 (
  log.progress(i, 20, {label: "Copying", width: 30})
  thread.sleep(100)
)
for i in 1 to 10 (
  log.spinner("Waiting", false)
  thread.sleep(100)
)
log.spinner("Waiting", true)
log.countdown(3, "Launching!")
Click the code to edit it

These write control characters that move the cursor, so they are meant for a terminal and not for log files. For richer building blocks (bars as strings, tables, colours), see tui.

Buffering busy output#

A program that prints many lines makes many small writes. log.setBuffer(ms) batches output, writing it at most that many milliseconds later or once 64 KB is waiting. log.flush() writes whatever is held back. setBuffer(0) turns buffering off again.

main.osl
import "std:log"

log.setBuffer(50)
for i in 1 to 3 ( log.plain("line %d", i) )
log.flush()
log.setBuffer(0)
Click the code to edit it

Stopping on fatal errors#

log.fatal(message) logs at ERROR level with a [FATAL] prefix and exits with status 1 at once. Deferred code doesn't run and threads stop. Use it for errors no caller could recover from, and prefer returning a result or throwing otherwise.

main.osl
import "std:log"

log.plain("starting")
log.fatal("configuration is missing")
log.plain("never printed")
Click the code to edit it
Quick check

With the default level, what happens to log.debug("x")?

Your turn

A tidy summary

Print a group named Totals that holds the plain lines apples: 3 and pears: 5, then the JSON line of both.

main.osl
Ctrl Enter runs
Expected output
--- Totals ---
apples: 3
pears: 5
--- End Group ---

{"apples":3,"pears":5}

API reference#

35 members, generated from the std:log source of OSL 9.3.0.

Functions

#log.assert(boolean condition, any ...message)

assert logs message at ERROR level ("Assertion failed" without one) when condition is false, and does nothing otherwise. It never throws; use std:testing to stop on a failed check.

#log.clear()

clear clears the terminal screen.

#log.clearHistory()

clearHistory removes every recorded entry.

#log.countByLevel() string[int]

countByLevel returns how many recorded entries each level has, keyed by level name; levels with no entries are absent.

#log.countdown(int from, any message)

countdown counts down from from to 1 on one line, one number per second, then prints message followed by Done!. Blocks for from seconds. Throws ValueError when from is negative.

#log.debug(any message, any ...args)

debug prints message at DEBUG level, formatted like log.info. DEBUG is below the default level, so call log.setLevel("DEBUG") to see it.

#log.disableHistory()

disableHistory clears the history and stops recording entries.

#log.enableHistory()

enableHistory clears the history and starts recording entries again. History is on by default.

#log.error(any message, any ...args)

error prints message at ERROR level, formatted like log.info.

#log.exportHistory(string path) result<unit, error>

exportHistory writes the recorded entries to the file at path, one per line as "[HH:MM:SS] [LEVEL] message", replacing the file. Fails with NotFoundError, PermissionError or IOError when the file can't be written.

#log.fatal(any message)

fatal logs message at ERROR level, prefixed with [FATAL], and exits the program with status 1 at once: deferred code doesn't run and running threads stop. Throw an error instead when callers should be able to recover.

#log.flush()

flush writes any output that setBuffer is holding back.

#log.getHistory() object[]

Deprecated. Deprecated: use log.history(), whose entries have a unix-ms time, instead.

getHistory returns the recorded entries as objects with timestamp (HH:MM:SS text), level and message keys.

#log.getLevel() string

Deprecated. Deprecated: use log.level() instead.

getLevel returns the current level's name.

#log.group(any title)

group prints a heading that starts a group of related output.

#log.groupEnd()

groupEnd prints the line that ends a group started with log.group.

#log.history() {level: string, message: string, time: number}[]

history returns the recorded entries, oldest first: at most the most recent 1000 at or above the level in force when they were logged. Each entry has keys time (unix ms), level and message (formatted, without the time or level).

#log.info(any message, any ...args)

info prints message at INFO level with the time, applying printf-style verbs (%s, %d, %f, ...) to args; leftover args are appended with spaces.

#log.json(any data)

json prints data as JSON on one line.

#log.level() string

level returns the current level's name, such as "INFO".

#log.log(string level, any message, any ...args)

log prints message at the named level, formatted like log.info. Throws ValueError for an unknown level name.

#log.plain(any message, any ...args)

plain prints message without a level, time or colour, formatted like log.info. It ignores the level and isn't recorded in the history.

#log.progress(number current, number total, object? options)

progress draws a progress bar for current out of total on the current line, redrawing in place on each call and ending the line once current reaches total. Options: width (characters, default 30) and label (text before the bar, default ""). Throws ValueError when total isn't above 0 or width is negative.

#log.progressBar(any current, any total, any ...rest)

Deprecated. Deprecated: use log.progress(current, total, {width, label}) instead.

progressBar draws a progress bar; it takes current, total, width and label and draws nothing when total is 0.

#log.separator()

separator prints a line of 80 dashes.

#log.setBuffer(int milliseconds)

setBuffer batches output from log and print, writing it at most milliseconds later or once 64 KB is waiting, so a program that writes many lines makes few system calls. Zero or less writes every line straight through again, which is the default.

#log.setLevel(string level)

setLevel sets the least severe level that is printed and recorded: "DEBUG", "INFO" (the default), "SUCCESS", "WARN" or "ERROR". Throws ValueError for any other name.

#log.spinner(any message, boolean done)

spinner prints the current frame of a spinner before message on the current line; call it repeatedly to animate it, and with done true to replace it with a check mark and end the line.

#log.success(any message, any ...args)

success prints message at SUCCESS level, which ranks with INFO, formatted like log.info.

#log.table(array headers, array rows)

table prints rows (arrays of cells) under headers as a text table.

#log.time(any message)

time prints message prefixed with the current time of day to the millisecond, such as [14:03:07.123].

#log.timestamp(any message)

timestamp prints message prefixed with the current time in unix microseconds, such as [1760000000123456].

#log.trace(any message)

trace prints message at DEBUG level, prefixed with [TRACE].

#log.warn(any message, any ...args)

warn prints message at WARN level, formatted like log.info.

#log.withTimestamp(string level, any message, any ...args)

withTimestamp prints message at the named level, prefixed with the date and time. Throws ValueError for an unknown level name.