logs
The logs namespace — 30 functions.
globals/logs/clear
logs.clear() -> boolean
Drop all buffered log entries. Lifetime per-level counts
(logs.count) are preserved.
Returns boolean — True on success.
logs.clear()
globals/logs/count
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
optsLogQueryOpts(optional) — Filter options, aslogs.querytakes.
Returns LogCounts — Counts summary table.
print("errors:", logs.count().errors)
local before = logs.count().last_seq
globals/logs/errors
logs.errors(limit: number?) -> { LogEntry }
Most-recent ERROR-level entries (newest first). limit
defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of ERROR log-entry tables.
for _, e in ipairs(logs.errors(20)) do print(e.message) end
globals/logs/find
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
textstring— Substring to search for.limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of matching log-entry tables in chronological order.
local hits = logs.find("MY_MARKER")
globals/logs/query
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
optsLogQueryOpts(optional) — Filter options.
Returns { LogEntry } — Array of matching log-entry tables.
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
logs.tail(limit: number?) -> { LogEntry }
Most-recent entries of any level in chronological order.
limit defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of the most recent log-entry tables.
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
globals/logs/template
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
messagestring— Log message to normalize.
Returns string — The message template.
local key = logs.template(entry.message)
globals/logs/warnings
logs.warnings(limit: number?) -> { LogEntry }
Most-recent WARN+ entries (newest first). limit defaults
to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of WARN+ log-entry tables.
print(#logs.warnings(), "warnings")
modules/logs/README
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
clear(): boolean
Drop all buffered log entries. Lifetime per-level counts
(logs.count) are preserved.
logs.clear()
modules/logs/count
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
optsLogQueryOpts?(optional) — Filter options, aslogs.querytakes.
print("errors:", logs.count().errors)
local before = logs.count().last_seq
modules/logs/errors
errors(limit: number?): { LogEntry }
Most-recent ERROR-level entries (newest first). limit
defaults to 100.
Parameters
limitnumber?(optional) — Maximum entries to return.
for _, e in ipairs(logs.errors(20)) do print(e.message) end
modules/logs/find
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
textstring— Substring to search for.limitnumber?(optional) — Maximum entries to return.
local hits = logs.find("MY_MARKER")
modules/logs/query
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
optsLogQueryOpts?(optional) — Filter options.
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
tail(limit: number?): { LogEntry }
Most-recent entries of any level in chronological order.
limit defaults to 100.
Parameters
limitnumber?(optional) — Maximum entries to return.
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
modules/logs/template
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
messagestring— Log message to normalize.
local key = logs.template(entry.message)
modules/logs/warnings
warnings(limit: number?): { LogEntry }
Most-recent WARN+ entries (newest first). limit defaults
to 100.
Parameters
limitnumber?(optional) — Maximum entries to return.
print(#logs.warnings(), "warnings")
tools/logs/clear
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
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
{ level = "error", limit = 10 }
{ within = 120 }
"shader"
tools/logs/search
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
{ 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
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
{ level = "error" }
{ within = 300 }
tools/logs/tail
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
{ limit = 40 }
{ type = "SCRIPT" }
"shader"
typed/builtin//modules/api/engine/logs/logs/clear
logs.clear() -> boolean
Drop all buffered log entries. Lifetime per-level counts
(logs.count) are preserved.
Returns boolean — True on success.
logs.clear()
typed/builtin//modules/api/engine/logs/logs/count
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
optsLogQueryOpts(optional) — Filter options, aslogs.querytakes.
Returns LogCounts — Counts summary table.
print("errors:", logs.count().errors)
local before = logs.count().last_seq
typed/builtin//modules/api/engine/logs/logs/errors
logs.errors(limit: number?) -> { LogEntry }
Most-recent ERROR-level entries (newest first). limit
defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of ERROR log-entry tables.
for _, e in ipairs(logs.errors(20)) do print(e.message) end
typed/builtin//modules/api/engine/logs/logs/find
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
textstring— Substring to search for.limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of matching log-entry tables in chronological order.
local hits = logs.find("MY_MARKER")
typed/builtin//modules/api/engine/logs/logs/query
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
optsLogQueryOpts(optional) — Filter options.
Returns { LogEntry } — Array of matching log-entry tables.
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
logs.tail(limit: number?) -> { LogEntry }
Most-recent entries of any level in chronological order.
limit defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of the most recent log-entry tables.
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
typed/builtin//modules/api/engine/logs/logs/template
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
messagestring— Log message to normalize.
Returns string — The message template.
local key = logs.template(entry.message)
typed/builtin//modules/api/engine/logs/logs/warnings
logs.warnings(limit: number?) -> { LogEntry }
Most-recent WARN+ entries (newest first). limit defaults
to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of WARN+ log-entry tables.
print(#logs.warnings(), "warnings")