SCUA

Manual

Logging

SCUA has logging built in, with levels you can filter at compile time and structured fields you get almost for free. A log statement looks like a function call but isn't quite one: below the build's level floor, the whole statement is removed from the program, arguments and all.

#The five levels

There are five level keywords, from lowest to highest: trace, debug, info, warn, severe. Each takes a message and prints it to standard output as [level] message.

let player = "Mara"
let hp = 30

info(`player {player} joined`)
debug(`hp is {hp}/100`)
warn(`hp low: {hp}`)
severe(`save upload failed, retrying from cache`)

Run it:

$ scua logging.scua
[info] player Mara joined
[debug] hp is 30/100
[warn] hp low: 30
[severe] save upload failed, retrying from cache

The message is a backtick template, so {...} holes interpolate normally.

There's deliberately no error level. error(v) already means something else in SCUA: it raises a fault. See the section below on how severe differs from that.

#Structured fields

A hole that's a bare name, like {player}, does double duty. It interpolates into the printed message, and it also becomes a named structured field attached to the log record. A field named player with the value "Mara", a field named hp with the value 30. A logging backend that consumes these records can then query by field, find every line where player was "Mara", without parsing the message text.

You can also attach explicit fields with a trailing table:

debug(`frame tick`, { frame = 120, dt = 0.016 })
[debug] frame tick

The default standard-output sink prints just the message, so you don't see the fields in the terminal. They travel with the record to any host sink that's listening.

#Filtering by level at compile time

Pass --log=<level> to set the build's floor. Any statement below that level is dropped from the program entirely. Not skipped at runtime, removed: its arguments are never evaluated, so an expensive call inside a stripped log statement costs nothing.

Take this program:

info(`player {player} joined`)
debug(`hp is {hp}/100`)
warn(`hp low: {hp}`)
severe(`save upload failed, retrying from cache`)

With the floor at info, the debug line is gone:

$ scua --log=info logging.scua
[info] player Mara joined
[warn] hp low: 30
[severe] save upload failed, retrying from cache

Raise the floor to warn and both info and debug disappear:

$ scua --log=warn logging.scua
[warn] hp low: 30
[severe] save upload failed, retrying from cache

Because stripping happens at compile time, this is how you keep trace and debug calls in your source for development and pay nothing for them in a shipped build. Treat log arguments as observation only: code that's needed for the program to be correct must not live inside a log call, because a higher floor will delete it.

#Timestamps

--log-time prefixes every line with an ISO-8601 timestamp:

$ scua --log-time logging.scua
2026-06-18T23:31:14.267Z [info] player Mara joined
2026-06-18T23:31:14.267Z [debug] hp is 30/100
2026-06-18T23:31:14.267Z [warn] hp low: 30
2026-06-18T23:31:14.267Z [severe] save upload failed, retrying from cache

It combines with --log; the floor still applies.

#severe is not a fault

This is the distinction to get right. severe(...) logs a serious problem at error severity and then keeps going. It does not raise, does not unwind, does not stop the program. It's for the recoverable failure you still want on an error dashboard: a save that fell back to cache, a retry that eventually worked.

severe(`disk almost full`)
print("kept going after severe")
$ scua severe.scua
[severe] disk almost full
kept going after severe

error(v) is the other thing. It raises a fault, which unwinds and stops unless something catches it. The code after it doesn't run:

print("before")
error("boom")
print("after")
$ scua fault.scua
before
scua: fault.scua:2: boom

after never prints, and the program exits non-zero. So: reach for severe to record a problem and carry on, and for error to actually abort the current work. An uncaught fault is reported to sinks at a higher severity than severe, so the two stay distinguishable downstream. Faults and how to handle them are covered in Errors and faults.

#The level keywords are contextual

trace, debug, info, warn, and severe only act as log statements in statement position. They're still usable as ordinary names elsewhere, so an existing variable called info won't clash:

let info = "just a string"
print(info)
just a string