Beta documentation. This is an early preview — content is still in active development. Feedback helps shape the final release. Share your thoughts or join the discussion.

Logging

The `log` library writes structured log entries to disk. In production, you debug without `print()` in response bodies. In development, you see everything.

On this page

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:

  1. Current log renamed to site_{id}.log.old
  2. New blank log created
  3. Old .old file 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.

Previous String Utilities