All articles

Understanding a voice session with Parlora 0.2.0

Follow the work behind a voice conversation with metrics, traces, and useful logs, using the monitoring tools your application chooses.

A voice assistant pauses before answering. Was it waiting for your backend? Was audio sitting in a queue? Did the connection need to recover? To the person on the call, each can sound like the same delay.

Listening to test calls matters, but it leaves you guessing about what happened inside the application. A transcript tells you what was said. It tells you much less about the work running alongside the conversation or how audio moved through the system.

Parlora 0.2.0 adds an observability contract built on Elixir’s :telemetry. It gives your application measurements and events for following a session, investigating delays, and connecting what happened in one call to patterns across many calls.

If you are new to Parlora, it is an Elixir framework for building voice AI agents with full-duplex speech-to-speech models. It coordinates the voice provider, audio channel, application policy, and backend work. The introduction explains how those pieces fit together.

Following the work through a session

A useful starting point is to separate the stages that can otherwise disappear into “the assistant was slow.” A request can wait before the backend starts, spend time executing, and then wait for its result to be submitted to the voice provider.

For example, your service might return quickly while its result waits for the conversation to reach a suitable point. Looking only at service latency would miss that wait. Looking only at the length of the call would hide both.

Parlora exposes separate operation events for backend queue time, observed execution, and result delivery. There are also events for session startup and shutdown, policy evaluation, interruptions, and connection recovery. Each operation has a start and a stop, with timing measurements and an outcome.

Those boundaries matter. Backend execution starts when Parlora observes the backend’s start notification. Delivery ends at the first delivery outcome; acceptance by a provider does not prove that someone heard the result. The measurements help you locate a delay without turning every observation into a claim about the caller’s experience.

Start with one event

You can use the new events without installing an exporter or running a monitoring service. Add Parlora to your application, or update an existing dependency:

{:parlora, "~> 0.2.0"}

Here is a complete handler that logs one summary when a session stops:

defmodule MyApp.ParloraObserver do
  require Logger

  def attach do
    :telemetry.attach(
      "my-app-parlora-session",
      [:parlora, :session, :stop],
      &__MODULE__.handle_event/4,
      nil
    )
  end

  def detach, do: :telemetry.detach("my-app-parlora-session")

  def handle_event(_event, %{duration: duration}, %{outcome: outcome}, _config) do
    duration_ms = System.convert_time_unit(duration, :native, :microsecond) / 1_000
    Logger.info("Parlora session stopped: outcome=#{outcome} duration_ms=#{duration_ms}")
  end
end

Call MyApp.ParloraObserver.attach() once from your application’s startup code, before starting sessions. It observes sessions throughout that VM. Call detach() when removing the integration.

Operation durations use Erlang’s native time units, so the conversion in the handler is important. Audio fields ending in _us already use microseconds. The observability guide lists the events, units, and exact timing boundaries.

Telemetry handlers run synchronously in the process emitting the event. Keep them short. Update a metric or hand work to a bounded buffer; send HTTP requests and perform exports outside the callback.

From one call to a pattern

Metrics help you see whether something is happening more often. You might track startup time, backend queue and execution durations, delivery outcomes, and audio queue observations. Group them by a small set of categories, such as operation and outcome, so a graph remains useful as traffic grows.

Traces help you follow an individual session. Operation events carry an operation_id, and child operations point to the session through parent_operation_id. Backend request operations also carry task and request identifiers. A tracing integration can use these to put the related operations together, even when the work crosses processes.

Keep those identifiers in traces or selected logs rather than using them as metric labels. Giving every session its own metric series makes aggregation harder and increases storage costs.

Parlora emits the observations. Your application chooses the metric reporter, tracing SDK, exporter, sampling, and retention. It does not automatically create OpenTelemetry spans or require a particular service. The development example below shows one way to make that connection.

Investigating an audio gap

Audio needs more than a request timer. A backend can complete on time while playback still pauses, and a healthy connection can carry audio that arrives unevenly.

The built-in voice providers expose the duration of queued audio and the age of the oldest queued frame. The audio pipeline reports scheduling lateness and how long it has been blocked. Together, these observations help you see whether audio is accumulating, draining, or waiting to move.

For browser PCM playback, gap observations also separate time without available source audio from the additional time spent waiting for the recovery buffer. That distinction helps you investigate whether changing playback behavior could help, or whether the application was already waiting for more source audio.

For example, suppose someone hears a pause even though the backend finished quickly. The audio queue drained, and the playback observations show a period without source audio followed by additional recovery buffering. That points the investigation toward audio arrival and playback recovery. You can then compare those observations with the surrounding events to narrow down the cause.

A gap alone is not enough to identify its cause. Silence can be intentional, and a source shortage does not tell you whether the provider, network, or local processing caused it. Compare the observations with what you heard, the surrounding audio, and the other events from that session.

Keeping logs useful

Observability should not turn a voice session into a log line for every audio frame. A session summary can be useful at :info. Selected diagnostics belong at :debug while investigating a problem. Unexpected conditions may deserve :warning, and unrecovered failures may deserve :error. Your application decides which events to log and configures Logger.

The new operational telemetry stream selects correlation fields and known categories without copying transcripts, prompts, tool data, or credentials. Conversation events from Parlora.subscribe/2 are a separate API and can contain application content, so select and redact that content before exporting it.

These events describe how Parlora ran. Whether a booking succeeded, an answer was correct, or a customer was satisfied remains an application concern.

Try it locally

The repository includes an optional development host with VictoriaMetrics, VictoriaTraces, and an OpenTelemetry Collector. It runs an offline Parlora session with deterministic adapters, so you can explore the integration without provider API keys or a microphone.

From a checkout of Parlora 0.2.0, with Docker running and the runtime and build prerequisites installed:

cd examples/observability
docker compose up -d
mix deps.get
mix run demo.exs

Leave the command running after it prints the session ID. Docker exposes the dashboards on your computer. With the example running, open VictoriaMetrics for metrics and VictoriaTraces for traces. These localhost URLs point to your own computer, using the ports configured in the example’s Docker Compose file.

The example guide includes queries to paste into each UI, a workload that also produces audio queue measurements, verification commands, and shutdown instructions.

The workload uses simulated delays and failures so you can see how they appear in your metrics and traces. All of the monitoring dependencies stay in this separate example application.

If you try it in your application, I would like to hear which measurements help you explain a call and which questions are still difficult to answer. You can share feedback on GitHub or find me on X.