Know what your app is doing
The log library writes structured log entries to disk. In production, you debug without print() in response bodies. In development, you see everything.
Logs are written to storage/logs/site_{id}.log.
Function reference
| Function | When it writes | Purpose |
|---|---|---|
log.info(msg) |
Always | Normal operations — logins, orders, page views |
log.error(msg) |
Always | Something broke — payment failed, API error |
log.warn(msg) |
Always | Potential problems — rate limit near, deprecation |
log.debug(msg) |
Only APP_DEBUG=true |
Development details — variable values, SQL queries |
log.should_debug() |
— | Returns true if APP_DEBUG=true |
Log format
Each entry is one JSON line:
[2026-05-25T15:30:00.000+00:00] lua.INFO: User logged in {"site":82468,"method":"GET","path":"/"} []
Basic logging
-- User actions
log.info("User " .. user_id .. " created a post")
-- Errors
log.error("Payment failed: " .. reason .. " for order " .. order_id)
-- Warnings
log.warn("Rate limit at " .. percentage .. "% for IP " .. env.remote_ip)
-- Debug (only in development)
log.debug("Query returned " .. result.total .. " results")
Conditional debug logging
Expensive debug operations should be guarded:
if log.should_debug() then
-- Serializing this large table is expensive — skip in production
log.debug("Full state: " .. encoder.json(large_state_table))
log.debug("Request headers: " .. encoder.json(req.headers))
end
Without the guard, encoder.json(large_table) still runs in production — the CPU cost is paid even though the output is discarded.
Log rotation
Logs auto-rotate at LOG_MAX_BYTES (default 5MB). When exceeded:
- Current log renamed to
site_{id}.log.old - New blank log created
- Old
.oldfile overwritten on next rotation
Set LOG_MAX_BYTES=10485760 for 10MB limit, or LOG_ENABLED=false to disable entirely.
Practical: request logging middleware
app:on("before", function(ctx)
ctx._req_start = os.clock()
end)
app:on("after", function(ctx, result)
local elapsed = math.floor((os.clock() - ctx._req_start) * 1000)
local status = type(result) == "table" and result.status or 200
if status >= 500 then
log.error(string.format("%s %s → %d (%dms)",
req.method, req.path, status, elapsed))
elseif status >= 400 then
log.warn(string.format("%s %s → %d (%dms)",
req.method, req.path, status, elapsed))
else
log.info(string.format("%s %s → %d (%dms)",
req.method, req.path, status, elapsed))
end
end)
Practical: tracking important events
app:post("/orders", function(ctx)
local order = api.dataset.create("orders", {
items = cart.items,
total = cart.total
})
log.info(string.format(
"Order #%d placed: %d items, $%.2f",
order.id, #cart.items, cart.total
))
return ctx:redirect("/orders/" .. order.id)
end)
Configuration reference
| Variable | Default | Description |
|---|---|---|
LOG_ENABLED |
true |
Set false to disable all logging (zero I/O) |
LOG_MAX_BYTES |
5242880 |
Max file size before rotation (5MB) |
APP_DEBUG |
false |
When true, log.debug() writes and print()/dump() output appears in responses |
Next: See the Dataset Advanced Querying guide for advanced data operations.