Setting Up Structured Logs for ElevenLabs Voice and Video Agents in a Next.js App

Set up JSON structured logs in a Next.js ElevenLabs agent app with session IDs, browser/server correlation, and debug-friendly events.
Introduction
Structured logging is one of the easiest ways to make a realtime agent debuggable without drowning in console noise. If you are building a voice or video agent in Next.js, you will eventually need to answer questions like:
Which session produced this transcript line?
Did the model latency spike before the user dropped?
Did the voice pipeline fail before or after the avatar connected?
Can I correlate browser events, server events, and provider events for the same conversation?
This post shows a practical setup for emitting structured logs from a Next.js app that fronts an ElevenLabs agent, so you can trace a single realtime conversation end to end. By the end, you should be able to:
define a minimal log schema that survives across browser and server boundaries,
propagate a session/correlation ID through your agent flow,
log agent lifecycle events in a way that is easy to search and ship to a log backend, and
avoid common mistakes that make realtime debugging harder than it needs to be.
Why structured logging matters for realtime agents
With a normal request/response app, a single request ID is often enough. Realtime agents are different: audio streaming, websocket state, model tokens, TTS synthesis, browser playback, and video/avatar rendering all happen on different timelines. If you log only free-form strings, you lose the ability to reliably filter, aggregate, and correlate events.
Structured logs solve that by treating each event as data, not prose. In practice, that means every log line should include a small set of consistent fields:
request_idorsession_idagent_idorconversation_ideventtypetstimestamprelevant metadata such as model, provider, latency, error code, or user-agent
The key is consistency. You do not need a giant schema. You need enough stable fields to reconstruct one conversation and compare many conversations.
Design a log schema that works for both browser and server
For a Next.js app, logs usually come from at least two places:
the server route that creates or initializes the agent session, and
the browser client that connects to the session and streams media/events.
These logs should share the same correlation key. A good pattern is:
generate a
session_idserver-side,include it in the response to the client,
attach it to every subsequent log line on both sides.
For example, a minimal schema might look like this:
That format is intentionally boring. Boring is good. It makes it easy to send logs to Datadog, Loki, Honeycomb, OpenSearch, or just grep them locally during development.
A few practical rules help a lot:
Use machine-readable timestamps in UTC.
Keep event names stable and namespaced, such as
agent.audio.startedoragent.tts.error.Do not log raw audio or long transcripts unless you have a clear retention policy.
Log the provider boundary explicitly when you cross from your app into ElevenLabs or another external service.
Implement a small logger in Next.js
You do not need a heavy logging framework to get started. A thin wrapper around console.log is enough, as long as it always emits JSON and injects shared context.
Then use it in your Next.js route that creates the conversation/session:
On the client, reuse the same ID when you log connection and playback events:
That gives you a single thread to follow across the whole interaction.
Capture the events that actually help you debug
When teams first add logging, they usually log too much of the wrong thing. For realtime agents, the useful events are usually the lifecycle boundaries and the failures between them.
A good starting set is:
agent.session.requestedagent.session.startedagent.websocket.connectedagent.audio.input_receivedagent.llm.response_startedagent.tts.requestedagent.audio.output_startedagent.avatar.frame_renderedor similar if you have a video faceagent.erroragent.session.ended
For each of those, keep the payload small and action-oriented. Include measurements when they explain behavior:
That is enough to answer questions like “Did synthesis get slow?” without turning your logs into a transcript store.
Two common gotchas:
Logging only errors. If you do not emit start/end events, you cannot tell where the pipeline stalled.
Using different IDs in different layers. If the browser has one ID and the server has another, correlation falls apart quickly.
Handling Next.js specifics: route handlers, server actions, and edge cases
In Next.js, keep your logging boundary clear. Route handlers are usually the right place for agent setup, session creation, and provider calls. Client components are the right place for UI events, media state changes, and websocket lifecycle logs.
If you run on serverless infrastructure, remember that console.log is often your first shipping path, but not your final observability strategy. Structured JSON is what makes the transition to a log pipeline painless later.
Also pay attention to async failure paths. A streaming agent can fail after the initial response is already sent. Log those failures where they happen, not just at request entry. For example:
If you are using websocket streams, log both connect and disconnect paths, plus close codes when available. A surprising number of “agent bugs” are actually transport issues.
Where Protoface fits when you want an avatar in the loop
If your ElevenLabs agent needs a synchronized talking face, Protoface gives you a clean place to attach that video layer without changing your core agent architecture. In a LiveKit-based voice stack, the Protoface plugin can drop an avatar into the agent so speech and lip movement stay aligned. That means your logs can stay focused on the agent pipeline while the avatar lifecycle is still observable as part of the same session.
In practice, you would log the same correlation ID at the moment you create the agent session, then pass that through your voice stack and avatar connection. If you are using the LiveKit plugin, the implementation details live in the plugin docs and examples, but the logging idea is the same: one session, one ID, one event stream.
When you need to wire this into your own setup, the docs are the best reference point: docs.protoface.com. If you want a concrete implementation to study, the ElevenLabs agent quickstart is a good companion repository: GitHub quickstart.
A practical debugging workflow
Once structured logs are in place, debugging gets much faster. A typical workflow looks like this:
filter by
session_id,read the event sequence in timestamp order,
find the last successful boundary event,
inspect the next failure or timeout,
compare latency across sessions to spot regressions.
This is especially useful for realtime voice agents because user-visible failure modes are often “soft” rather than hard. The connection may succeed, but playback stalls. The model may respond, but TTS is delayed. The avatar may render, but audio/video drift builds over time. Structured logs make those cases visible.
If you later add a proper tracing system, the same correlation ID and event names can seed spans and metrics. The logging schema you define now should be reusable, not throwaway.
Conclusion
For a Next.js app that fronts an ElevenLabs voice or video agent, structured logs are not optional polish; they are the shortest path to understanding realtime behavior. Start with one JSON logger, one correlation ID, and a handful of lifecycle events. Keep payloads small, log failures where they happen, and make sure the browser and server speak the same event language.
If you want to integrate an avatar layer into the same pipeline, the Protoface docs and quickstarts are the right place to connect the dots. From there, you can extend the same logging pattern across your agent, avatar, and transport boundaries without changing the core approach.
