<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jay Prajapati</title>
    <description>The latest articles on DEV Community by Jay Prajapati (@jay9122).</description>
    <link>https://dev.to/jay9122</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1093703%2Fc855075e-bf58-4b57-8764-85d9e17065da.jpg</url>
      <title>DEV Community: Jay Prajapati</title>
      <link>https://dev.to/jay9122</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jay9122"/>
    <language>en</language>
    <item>
      <title>Tracing Voice AI is Hard: How I Instrumented Streaming LLMs with OpenTelemetry and SigNoz</title>
      <dc:creator>Jay Prajapati</dc:creator>
      <pubDate>Sun, 26 Jul 2026 18:06:00 +0000</pubDate>
      <link>https://dev.to/jay9122/tracing-voice-ai-is-hard-how-i-instrumented-streaming-llms-with-opentelemetry-and-signoz-5c03</link>
      <guid>https://dev.to/jay9122/tracing-voice-ai-is-hard-how-i-instrumented-streaming-llms-with-opentelemetry-and-signoz-5c03</guid>
      <description>&lt;p&gt;Voice AI agents are magical until they start lagging. Suddenly my Groq-powered voice assistant started taking 3 seconds to respond and standard APM tools were useless. They could tell me that an HTTP request was open, but they couldn’t tell me if the bottleneck was the Speech-to-Text (STT) transcription, the LLM’s Time to First Token (TTFT), or the Text-to-Speech (TTS) synthesis. &lt;/p&gt;

&lt;p&gt;Here’s how I did this by building &lt;a href="https://github.com/Priyank911/zooid" rel="noopener noreferrer"&gt;Zooid&lt;/a&gt;, a custom OpenTelemetry instrumentation layer that traces streaming Voice AI, tracks FinOps token costs, and visualizes everything natively in SigNoz.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Web Tracing or Voice Tracing
&lt;/h2&gt;

&lt;p&gt;A typical CRUD web application is easy to trace. A request comes in, hits a database, and returns a JSON response. You drop in the &lt;code&gt;opentelemetry-instrumentation-flask&lt;/code&gt; package, and you are done. &lt;/p&gt;

&lt;p&gt;Voice AI is a whole other beast. A "Voice Turn" consists of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Obtaining raw audio streams (STT).&lt;/li&gt;
&lt;li&gt;Streaming text tokens from an LLM as they are produced.&lt;/li&gt;
&lt;li&gt;On the fly conversion of those text chunks back to audio streams (TTS)&lt;/li&gt;
&lt;li&gt;Managing sudden interruption from the user (barge-in).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where the normal auto-instrumentation fails, since the LLM returns an async generator and not a static response. If you just trace the function execution, the span ends when the generator is &lt;em&gt;created&lt;/em&gt;, not when the stream actually &lt;em&gt;finishes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I needed a way to track the lifecycle of a streaming conversation manually, measure Voice specific metrics (like Time To First Audio) and get this data into SigNoz.# Step 1. Instrumenting the async stream&lt;/p&gt;

&lt;p&gt;I created a custom Python decorator (@instrument_voice_app) that manually manages the OpenTelemetry tracer to address the streaming problem. &lt;/p&gt;

&lt;p&gt;Instead of wrapping a function, it injects a &lt;code&gt;VoiceTurnContext&lt;/code&gt; object into the agent. This enables the inner AI logic to tell you exactly when specific milestones are achieved, such as when the initial audio byte is streamed back to the user.&lt;/p&gt;

&lt;p&gt;Here's the basic logic I used to follow the stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zooid.voice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;instrument_voice_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;turn_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VoiceTurnContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# Execute the Voice Agent
&lt;/span&gt;                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Record Voice-Specific Metrics
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_audio_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;ttfa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_audio_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;turn_start&lt;/span&gt;
                    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice.ttfa_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttfa&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stt_confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stt.confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stt_confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice.turn_cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave me a beautiful, continuous trace spanning the entire user interaction, but it introduced a new problem: &lt;strong&gt;How do I visualize these custom attributes in SigNoz?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The ClickHouse “Gotcha” (and how I fix it)
&lt;/h2&gt;

&lt;p&gt;Sending data to the OpenTelemetry Collector (localhost:4317) was quite easy. I could immediately see the traces in the SigNoz "Traces" tab.  &lt;/p&gt;

&lt;p&gt;But when I went to the "Dashboards" tab and tried to create a Time-Series panel for &lt;code&gt;voice.turn_cost_usd&lt;/code&gt; and &lt;code&gt;voice.ttfa_ms&lt;/code&gt;, the dashboard showed &lt;strong&gt;"No Data"&lt;/strong&gt;.## The Problem with It&lt;br&gt;
This is where I found a very important piece of information on how SigNoz stores OpenTelemetry attributes in ClickHouse. &lt;/p&gt;

&lt;p&gt;In Python, when you do span.set_attribute("voice.ttfa_ms", 450.5) it is sent as a Number by OpenTelemetry. This is cleanly stored in the new SigNoz trace schema (&lt;code&gt;signoz_index_v3&lt;/code&gt;). However, SigNoz custom dashboard query builder depends on materialized views to map these attributes correctly to aggregate these metrics effectively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The crucial missing lines in the Materialized View&lt;/span&gt;
&lt;span class="n"&gt;attributes_number&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;numberTagMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;attributes_bool&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;boolTagMap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I applied this fix and backfilled the index, the query builder instantly recognized my custom metrics. I was able to query &lt;code&gt;numberTagMap.voice.turn_cost_usd&lt;/code&gt; as a Number data type directly in the SigNoz UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Building the Staff SRE Dashboard
&lt;/h2&gt;

&lt;p&gt;With the data flowing correctly, I built a custom JSON Dashboard in SigNoz tailored entirely for Voice AI. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;(If you are following along, you can import this directly into your SigNoz instance by clicking "Import JSON" in the Dashboards tab).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of looking at generic CPU usage, my dashboard tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P99 TTFA (Time To First Audio)&lt;/strong&gt;: The most critical metric for conversational AI. If this goes over 800ms, the user thinks the bot is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FinOps / Cost Per Turn&lt;/strong&gt;: Aggregating the &lt;code&gt;voice.turn_cost_usd&lt;/code&gt; attribute to track Groq/OpenAI token spend in real-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Barge-in Rate&lt;/strong&gt;: Tracking the &lt;code&gt;voice.interrupted&lt;/code&gt; boolean attribute to see how often users cut the AI off (a great indicator of whether the bot is talking too much).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Insert Screenshot of the SigNoz Dashboard showing the TTFA Time Series and FinOps Cost panels here)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerting on AI Hallucinations and Latency
&lt;/h3&gt;

&lt;p&gt;Finally, I utilized SigNoz's Alert Rules to act like a Staff SRE. I set up an alert that triggers if the &lt;code&gt;P99 TTFA &amp;gt; 1000ms&lt;/code&gt; for more than 5 minutes. &lt;/p&gt;

&lt;p&gt;Because we injected the &lt;code&gt;LLM_PROVIDER&lt;/code&gt; as a span attribute (e.g., &lt;code&gt;Groq&lt;/code&gt; vs &lt;code&gt;OpenAI&lt;/code&gt;), the alert payload directly tells me &lt;em&gt;which&lt;/em&gt; provider is lagging, allowing my team to instantly route traffic to a fallback provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry is incredibly extensible.&lt;/strong&gt; You aren't limited to HTTP spans. By managing the context manually, you can trace practically any asynchronous, streaming workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know your ClickHouse schema.&lt;/strong&gt; If your custom attributes aren't showing up in SigNoz dashboards, check how your Number and Boolean tags are being mapped in the underlying ClickHouse tables. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice AI requires FinOps tracing.&lt;/strong&gt; Tracking latency isn't enough anymore. Injecting token costs (&lt;code&gt;voice.turn_cost_usd&lt;/code&gt;) directly into the trace span means you can correlate slow performance directly with high inference costs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Standard APM isn't built for the generative AI era, but OpenTelemetry and SigNoz give you the primitives to build exactly what you need. By manually instrumenting our async generators and mapping custom number attributes in ClickHouse, we turned a black-box Voice AI script into a fully observable, enterprise-grade system.&lt;/p&gt;

&lt;p&gt;If you want to try it out for your own Voice AI projects, you can grab the Python SDK from &lt;a href="https://pypi.org/project/zooid/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;, generate a full boilerplate app via &lt;code&gt;npx create-zooid&lt;/code&gt; (&lt;a href="https://www.npmjs.com/package/create-zooid" rel="noopener noreferrer"&gt;npm&lt;/a&gt;), or check out the full code on &lt;a href="https://github.com/Priyank911/zooid" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the WeMakeDevs &amp;amp; SigNoz Observability Hackathon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>monitoring</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
