---
title: "logs"
description: "The logs namespace — the engine's Luau API reference for logs."
section: "API Reference"
slug: "api-logs"
canonical: "https://origozero.ai/docs/api-logs"
updated: "2026-09-05T23:13:46.670865491+00:00"
tags: ["api", "reference"]
---

# logs

The `logs` namespace — 30 functions.

## globals/logs/clear {#globals-logs-clear}

```lua
logs.clear() -> boolean
```

Drop all buffered log entries. Lifetime per-level counts
(`logs.count`) are preserved.

**Returns** `boolean` — True on success.

```lua
logs.clear()
```

## globals/logs/count {#globals-logs-count}

```lua
logs.count(opts: LogQueryOpts?) -> LogCounts
```

Aggregate counters for the log ring. Lifetime counts survive
eviction, so `errors` reflects the total seen even if the lines
have scrolled out of the buffer. `opts` takes the same filter table
as `logs.query`, and `matched` is how many held entries it selects,
counted without materialising them — `limit` and `newest_first` bound
and order what a query RETURNS, so they leave `matched` alone. `mcp`
is how many held entries record your own tool traffic; a query leaves
those out, so with no `opts`, `matched` + `mcp` is everything held.
`last_seq` is the cursor for
incremental polling: read it before an action, then pass it as
`logs.query({ since = <that> })` afterwards to see only what the
action logged.

**Parameters**

- `opts` `LogQueryOpts` _(optional)_ — Filter options, as `logs.query` takes.

**Returns** `LogCounts` — Counts summary table.

```lua
print("errors:", logs.count().errors)
local before = logs.count().last_seq
```

## globals/logs/errors {#globals-logs-errors}

```lua
logs.errors(limit: number?) -> { LogEntry }
```

Most-recent ERROR-level entries (newest first). `limit`
defaults to 100.

**Parameters**

- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of ERROR log-entry tables.

```lua
for _, e in ipairs(logs.errors(20)) do print(e.message) end
```

## globals/logs/find {#globals-logs-find}

```lua
logs.find(text: string, limit: number?) -> { LogEntry }
```

Case-insensitive substring search over log messages. `limit`
defaults to 200 (keeps the most recent matches). Searches what the
engine logged, so looking for a marker cannot return the call that
looked for it; `logs.query({ contains = ..., include_mcp = true })`
searches your own tool traffic too.

**Parameters**

- `text` `string` — Substring to search for.
- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of matching log-entry tables in chronological order.

```lua
local hits = logs.find("MY_MARKER")
```

## globals/logs/query {#globals-logs-query}

```lua
logs.query(opts: LogQueryOpts?) -> { LogEntry }
```

Query the engine's in-memory log ring — the filtered view of what
also reads as plain text at `/zero/runtime/logs/engine`. Answers about what the
engine logged: the MCP record of your own tool traffic is left out,
because the call carrying the query is one of those records and an
unqualified search would match itself. `type = "MCP"` selects them;
`include_mcp = true` mixes them in with everything else. On a world
several sessions share, `origin = "local"` narrows the answer to the
lines this session's own authoring caused.

**Parameters**

- `opts` `LogQueryOpts` _(optional)_ — Filter options.

**Returns** `{ LogEntry }` — Array of matching log-entry tables.

```lua
logs.query({ entity = "guard-1", limit = 20 })
logs.query({ level = "error", context = 3 })
for _, e in ipairs(logs.query({ level = "warn", limit = 50 })) do print(e.message) end
```

## globals/logs/tail {#globals-logs-tail}

```lua
logs.tail(limit: number?) -> { LogEntry }
```

Most-recent entries of any level in chronological order.
`limit` defaults to 100.

**Parameters**

- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of the most recent log-entry tables.

```lua
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
```

## globals/logs/template {#globals-logs-template}

```lua
logs.template(message: string) -> string
```

Normalize a message to its template — the same line with the parts
that vary between occurrences (numbers, hashes, entity ids) masked out.
Two messages that differ only in those parts share a template, which is
what turns "this error repeated 400 times" into one row instead of 400.
The engine keys its own error retention by the same normalization, so
grouping built on this agrees with what survives ring eviction.

**Parameters**

- `message` `string` — Log message to normalize.

**Returns** `string` — The message template.

```lua
local key = logs.template(entry.message)
```

## globals/logs/warnings {#globals-logs-warnings}

```lua
logs.warnings(limit: number?) -> { LogEntry }
```

Most-recent WARN+ entries (newest first). `limit` defaults
to 100.

**Parameters**

- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of WARN+ log-entry tables.

```lua
print(#logs.warnings(), "warnings")
```

## modules/logs/README {#modules-logs-readme}

```lua
require("@builtin/modules/api/engine/logs") -- logs (also available as global 'logs')
```

Read-only query surface over the engine's in-memory log ring buffer. Public Luau surface over the `__logs` Internal FFI namespace.

Usage: local logs = require("@builtin/modules/api/engine/logs")
Also available as global: logs

## modules/logs/clear {#modules-logs-clear}

```lua
clear(): boolean
```

Drop all buffered log entries. Lifetime per-level counts
(`logs.count`) are preserved.

```lua
logs.clear()
```

## modules/logs/count {#modules-logs-count}

```lua
count(opts: LogQueryOpts?): LogCounts
```

Aggregate counters for the log ring. Lifetime counts survive
eviction, so `errors` reflects the total seen even if the lines
have scrolled out of the buffer. `opts` takes the same filter table
as `logs.query`, and `matched` is how many held entries it selects,
counted without materialising them — `limit` and `newest_first` bound
and order what a query RETURNS, so they leave `matched` alone. `mcp`
is how many held entries record your own tool traffic; a query leaves
those out, so with no `opts`, `matched` + `mcp` is everything held.
`last_seq` is the cursor for
incremental polling: read it before an action, then pass it as
`logs.query({ since = <that> })` afterwards to see only what the
action logged.

**Parameters**

- `opts` `LogQueryOpts?` _(optional)_ — Filter options, as `logs.query` takes.

```lua
print("errors:", logs.count().errors)
local before = logs.count().last_seq
```

## modules/logs/errors {#modules-logs-errors}

```lua
errors(limit: number?): { LogEntry }
```

Most-recent ERROR-level entries (newest first). `limit`
defaults to 100.

**Parameters**

- `limit` `number?` _(optional)_ — Maximum entries to return.

```lua
for _, e in ipairs(logs.errors(20)) do print(e.message) end
```

## modules/logs/find {#modules-logs-find}

```lua
find(text: string, limit: number?): { LogEntry }
```

Case-insensitive substring search over log messages. `limit`
defaults to 200 (keeps the most recent matches). Searches what the
engine logged, so looking for a marker cannot return the call that
looked for it; `logs.query({ contains = ..., include_mcp = true })`
searches your own tool traffic too.

**Parameters**

- `text` `string` — Substring to search for.
- `limit` `number?` _(optional)_ — Maximum entries to return.

```lua
local hits = logs.find("MY_MARKER")
```

## modules/logs/query {#modules-logs-query}

```lua
query(opts: LogQueryOpts?): { LogEntry }
```

Query the engine's in-memory log ring — the filtered view of what
also reads as plain text at `/zero/runtime/logs/engine`. Answers about what the
engine logged: the MCP record of your own tool traffic is left out,
because the call carrying the query is one of those records and an
unqualified search would match itself. `type = "MCP"` selects them;
`include_mcp = true` mixes them in with everything else. On a world
several sessions share, `origin = "local"` narrows the answer to the
lines this session's own authoring caused.

**Parameters**

- `opts` `LogQueryOpts?` _(optional)_ — Filter options.

```lua
logs.query({ entity = "guard-1", limit = 20 })
logs.query({ level = "error", context = 3 })
for _, e in ipairs(logs.query({ level = "warn", limit = 50 })) do print(e.message) end
```

## modules/logs/tail {#modules-logs-tail}

```lua
tail(limit: number?): { LogEntry }
```

Most-recent entries of any level in chronological order.
`limit` defaults to 100.

**Parameters**

- `limit` `number?` _(optional)_ — Maximum entries to return.

```lua
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
```

## modules/logs/template {#modules-logs-template}

```lua
template(message: string): string
```

Normalize a message to its template — the same line with the parts
that vary between occurrences (numbers, hashes, entity ids) masked out.
Two messages that differ only in those parts share a template, which is
what turns "this error repeated 400 times" into one row instead of 400.
The engine keys its own error retention by the same normalization, so
grouping built on this agrees with what survives ring eviction.

**Parameters**

- `message` `string` — Log message to normalize.

```lua
local key = logs.template(entry.message)
```

## modules/logs/warnings {#modules-logs-warnings}

```lua
warnings(limit: number?): { LogEntry }
```

Most-recent WARN+ entries (newest first). `limit` defaults
to 100.

**Parameters**

- `limit` `number?` _(optional)_ — Maximum entries to return.

```lua
print(#logs.warnings(), "warnings")
```

## tools/logs/clear {#tools-logs-clear}

```lua
logs.clear() -> ClearResult
```

Drop the buffered log entries, so a following search sees only what happened after the clear. Lifetime per-level counts are preserved, so "were there ever any errors" stays answerable. Rarely the right move: `summary` returns a `cursor` which, passed back to `search` as `since`, isolates what an action logged without discarding lines anybody else may want.

**Returns** `ClearResult`

## tools/logs/errors {#tools-logs-errors}

```lua
logs.errors(opts?: (string | ErrorsOpts)) -> ErrorsPage
```

Recent errors and warnings, newest first. A snapshot — the same lines however many times it is called. That is the difference from the `problems` tool in the `debug` toolbox, which drains: it advances a read-cursor and reports only what has not been seen, so that one answers "what is new" while this answers "what is there". Accepts the same filters as `search`.

**Parameters**

- `opts` `(string | ErrorsOpts)` _(optional)_

**Returns** `ErrorsPage`

```lua
{ level = "error", limit = 10 }
{ within = 120 }
"shader"
```

## tools/logs/search {#tools-logs-search}

```lua
logs.search(opts?: (string | SearchQuery)) -> SearchResult
```

Search what the engine logged. Filter by severity, subsystem, substring or regex, the script or entity that logged it, and a time window — every filter is optional and they narrow together. This is the general view over the same ring the `problems` tool in the `debug` toolbox drains: reach for it when you need a specific message, a specific entity, a specific minute, or the lines that led up to a failure. The same ring reads as plain text at `/zero/runtime/logs/engine`, which is the shorter reach for a string you can already name; this is the view that filters it.

**Parameters**

- `opts` `(string | SearchQuery)` _(optional)_

**Returns** `SearchResult`

```lua
{ level = "error", within = 60 }
{ entity = "ent_9f3c", limit = 20 }
{ level = "error", context = 5 }
{ level = "warn", group = true }
{ contains = "shader", type = "RENDERER" }
"shader"
```

## tools/logs/summary {#tools-logs-summary}

```lua
logs.summary(opts?: (string | SummaryOpts)) -> SummaryReport
```

How many lines the engine logged, of what severity, from which subsystems and entities, and which messages repeat — the shape of the noise before you go looking inside it. Also the cheapest "did anything go wrong" check, and the source of the cursor for incremental polling: read `cursor` before an action, pass it back to `search` as `since` afterwards, and you see only what that action logged. Accepts the same filters as `search`, so it can describe a slice as well as the whole.

**Parameters**

- `opts` `(string | SummaryOpts)` _(optional)_

**Returns** `SummaryReport`

```lua
{ level = "error" }
{ within = 300 }
```

## tools/logs/tail {#tools-logs-tail}

```lua
logs.tail(opts?: (string | TailOpts)) -> TailPage
```

The most recent lines the engine logged, any level, oldest first — the last line is the newest. The view for when you do not yet know what you are looking for; once you do, `search` narrows it. Accepts the same filters as `search`.

**Parameters**

- `opts` `(string | TailOpts)` _(optional)_

**Returns** `TailPage`

```lua
{ limit = 40 }
{ type = "SCRIPT" }
"shader"
```

## typed/builtin//modules/api/engine/logs/logs/clear {#typed-builtin-modules-api-engine-logs-logs-clear}

```lua
logs.clear() -> boolean
```

Drop all buffered log entries. Lifetime per-level counts
(`logs.count`) are preserved.

**Returns** `boolean` — True on success.

```lua
logs.clear()
```

## typed/builtin//modules/api/engine/logs/logs/count {#typed-builtin-modules-api-engine-logs-logs-count}

```lua
logs.count(opts: LogQueryOpts?) -> LogCounts
```

Aggregate counters for the log ring. Lifetime counts survive
eviction, so `errors` reflects the total seen even if the lines
have scrolled out of the buffer. `opts` takes the same filter table
as `logs.query`, and `matched` is how many held entries it selects,
counted without materialising them — `limit` and `newest_first` bound
and order what a query RETURNS, so they leave `matched` alone. `mcp`
is how many held entries record your own tool traffic; a query leaves
those out, so with no `opts`, `matched` + `mcp` is everything held.
`last_seq` is the cursor for
incremental polling: read it before an action, then pass it as
`logs.query({ since = <that> })` afterwards to see only what the
action logged.

**Parameters**

- `opts` `LogQueryOpts` _(optional)_ — Filter options, as `logs.query` takes.

**Returns** `LogCounts` — Counts summary table.

```lua
print("errors:", logs.count().errors)
local before = logs.count().last_seq
```

## typed/builtin//modules/api/engine/logs/logs/errors {#typed-builtin-modules-api-engine-logs-logs-errors}

```lua
logs.errors(limit: number?) -> { LogEntry }
```

Most-recent ERROR-level entries (newest first). `limit`
defaults to 100.

**Parameters**

- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of ERROR log-entry tables.

```lua
for _, e in ipairs(logs.errors(20)) do print(e.message) end
```

## typed/builtin//modules/api/engine/logs/logs/find {#typed-builtin-modules-api-engine-logs-logs-find}

```lua
logs.find(text: string, limit: number?) -> { LogEntry }
```

Case-insensitive substring search over log messages. `limit`
defaults to 200 (keeps the most recent matches). Searches what the
engine logged, so looking for a marker cannot return the call that
looked for it; `logs.query({ contains = ..., include_mcp = true })`
searches your own tool traffic too.

**Parameters**

- `text` `string` — Substring to search for.
- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of matching log-entry tables in chronological order.

```lua
local hits = logs.find("MY_MARKER")
```

## typed/builtin//modules/api/engine/logs/logs/query {#typed-builtin-modules-api-engine-logs-logs-query}

```lua
logs.query(opts: LogQueryOpts?) -> { LogEntry }
```

Query the engine's in-memory log ring — the filtered view of what
also reads as plain text at `/zero/runtime/logs/engine`. Answers about what the
engine logged: the MCP record of your own tool traffic is left out,
because the call carrying the query is one of those records and an
unqualified search would match itself. `type = "MCP"` selects them;
`include_mcp = true` mixes them in with everything else. On a world
several sessions share, `origin = "local"` narrows the answer to the
lines this session's own authoring caused.

**Parameters**

- `opts` `LogQueryOpts` _(optional)_ — Filter options.

**Returns** `{ LogEntry }` — Array of matching log-entry tables.

```lua
logs.query({ entity = "guard-1", limit = 20 })
logs.query({ level = "error", context = 3 })
for _, e in ipairs(logs.query({ level = "warn", limit = 50 })) do print(e.message) end
```

## typed/builtin//modules/api/engine/logs/logs/tail {#typed-builtin-modules-api-engine-logs-logs-tail}

```lua
logs.tail(limit: number?) -> { LogEntry }
```

Most-recent entries of any level in chronological order.
`limit` defaults to 100.

**Parameters**

- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of the most recent log-entry tables.

```lua
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
```

## typed/builtin//modules/api/engine/logs/logs/template {#typed-builtin-modules-api-engine-logs-logs-template}

```lua
logs.template(message: string) -> string
```

Normalize a message to its template — the same line with the parts
that vary between occurrences (numbers, hashes, entity ids) masked out.
Two messages that differ only in those parts share a template, which is
what turns "this error repeated 400 times" into one row instead of 400.
The engine keys its own error retention by the same normalization, so
grouping built on this agrees with what survives ring eviction.

**Parameters**

- `message` `string` — Log message to normalize.

**Returns** `string` — The message template.

```lua
local key = logs.template(entry.message)
```

## typed/builtin//modules/api/engine/logs/logs/warnings {#typed-builtin-modules-api-engine-logs-logs-warnings}

```lua
logs.warnings(limit: number?) -> { LogEntry }
```

Most-recent WARN+ entries (newest first). `limit` defaults
to 100.

**Parameters**

- `limit` `number` _(optional)_ — Maximum entries to return.

**Returns** `{ LogEntry }` — Array of WARN+ log-entry tables.

```lua
print(#logs.warnings(), "warnings")
```
