How do I trace a TypeScript AI agent running in AWS Lambda?
Rius traces a TypeScript agent in AWS Lambda when you call init() once per process and await client.flush() at the end of every invocation. The TypeScript SDK installs no exit hook, and Lambda freezes the process between invocations, so the explicit flush is how your spans leave. Turn the heartbeat off, because a frozen process cannot send it reliably.
How it works
- One init per process.
init()registers the global OpenTelemetry provider, which is write-once. A second globalinit()logs a warning and returns the existing client, so keep the call at module scope rather than inside the handler. - Spans are batched in the background. The SDK queues spans in memory and exports them on a timer. Lambda freezes the process between invocations, so a frozen timer exports nothing and nothing flushes for you at exit.
flush()reports delivery. It forces an export and resolvesfalsewhen the most recent export failed, for example a 401 from a bad API key. Failed batches are dropped and never buffered for later, so check the return value rather than assuming spans left.- Auto-instrumentation attaches asynchronously.
init()is synchronous, but the integrations attach a moment later. Awaitclient.readybefore your first span if you need them in place. It resolves with the names that attached and never rejects. - The heartbeat is for long-lived processes. It pings Rius for the life of the process to power the Agents view. In a frozen runtime the pings stop and the instance flaps between alive and
staleorgone.
Flush at the invocation boundary only. Calling flush() inside hot paths blocks until the export completes.
Set it up
-
Install the SDK and its peer dependency into the function’s bundle. Node 18 or newer is required. The full option list is in Installation and configuration.
npm install @glassflow-ai/rius @opentelemetry/api -
Set
RIUS_API_KEYin the function’s environment variables, so no key sits in your code. The SDK reads it automatically. SetRIUS_SERVICE_NAMEtoo if you prefer to keep the name out of code. -
Initialize at module scope, wrap your agent in a named function, and flush in a
finallyblock so the flush runs on every invocation, whether the handler returns or throws. Name the wrapped function, because an anonymous one traces as anonymous. Your own model calls go insiderunAgent.import { init, observe } from '@glassflow-ai/rius' // Module scope: init() runs once per process, not once per invocation. const client = init({ serviceName: 'my-agent', heartbeat: false, // a frozen process cannot keep a heartbeat alive }) const runAgent = observe(async function runAgent(query: string) { // your model calls and tool calls go here return 'answer' }) export const handler = async (event: { query: string }) => { await client.ready // resolves at once after the first invocation try { return await runAgent(event.query) } finally { const delivered = await client.flush() if (!delivered) console.warn('the most recent span export failed') } } -
Install the instrumentation for the libraries you call, such as
@arizeai/openinference-instrumentation-openaifor OpenAI, so model calls arrive as spans with tokens and cost.init()enables whatever it finds. The Integrations page lists each library. -
If you bundle with pnpm and the function is deployed through file tracing, the
openaioranthropicintegration can stay off while everything else looks fine. In that caseclient.readyleaves it out and no model-call spans appear. Pass the modules you import yourself throughinstrumentModules, as shown under pnpm and serverless deployments on the Integrations page. -
Invoke the function and open the trace in the Rius console. If spans are missing, log what
flush()returns and whatclient.readyresolved to.
What you get
- One trace per invocation, with the span for each model call, tool call and agent step, plus tokens and a list-price cost estimate in US dollars.
- A delivery signal on every invocation, because
flush()returnsfalsewhen the last export failed. - No stray heartbeat pings from a frozen process, so the Agents view does not show your function flapping between states. The agent is marked no heartbeat on the Agents page and has no Instances section, while traces still group under the service name.
- Settings that suit short invocations. Lower
OTEL_BSP_SCHEDULE_DELAY(default 5000 ms) if you tune for latency-sensitive shutdowns, and raiseOTEL_BSP_MAX_QUEUE_SIZE(default 2048) for chatty agents before you lower the sample rate.
The export pipeline, retries and what gets dropped are covered in Reliability.
When something else fits
- If your agent runs as a long-lived server, you do not need a flush per request. Call
await client.shutdown()on graceful shutdown instead, and leave the heartbeat on. - If you need to know that work is in progress or that a run crashed, enable
partialSpans, which sends a content-free snapshot of each span when it starts. This is described in Reliability.