Skip to Content
Rius

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 global init() 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 resolves false when 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. Await client.ready before 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 stale or gone.

Flush at the invocation boundary only. Calling flush() inside hot paths blocks until the export completes.

Set it up

  1. 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
  2. Set RIUS_API_KEY in the function’s environment variables, so no key sits in your code. The SDK reads it automatically. Set RIUS_SERVICE_NAME too if you prefer to keep the name out of code.

  3. Initialize at module scope, wrap your agent in a named function, and flush in a finally block 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 inside runAgent.

    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') } }
  4. Install the instrumentation for the libraries you call, such as @arizeai/openinference-instrumentation-openai for OpenAI, so model calls arrive as spans with tokens and cost. init() enables whatever it finds. The Integrations page lists each library.

  5. If you bundle with pnpm and the function is deployed through file tracing, the openai or anthropic integration can stay off while everything else looks fine. In that case client.ready leaves it out and no model-call spans appear. Pass the modules you import yourself through instrumentModules, as shown under pnpm and serverless deployments on the Integrations page.

  6. Invoke the function and open the trace in the Rius console. If spans are missing, log what flush() returns and what client.ready resolved 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() returns false when 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 raise OTEL_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.

Still stuck? Get help

Last updated on