# log

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

This is the Markdown version of https://osl-lang.org/packages/log. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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.

```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")
```

Its output changes from run to run.

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.

```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" )
```

Its output changes from run to run.

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.

```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 )
```

Its output changes from run to run.

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.

```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()
```

Output:

```text

--- Report ---
total: 42 items
{"items":[1,2],"ok":true}
+------+-----+
| name | qty |
+------+-----+
| bolt | 12  |
| nut  | 30  |
+------+-----+
--- End Group ---

--------------------------------------------------------------------------------
```

## 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.

```osl
import "std:log"

log.time("request received")
log.timestamp("tick")
log.withTimestamp("INFO", "something happened")
```

Its output changes from run to run.

## 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](/packages/testing.md) when a failure must be fatal.

```osl
import "std:log"

log.assert(1 + 1 == 2, "arithmetic works")
log.assert(1 + 1 == 3, "arithmetic is broken")
log log.countByLevel()
```

Its output changes from run to run.

## 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.

```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
```

Its output changes from run to run.

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

```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
```

Its output changes from run to run.

## 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.

```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!")
```

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](/packages/tui.md).

## 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.

```osl
import "std:log"

log.setBuffer(50)
for i in 1 to 3 ( log.plain("line %d", i) )
log.flush()
log.setBuffer(0)
```

Output:

```text
line 1
line 2
line 3
```

## 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.

```osl
import "std:log"

log.plain("starting")
log.fatal("configuration is missing")
log.plain("never printed")
```

Its output changes from run to run.

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

- [ ] It is printed in grey.
- [x] It is dropped.
- [ ] It throws.

Answer: DEBUG is below INFO. Call `log.setLevel("DEBUG")` to see it.

### Exercise: A tidy summary

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

Starting code:

```osl
import "std:log"

log.plain("nothing yet")
```

It should print:

```text

--- Totals ---
apples: 3
pears: 5
--- End Group ---

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

Hint 1: `log.group`, `log.plain` and `log.groupEnd`

Hint 2: `log.json({apples: 3, pears: 5})`

A solution:

```osl
import "std:log"

log.group("Totals")
log.plain("apples: %d", 3)
log.plain("pears: %d", 5)
log.groupEnd()
log.json({apples: 3, pears: 5})
```

## API reference

## API reference

Every member of `std:log`, generated from the source of OSL.

### 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.
