Debug Logging
I needed a way to see what documents are doing in production without paying for it when nobody's watching. @debug is the answer — a built-in statement that publishes formatted messages from running documents, but only actually does the formatting work when someone is subscribed. Zero overhead otherwise.
The @debug Statement
@debug("simple message with no args");
@debug("player {0} scored {1} points", player_name, score);
The first argument is always a string literal format string. Additional arguments get substituted at positions marked by {N} placeholders.
Format Specifiers
Two forms:
| Specifier | Description | Example |
|---|---|---|
{N} |
Insert argument N as a string (0-indexed) | @debug("value is {0}", x) |
{N|zpK} |
Zero-pad argument N to K digits | @debug("id = {0|zp6}", id) produces id = 000042 |
int score = 42;
string name = "Alice";
@debug("Player {0} has score {1}", name, score);
// Output: "Player Alice has score 42"
@debug("Padded score: {0|zp10}", score);
// Output: "Padded score: 0000000042"
Supported Argument Types
These types work as @debug arguments:
string,int,long,double,boolmessage,dynamic,enum,principal- Vector types:
vec2,vec3,vec4 - Matrix types:
mat2,mat3,mat4,math4
Tables, records, and other complex types are not supported — you'll get a compile-time error. I could have made them work by serializing to JSON, but that would undermine the zero-cost guarantee.
Compile-Time Validation
The format string is validated at compile time. The compiler catches:
- Unclosed braces:
@debug("unclosed {0", x)— error - Out-of-range indices:
@debug("{0} {2}", x)— error (index 2 with only 1 argument) - Unknown format specifiers:
@debug("{0|badspec}", x)— error - Non-convertible types:
@debug("table={0}", my_table)— error
No runtime surprises. If it compiles, the format string is valid.
Debug Policy
You control who can subscribe to debug output by defining a debug policy in the @static block:
@static {
create { return true; }
debug { return @who.isAdamaDeveloper(); }
}
The debug policy:
- Receives the subscriber's identity via
@who - Returns
trueto allow the subscription,falseto deny - Is evaluated at subscription time
- Defaults to
false(deny all) if not defined
Debug output is off by default. You have to explicitly opt in via policy. I'd rather err on the side of silence than accidentally leak internal state.
Performance
This is the part I care most about. @debug is designed for zero overhead when nobody's listening:
- If no debug subscribers are connected, the format string is never evaluated and no allocations occur
- The format string is parsed and validated at compile time, so runtime formatting is efficient
- Arguments are only converted to strings when a subscriber exists
This means you can leave @debug statements in production code without guilt. They cost nothing until someone subscribes.
Usage Patterns
Tracing State Machine Transitions
#processing {
@debug("entering #processing, round={0}, items={1}", round, items.size());
// ... do work ...
transition #waiting;
}
#waiting {
@debug("entering #waiting");
}
Monitoring Channel Activity
channel place_bet(BetMsg msg) {
@debug("bet from {0}: amount={1}", @who, msg.amount);
// ... process bet ...
}
Tracking Agent Behavior
agent Helper {
max_tool_rounds = 5;
@description("Search for information")
tool<SearchInput, SearchOutput> search {
@debug("agent tool search called: query={0}", request.query);
return { result: "..." };
}
}
Debugging Formulas and Computations
procedure recalculate() {
@debug("recalculating: x={0}, y={1}, sum={2}", x, y, x + y);
// ... computation ...
}