Skip to content

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.

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
nola.config.ts
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.

nola.config.ts
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 }), an http(s) URL, nola.tracer(...) or your own observer object. A URL is shorthand for nola.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.

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:

nola.config.ts
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() uses NOLA_API_URL, falling back to https://api.nola.sh.
  • A URL string or { baseUrl, apiKey?, apiKeyEnv?, fetch? } selects another server. Loopback connections need no key; other hosts use NOLA_API_KEY by default.
  • A string that is not an http(s) URL produces NOLA3003.

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 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

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