Telemetry — observers, receipts, tracers
Every ask emits events and produces a receipt when it finishes. Use telemetry to send them to the terminal, a Nola console or your own observers. Observers receive events without changing the ask.
The terminal
Section titled “The terminal”By default, terminalTrace() prints every event at level debug. Set telemetry: { level } to choose how much detail appears:
| Level | Events shown |
|---|---|
silent |
none |
error |
failed asks |
warn |
errors, validation failures and retries |
info |
the above, plus ask and infer-function start/end |
debug |
the above, plus every provider request and reply |
import { openai } from "@nola-lang/providers";import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({ model: openai({ model: "gpt-5-mini" }), telemetry: { level: "info" },});Terminal output includes the time, event kind, source location, provider and duration, with each attempt indented under its ask. It uses the same format and colours as nola console.
Output goes to stderr, leaving stdout for your program. Colours are enabled on a TTY; NO_COLOR disables them, and terminalTrace({ color }) overrides that choice.
The telemetry list
Section titled “The telemetry list”import { openai } from "@nola-lang/providers";import { defineConfig, terminalTrace } from "@nola-lang/runtime";
export default defineConfig({ model: openai({ model: "gpt-5-mini" }), telemetry: [ "http://localhost:4141", // a local Nola console — a URL is a tracer terminalTrace({ level: "info" }), // keep the terminal — a list implies nothing { name: "timing", onAskEnd: ({ receipt }) => { console.log(`${receipt.site} via ${receipt.servedBy}: ${receipt.durationMs}ms, ${receipt.attempts} attempt(s)`); }, }, ],});- Add
terminalTrace({ level }), anhttp(s)URL,nola.tracer(...)or your own observer object. A URL is shorthand fornola.tracer(url). - A custom observer implements any of the event methods listed below and may have a
name. Its type,NolaTelemetry, is exported from@nola-lang/runtime. - A single observer or URL can be supplied without an array. An observer or list replaces the default terminal output; include
terminalTrace()to keep it. An empty list is silent. - Events are delivered to every entry in list order. If an observer throws, Nola catches the error and warns once for that observer and event method. The ask continues.
Tracers
Section titled “Tracers”A tracer sends events to a Nola-Protocol server, such as the local console started by npx nola-lang console or the platform. Set telemetry to the server’s URL:
import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({ model: "nola", telemetry: "http://127.0.0.1:4141", // the URL `npx nola-lang console` prints});A URL is shorthand for nola.tracer(url). Use nola.tracer(...) from @nola-lang/runtime when you need options:
nola.tracer()usesNOLA_API_URL, falling back tohttps://api.nola.sh.- A URL string or
{ baseUrl, apiKey?, apiKeyEnv?, fetch? }selects another server. Loopback connections need no key; other hosts useNOLA_API_KEYby default. - A string that is not an
http(s)URL producesNOLA3003.
The tracer sends one POST per event without waiting for the response. It redacts content before sending and does not buffer events. If the server is unreachable, it warns once per process and keeps trying on later events. A tracer cannot block or fail an ask.
A single tracer replaces terminal output. To keep both, use ["http://127.0.0.1:4141", terminalTrace()]. Tracing is configured separately from inference: using the platform model does not enable it, and a listed tracer sends events regardless of whether the server stores them.
For development, set NOLA_TRACING_URL to attach a tracer without editing the config. Nola appends it to the telemetry list unless the config already includes a tracer; in that case, it ignores the variable and prints one notice. Unset the variable before the next run to remove it.
Trace envelopes include the app’s project name, taken from config or the nearest package.json name. The console uses it to group runs from different apps. The project name does not affect the ask fingerprint.
When a tracer sends to a non-loopback host, Nola prints a notice naming the host once per process. This makes it clear where prompts and outputs are being sent.
Event payloads
Section titled “Event payloads”| Event | Payload |
|---|---|
onAskStart |
askId, site, provider, invocationId, spanPath (invocation ids from the root frame to the ask site — spanPath[0] identifies the whole invocation, which the console shows as one trace), def (compiler-stamped identity of the authored ask — stable across line shifts, so executions group by definition), instruction, kind (extract or call), callee and hint (call intents — the function name and the authored hint), typeText (extract intents — the target type as TypeScript text, e.g. "quote" | "order" or Ticket) |
onProviderRequest |
askId, attempt, provider, intent (the ask as data — scopes, inputs, task, schema; carries the correction on the retry), params, profile (the ask with <name> name when the platform serves inference) |
onProviderResponse |
askId, attempt, provider, text, durationMs, sent (what a text-rendering provider sent: { system, messages }) |
onValidationFailed |
askId, attempt, error, site — the answer did not match the schema; a correction attempt follows |
onRetry |
askId, attempt, reason, site |
onAskEnd |
askId, receipt (below) |
onInvocationStart |
invocationId, parentInvocationId (absent on a root — a bare await, a plain-TS call, or .detached()), spanPath, fn, file, detached — fires the moment an infer function’s frame opens, for every frame |
onInvocationEnd |
invocationId, parentInvocationId, status (ok or error), durationMs, trace — this frame’s spans (asks and nested invocations); fires for every frame, so filter on a missing parentInvocationId to see each trace once |
The receipt
Section titled “The receipt”AskReceipt, delivered on onAskEnd:
| Field | Meaning |
|---|---|
askId, site |
the ask and its source location (file:line:col in the .tsi) |
originalPrompt |
the default rendering of the ask as first composed |
effectivePrompt |
what the provider sent on the last attempt, when it reports it; else the default rendering — diverges from the original on a correction retry |
schema |
the JSON Schema the answer was validated against |
servedBy |
the provider name that answered |
attempts |
provider round trips actually performed |
outcome |
{ ok: true, value } or { ok: false, error } |
usage |
{ inputTokens?, outputTokens? } — reserved; built-in providers do not currently report token usage |
durationMs |
wall-clock time of the ask |
invocationId, spanPath |
the invocation this ask belongs to, and the chain from the root frame |
fingerprint |
the canonical ask fingerprint — the replay key |
profile |
the inference profile sent with the request (ask with <name> when the platform serves inference); absent otherwise |
def |
the ask’s compiler-stamped source identity — a hash of the authored text (file, instruction, type; never line numbers), so executions of the same ask group together across runs and edits elsewhere in the file |
kind |
extract for an extractor, call for the slot-filling ask of a call intent |
meta |
scratch space for middleware stages; currently always {} because middleware is inactive |
The receipt’s outcome.error is redacted with redactError. Its prompt fields contain the conversation as composed, so any secret passed as a contextual value remains in originalPrompt and effectivePrompt. See Environments and secrets.
Next: Record and replay