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
#Related pages
- Strings and interpolation for the
{...}templates log messages are built from. - Errors and faults for
error, faults, and howseverefits alongside them. - Conditional compilation for the other compile-time switch, choosing whole branches of code by build flag.