Log statements are part of Vary's grammar. The compiler parses them, validates field types, checks purity constraints, and generates level-gated bytecode. At runtime, events are dispatched to a configurable sink.
There is nothing to import. You write log info "event_id" "message" and the compiler handles the rest.
A log statement has a level, a stable event ID, and an optional message:
log info "app_started" "Server is running"
log debug "cache_miss" "Key not found"
log error "db_timeout" "Connection timed out"
The short form takes the level, event ID (a string literal), and an optional message expression. The event ID is a stable identifier for this log point. It does not change when you reword the message.
Five levels are available, from lowest to highest priority:
| Level | Use |
|---|---|
trace | Detailed diagnostic output |
debug | Internal state useful during development |
info | Normal operational events |
warn | Unexpected but recoverable situations |
error | Failures that need attention |
The runtime filters events below the active level. Setting the level to warn silences trace, debug, and info events.
For richer events, use the block form to attach typed fields:
let name = "alice"
let count = 3
let elapsed = 0.042
log info "user_login" {
message = "User logged in"
field "username" = name
field "attempt" = count
field "latency_ms" = elapsed * 1000.0
}
Field values must be one of four primitive types:
| Type | Example |
|---|---|
Int | field "port" = 8080 |
Float | field "latency" = 3.14 |
Bool | field "cached" = True |
Str | field "user" = name |
The compiler rejects complex types (lists, dicts, objects) at compile time. This keeps log events flat, avoids serialization overhead, and means any sink can write them without custom encoders.
Logger blocks scope a name to all nested log statements:
logger "auth" {
log info "login_attempt" "Checking credentials"
log info "login_success" "Authenticated"
}
The compiler propagates the logger name into each nested log statement. Logger blocks can nest, and names are joined with dots:
logger "app" {
logger "auth" {
log info "check" "Verifying token"
# logger name: app.auth
}
}
Context blocks attach fields that propagate to every log statement inside them:
logger "api" {
context {
field "service" = "orders"
field "version" = "2.1"
}
log info "request_start" "Handling request"
# event includes service="orders" and version="2.1"
}
Context fields are merged into each log event at compile time. If a log statement defines a field with the same name as a context field, the log statement's value takes precedence.
Five sinks are built in:
| Sink | Output | Default |
|---|---|---|
stdout | Plain text, one line per event | Yes |
color | ANSI-colored output for terminals | |
json | One JSON object per line | |
memory | In-memory buffer for testing | |
none | Discards all events |
Switch sinks at runtime:
import log
log.set_sink("json")
log info "test_event" "Testing JSON output"
log.set_sink("color")
log info "test_event" "Testing color output"
Set the minimum level at runtime:
import log
log.set_level("debug") # show debug and above
log debug "test" "This is visible"
log.set_level("warn") # show warn and error only
log debug "test" "This is hidden"
log warn "test" "This is visible"
The default level is info.
For testing, capture log events in memory instead of printing them:
import log
test "login logs username" {
log.set_sink("none")
log.capture()
log info "login" {
message = "User logged in"
field "username" = "alice"
}
let events = log.events()
observe len(events) == 1
}
log.capture() starts collecting events on the current thread. log.events() returns the collected events as a list and clears the buffer. Events are captured regardless of the active sink, so you can set the sink to "none" to suppress output during tests.
Log statements are side effects. The compiler rejects them inside pure functions:
pure def add(a: Int, b: Int) -> Int {
log info "adding" "Computing sum" # compile error
return a + b
}
This is enforced by the effect system. A function that logs cannot be pure.
The compiler treats log statements specially during mutation testing.
Event IDs and message strings are not mutated. Changing "user_login" to "user_logout" would not test anything meaningful about correctness. Field value expressions are mutated normally: if your code logs field "count" = len(items), the mutator may replace len(items) with 0 to see if any test notices.
A library-based logger would be opaque to the mutation engine. Because log is grammar, the compiler can tell the difference between a label and a value.
| Function | Signature | Purpose |
|---|---|---|
capture_logs() | () -> None | Start capturing log events on the current thread |
captured_events() | () -> List | Return captured events and clear the buffer |
log_set_level(level) | (Str) -> None | Set minimum log level ("trace", "debug", "info", "warn", "error") |
log_set_sink(sink) | (Str) -> None | Set active sink ("stdout", "color", "json", "memory", "none") |