<?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: Gajanan Gitte</title>
    <description>The latest articles on DEV Community by Gajanan Gitte (@gajanangitte).</description>
    <link>https://dev.to/gajanangitte</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%2F4035454%2F5c016477-fe78-4fd4-8970-3e8a2b33aa1a.jpg</url>
      <title>DEV Community: Gajanan Gitte</title>
      <link>https://dev.to/gajanangitte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gajanangitte"/>
    <language>en</language>
    <item>
      <title>I gave my local Llama agent the SigNoz MCP server and asked it to debug itself</title>
      <dc:creator>Gajanan Gitte</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:00:37 +0000</pubDate>
      <link>https://dev.to/gajanangitte/i-gave-my-local-llama-agent-the-signoz-mcp-server-and-asked-it-to-debug-itself-5909</link>
      <guid>https://dev.to/gajanangitte/i-gave-my-local-llama-agent-the-signoz-mcp-server-and-asked-it-to-debug-itself-5909</guid>
      <description>&lt;h1&gt;
  
  
  The agent that reads its own traces
&lt;/h1&gt;

&lt;p&gt;Observability is something we do &lt;em&gt;to&lt;/em&gt; our agents. We instrument them, then a human opens SigNoz and reads the waterfall. But SigNoz now ships an &lt;strong&gt;MCP server&lt;/strong&gt; — a &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; interface that turns "search traces", "aggregate latency", "list services" into tools an LLM can call. So the console isn't human-only anymore. Which raises a fun question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if the agent read its &lt;strong&gt;own&lt;/strong&gt; traces and debugged &lt;strong&gt;itself&lt;/strong&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I connected my agent — a tiny &lt;code&gt;qwen2.5:3b&lt;/code&gt; running on &lt;strong&gt;CPU&lt;/strong&gt; via Ollama, no GPU, no cloud — to the SigNoz MCP server and asked it one question: &lt;strong&gt;"Why are you slow?"&lt;/strong&gt; It called an MCP tool, pulled its own p95 latency out of SigNoz, and answered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The llm.chat model calls have a p95 latency of approximately **101,375.3 ms&lt;/em&gt;&lt;em&gt;, while the tool.&lt;/em&gt; calls have a p95 of about &lt;strong&gt;1.5 ms&lt;/strong&gt;. The model call is roughly &lt;strong&gt;67,584 times slower&lt;/strong&gt; than the tool call... focus on optimizing the llm.chat model calls or consider a more efficient alternative."*&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the agent re-discovering, from the &lt;em&gt;inside&lt;/em&gt;, the exact lesson I found by hand in &lt;a href="https://dev.to/gajanangitte/i-gave-a-local-llama-agent-opentelemetry-eyes-tracing-tokens-cost-and-the-latency-tail-in-4380"&gt;my first post&lt;/a&gt; (one LLM call was 84.5% of a request). Only this time I didn't read the trace. It did.&lt;/p&gt;

&lt;p&gt;Everything is local and free: &lt;strong&gt;SigNoz v0.133&lt;/strong&gt;, the &lt;strong&gt;SigNoz MCP server&lt;/strong&gt;, and &lt;strong&gt;Ollama&lt;/strong&gt; on CPU. Here's how it works — and the two things that actually made a tiny model reliable over MCP.&lt;/p&gt;




&lt;h2&gt;
  
  
  The loop: an agent that observes the observer
&lt;/h2&gt;

&lt;p&gt;The agent already emitted OpenTelemetry traces (&lt;code&gt;agent.invoke → llm.chat → tool.*&lt;/code&gt;). To let it read them back, I gave it a second entrypoint with a different toolset: instead of the fake SRE tools, it gets &lt;strong&gt;SigNoz MCP tools&lt;/strong&gt;, and the whole session is rooted in an &lt;code&gt;agent.introspect&lt;/code&gt; span. That means the self-debugging run is &lt;em&gt;itself&lt;/em&gt; a trace you can open in SigNoz — the observer, observed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kde09q9tsrlxf7371v0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kde09q9tsrlxf7371v0.png" alt="The self-debug trace: agent.introspect calls llm.chat, then a tool that wraps a real mcp.signoz_aggregate_traces call, then a final llm.chat — 5 spans, 0 errors"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Top to bottom: &lt;code&gt;agent.introspect&lt;/code&gt; → &lt;code&gt;llm.chat&lt;/code&gt; (the model decides what to look at) → &lt;code&gt;tool.latency_by_operation&lt;/code&gt; → &lt;strong&gt;&lt;code&gt;mcp.signoz_aggregate_traces&lt;/code&gt;&lt;/strong&gt; (a real MCP call out to the server) → &lt;code&gt;llm.chat&lt;/code&gt; (the diagnosis). The agent made an MCP request to SigNoz &lt;em&gt;as a step inside its own trace&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The MCP server itself is one binary in HTTP mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SIGNOZ_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 &lt;span class="nv"&gt;SIGNOZ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;TRANSPORT_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http &lt;span class="nv"&gt;MCP_SERVER_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8000 ./signoz-mcp-server
&lt;span class="c"&gt;# -&amp;gt; listening on :8000/mcp, 41 tools registered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the client bridge is deliberately tiny — the agent loop is synchronous, so each tool call opens a short-lived streamable-HTTP session:&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;mcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.client.streamable_http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamablehttp_client&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SigNozMCP&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;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&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;run&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;with&lt;/span&gt; &lt;span class="nf"&gt;streamablehttp_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&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;with&lt;/span&gt; &lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&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;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool the model calls is wrapped in its own &lt;code&gt;mcp.*&lt;/code&gt; span, so the MCP round-trip shows up in the trace with the exact request it made:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_mcp_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLIENT&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;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;mcp.tool.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&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;mcp.method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&lt;/span&gt;&lt;span class="sh"&gt;"&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;mcp.transport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamable_http&lt;/span&gt;&lt;span class="sh"&gt;"&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;mcp.request.args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click that span and there's the proof it was a genuine MCP protocol call — and that the agent was querying &lt;strong&gt;itself&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqa87ahvtc2k4eaby7ae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqa87ahvtc2k4eaby7ae.png" alt="Span details for mcp.signoz_aggregate_traces: mcp.method=tools/call, mcp.transport=streamable_http, mcp.server.url=localhost:8000/mcp, and request args filtering service.name='observable-agent'"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp.tool.name: signoz_aggregate_traces&lt;/code&gt;, &lt;code&gt;mcp.method: tools/call&lt;/code&gt;, and the args: &lt;em&gt;aggregate p95 of &lt;code&gt;duration_nano&lt;/code&gt;, grouped by span name, filtered to &lt;code&gt;service.name = 'observable-agent'&lt;/code&gt;.&lt;/em&gt; The agent asked SigNoz for its own latency profile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest problem #1: a 3B model can't drive 41 raw MCP tools
&lt;/h2&gt;

&lt;p&gt;Here's the part the demos skip. The SigNoz MCP server exposes &lt;strong&gt;41 tools&lt;/strong&gt;, and the good ones take rich arguments — nanosecond durations, free-form SigNoz filter expressions, &lt;code&gt;groupBy&lt;/code&gt; keys, relative time ranges. That's fine for Claude. It is &lt;em&gt;not&lt;/em&gt; fine for a 3B model on CPU. My first attempt handed the model the raw tools. &lt;code&gt;llama3.2:3b&lt;/code&gt; didn't even emit a valid tool call — it printed one as text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;A:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"latency_by_operation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"parameters:{}}   # &amp;lt;- malformed, not a real tool call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fixes made it reliable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A tool-calling model.&lt;/strong&gt; &lt;code&gt;llama3.2:3b&lt;/code&gt; fumbles the function-calling format; &lt;code&gt;qwen2.5:3b&lt;/code&gt; (same size, same CPU) gets it right. Model choice matters more than model size here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curated, argument-free tools.&lt;/strong&gt; Instead of exposing 41 tools with complex schemas, I expose &lt;strong&gt;three&lt;/strong&gt; with no arguments — &lt;code&gt;list_my_services&lt;/code&gt;, &lt;code&gt;latency_by_operation&lt;/code&gt;, &lt;code&gt;find_slowest_traces&lt;/code&gt; — each of which pins the correct arguments and calls a real MCP tool under the hood. The model picks &lt;em&gt;what to look at&lt;/em&gt;; the wrapper handles &lt;em&gt;how to ask&lt;/em&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;latency_by_operation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;MCP: signoz_aggregate_traces -&amp;gt; p95 latency per operation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_mcp_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signoz_aggregate_traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aggregateOn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_nano&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupBy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service.name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SERVICE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeRange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;# ...trim the big JSON down to [{operation, p95_ms}] the model can reason about
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the pattern for small models + MCP: &lt;strong&gt;don't expose the raw firehose; expose a few intention-shaped tools and let the server do the heavy lifting.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest problem #2: small models hallucinate numbers, so let SigNoz do the math
&lt;/h2&gt;

&lt;p&gt;Even with a clean tool result, &lt;code&gt;qwen2.5:3b&lt;/code&gt;'s first diagnosis invented figures out of thin air — "llm.chat p95 is 200ms, tools are 300ms" — numbers that appear &lt;em&gt;nowhere&lt;/em&gt; in the data. A 3B model is a bad calculator and a worse transcriber of long decimals.&lt;/p&gt;

&lt;p&gt;The fix isn't a bigger model — it's &lt;strong&gt;the right division of labour&lt;/strong&gt;. Percentile math is exactly what SigNoz's aggregation is &lt;em&gt;for&lt;/em&gt;, so I compute the comparison in the tool and hand the model a factual, hard-to-hallucinate headline. The model's job is to orchestrate and interpret, not to do nanosecond arithmetic in its head:&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="n"&gt;headline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm.chat p95 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;llm_ms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms; slowest tool (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_ms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms; the model call is ~&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;x slower than the tool call.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that grounding, the diagnosis became correct and specific — and you can see the exact numbers it read, captured in the tool span's result attribute:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7602yprx8dovsgjv9et2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7602yprx8dovsgjv9et2.png" alt="Span details for tool.latency_by_operation: tool.result shows llm.chat p95=101375.3ms, slowest tool 1.5ms, ~67584x slower, plus the full per-operation list"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;llm.chat&lt;/code&gt; p95 &lt;strong&gt;101,375.3 ms&lt;/strong&gt; vs the slowest real tool at &lt;strong&gt;1.5 ms&lt;/strong&gt;. The agent read that and concluded, correctly, that it is ~67,000× bottlenecked on the model — not its logic, not its tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  The observer effect is real (and it's in the trace)
&lt;/h2&gt;

&lt;p&gt;One honest wrinkle: the introspection runs are themselves slow (CPU inference), so they &lt;strong&gt;pollute the very data the agent reads&lt;/strong&gt;. The first version of my latency tool proudly reported that &lt;code&gt;agent.introspect&lt;/code&gt; was the slowest operation — because it was measuring itself. The &lt;code&gt;mcp.*&lt;/code&gt; and telemetry-reading &lt;code&gt;tool.*&lt;/code&gt; spans, dominated by the SigNoz round-trip, leaked into the "tool" numbers too and shrank the ratio from ~67,000× to a meaningless 12×.&lt;/p&gt;

&lt;p&gt;The fix was to exclude the introspection scaffolding (&lt;code&gt;agent.introspect&lt;/code&gt;, &lt;code&gt;mcp.*&lt;/code&gt;, the three reader tools) from the comparison so the agent reasons about its real request-serving workload. But it's a genuine lesson worth stating plainly: &lt;strong&gt;when an agent observes itself, it perturbs what it observes.&lt;/strong&gt; You can see it happen span by span.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is a different corner of MCP
&lt;/h2&gt;

&lt;p&gt;SigNoz's own MCP examples — reasonably — show a frontier cloud model (Claude, Cursor) querying a production stack. This is the opposite extreme on purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Typical MCP demo&lt;/th&gt;
&lt;th&gt;This&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Cloud frontier (Claude/GPT)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qwen2.5:3b&lt;/code&gt;, &lt;strong&gt;CPU-only, local&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;Someone else's production&lt;/td&gt;
&lt;td&gt;Its &lt;strong&gt;own&lt;/strong&gt; traces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Fully &lt;strong&gt;offline&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point&lt;/td&gt;
&lt;td&gt;Human asks about a service&lt;/td&gt;
&lt;td&gt;Agent &lt;strong&gt;debugs itself&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of it is cloud-shaped. A 3-billion-parameter model on a laptop CPU used a standards-based protocol to read its own OpenTelemetry data out of a self-hosted backend and reached a true conclusion about its own performance. That is a self-observing agent, built from open parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's honest about this
&lt;/h2&gt;

&lt;p&gt;The model doesn't do the percentile math and I curated its tools — that's not cheating, it's system design: the LLM orchestrates and interprets, SigNoz computes, MCP is the contract between them. What's completely real is the loop: the agent &lt;em&gt;chose&lt;/em&gt; to look at latency, issued a real &lt;code&gt;tools/call&lt;/code&gt; to the SigNoz MCP server, read numbers measured from its own spans, and diagnosed itself. And it did it on hardware you already own.&lt;/p&gt;

&lt;p&gt;The takeaway from my &lt;a href="https://dev.to/gajanangitte/i-gave-a-local-llama-agent-opentelemetry-eyes-tracing-tokens-cost-and-the-latency-tail-in-4380"&gt;first&lt;/a&gt; and &lt;a href="https://dev.to/gajanangitte/the-retry-tax-what-a-local-llama-agents-silent-retries-actually-cost-measured-in-self-hosted-3io2"&gt;second&lt;/a&gt; posts was "you can't own an agent you can't observe." MCP adds a sequel: once the telemetry is an agent-callable API, the agent can observe &lt;em&gt;itself&lt;/em&gt; — and the next step, closing the loop from &lt;em&gt;diagnose&lt;/em&gt; to &lt;em&gt;act&lt;/em&gt;, is suddenly a very short walk.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stack: SigNoz v0.133 (self-hosted, WSL2 + Docker Engine), SigNoz MCP server (HTTP/streamable transport, 41 tools), OpenTelemetry Python SDK, Ollama &lt;code&gt;qwen2.5:3b&lt;/code&gt; on CPU. Introspection agent + MCP bridge + curated tools: ~200 lines of Python. Built for the WeMakeDevs × SigNoz "Agents of SigNoz" hackathon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>observability</category>
      <category>opentelemetry</category>
      <category>signoz</category>
      <category>mcp</category>
    </item>
    <item>
      <title>The Retry Tax: what a local Llama agent's silent retries actually cost — measured in self-hosted SigNoz</title>
      <dc:creator>Gajanan Gitte</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:00:33 +0000</pubDate>
      <link>https://dev.to/gajanangitte/the-retry-tax-what-a-local-llama-agents-silent-retries-actually-cost-measured-in-self-hosted-3io2</link>
      <guid>https://dev.to/gajanangitte/the-retry-tax-what-a-local-llama-agents-silent-retries-actually-cost-measured-in-self-hosted-3io2</guid>
      <description>&lt;h1&gt;
  
  
  The Retry Tax
&lt;/h1&gt;

&lt;p&gt;Every "AI agent" retries. The model returns malformed JSON, a guardrail rejects the answer, a tool-call schema doesn't validate, a request times out — so the agent quietly runs the model again. That retry is usually invisible. It doesn't throw. The user still gets an answer. But you paid for the inference &lt;strong&gt;twice&lt;/strong&gt;: twice the tokens, twice the latency, twice the GPU-seconds. I call it the &lt;strong&gt;retry tax&lt;/strong&gt;, and until you trace it, it's a silent line item on your bill.&lt;/p&gt;

&lt;p&gt;In an &lt;a href="https://dev.to/gajanangitte/i-gave-a-local-llama-agent-opentelemetry-eyes-tracing-tokens-cost-and-the-latency-tail-in-4380"&gt;earlier post&lt;/a&gt; I gave a small local-Llama SRE agent OpenTelemetry eyes and found one LLM call was 84.5% of a request. This post is the follow-up I actually care about: I made that agent &lt;strong&gt;retry once&lt;/strong&gt;, deliberately, and measured the tax in &lt;strong&gt;self-hosted SigNoz&lt;/strong&gt;. One wasted call — a response the agent generated and then threw away — turned out to be &lt;strong&gt;34% of the request's wall-clock time&lt;/strong&gt;. I only know that because the trace shows the work that got discarded.&lt;/p&gt;

&lt;p&gt;Everything here is local and free: &lt;strong&gt;SigNoz v0.133&lt;/strong&gt;, &lt;strong&gt;Ollama&lt;/strong&gt; running &lt;code&gt;llama3.2:1b&lt;/code&gt; on CPU, and ~200 lines of instrumented Python. No cloud model, no vendor dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  The failure I wanted to see
&lt;/h2&gt;

&lt;p&gt;Real retries have messy causes. To study the &lt;em&gt;cost&lt;/em&gt; of a retry without the noise of &lt;em&gt;why&lt;/em&gt;, I injected a clean, deterministic fault: &lt;strong&gt;drop the first completed response exactly once per request.&lt;/strong&gt; The key word is &lt;em&gt;completed&lt;/em&gt; — the model already ran, the tokens were already spent. Then the agent has to infer again. That's the honest shape of a retry: you don't get the tokens back.&lt;/p&gt;

&lt;p&gt;The whole fault is four environment-driven knobs in &lt;code&gt;config.py&lt;/code&gt;:&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="c1"&gt;# EXPERIMENT_ID tags every span + metric so "control" and "chaos" cohorts are
# directly comparable in SigNoz. CHAOS_DROP_RESPONSE_ONCE drops the first
# completed response of each request, forcing exactly one retry.
&lt;/span&gt;&lt;span class="n"&gt;EXPERIMENT_ID&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPERIMENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CHAOS_DROP_ONCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHAOS_DROP_RESPONSE_ONCE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;LLM_MAX_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_MAX_ATTEMPTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;RETRY_BACKOFF_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRY_BACKOFF_MS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;250&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's LLM round-trip becomes an attempt loop. Each attempt is its own &lt;code&gt;llm.chat&lt;/code&gt; span, so a retry literally &lt;em&gt;duplicates&lt;/em&gt; the span inside the same trace — the retry tax made visible:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LLM_MAX_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CHAOS_DROP_ONCE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;llm.chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLIENT&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;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;llm.attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;   &lt;span class="c1"&gt;# inference runs here
&lt;/span&gt;            &lt;span class="n"&gt;in_tok&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt;
            &lt;span class="n"&gt;out_tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion_tokens&lt;/span&gt;

            &lt;span class="c1"&gt;# Injected fault: drop this completed response exactly once. The
&lt;/span&gt;            &lt;span class="c1"&gt;# tokens above were really generated, so we still record them
&lt;/span&gt;            &lt;span class="c1"&gt;# (status="dropped") -- that is the wasted work of the retry.
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chaos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;armed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chaos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;armed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                &lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dropped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_dropped&lt;/span&gt;&lt;span class="sh"&gt;"&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;fault.injected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&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;retry.reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_dropped&lt;/span&gt;&lt;span class="sh"&gt;"&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;add_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry.scheduled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry.reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_dropped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry.backoff_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;backoff_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next.attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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="nc"&gt;Status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response dropped after completion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff_ms&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this trace &lt;em&gt;useful&lt;/em&gt; instead of just red:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The dropped attempt still &lt;strong&gt;records its tokens&lt;/strong&gt; (&lt;code&gt;status="dropped"&lt;/code&gt;). Wasted work you don't measure isn't waste — it's a mystery.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;experiment.id&lt;/code&gt; is stamped on every span and every metric, so a &lt;code&gt;control&lt;/code&gt; run and a &lt;code&gt;chaos&lt;/code&gt; run are directly comparable in the Query Builder. One counter counts the retries themselves:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Count one retry (e.g. a dropped response forced the agent to re-infer).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;_retries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;_tag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry.reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Two cohorts, same ten questions
&lt;/h2&gt;

&lt;p&gt;I ran the identical load twice — ten SRE questions like &lt;em&gt;"Is checkout healthy?"&lt;/em&gt; — and tagged them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# control: no chaos&lt;/span&gt;
&lt;span class="nv"&gt;EXPERIMENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rt-control python run_load.py

&lt;span class="c"&gt;# chaos: drop the first response of every request, once&lt;/span&gt;
&lt;span class="nv"&gt;EXPERIMENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rt-chaos &lt;span class="nv"&gt;CHAOS_DROP_RESPONSE_ONCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python run_load.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I asked ClickHouse (SigNoz's store) what the tax was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric (10 requests each)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;rt-control&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;rt-chaos&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;llm.chat&lt;/code&gt; spans&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+50%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dropped (wasted) calls&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens&lt;/td&gt;
&lt;td&gt;7,322&lt;/td&gt;
&lt;td&gt;11,515&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+57%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;— of which &lt;em&gt;wasted&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,393&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;38% of all tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost (USD)&lt;/td&gt;
&lt;td&gt;$0.000734&lt;/td&gt;
&lt;td&gt;$0.001153&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+57%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg latency / request&lt;/td&gt;
&lt;td&gt;43.1 s&lt;/td&gt;
&lt;td&gt;57.0 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+32%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency&lt;/td&gt;
&lt;td&gt;91.2 s&lt;/td&gt;
&lt;td&gt;106.1 s&lt;/td&gt;
&lt;td&gt;+16%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One retry per request — the mildest possible retry policy — cost &lt;strong&gt;57% more tokens and money&lt;/strong&gt; and made the agent &lt;strong&gt;a third slower&lt;/strong&gt;. &lt;strong&gt;38% of every token the chaos cohort burned went to answers it threw in the bin.&lt;/strong&gt; Now imagine a real policy of "retry up to 3× on invalid JSON" against a paid API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading the tax in a single trace
&lt;/h2&gt;

&lt;p&gt;Here's one &lt;code&gt;chaos&lt;/code&gt; request. The waterfall tells the whole story at a glance — &lt;strong&gt;5 spans, 1 error&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1hfjvd4g3xo4h3bavhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1hfjvd4g3xo4h3bavhb.png" alt="Chaos trace: a dropped llm.chat (red), an immediate retry, the tool call, then the final answer" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Top to bottom: &lt;code&gt;agent.invoke&lt;/code&gt; → a &lt;strong&gt;red &lt;code&gt;llm.chat&lt;/code&gt;&lt;/strong&gt; (the dropped attempt) → a second &lt;code&gt;llm.chat&lt;/code&gt; (the retry) → &lt;code&gt;tool.get_service_health&lt;/code&gt; → a final &lt;code&gt;llm.chat&lt;/code&gt;. Two back-to-back inference spans &lt;em&gt;before any useful work happens&lt;/em&gt;. In the clean cohort that first red span simply doesn't exist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe4fn8telwx20s00srp09.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe4fn8telwx20s00srp09.png" alt="Control trace: one inference per step, no retry, zero errors" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the red span and the attributes make the waste concrete — and queryable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb377abwwosl9u9e5rxz9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb377abwwosl9u9e5rxz9.png" alt="Span details: fault.injected=true, retry.reason=response_dropped, 431 tokens spent, a retry.scheduled event with 250ms backoff" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That single dropped call was &lt;strong&gt;11.12s — 34.08% of total execution time&lt;/strong&gt;, spent &lt;strong&gt;431 tokens&lt;/strong&gt; (413 in + 18 out), and carries a &lt;code&gt;retry.scheduled&lt;/code&gt; &lt;strong&gt;span event&lt;/strong&gt; with &lt;code&gt;retry.backoff_ms: 250&lt;/code&gt; and &lt;code&gt;next.attempt: 2&lt;/code&gt;. Because these are real span attributes, "show me every dropped inference and how many tokens it wasted" is just a filter: &lt;code&gt;name = 'llm.chat' AND retry.reason = 'response_dropped'&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A dashboard for the tax
&lt;/h2&gt;

&lt;p&gt;Same filter, turned into a &lt;strong&gt;Retry Tax dashboard&lt;/strong&gt; via the SigNoz dashboards API (v5 query-builder schema, &lt;code&gt;dataSource: traces&lt;/code&gt;). The &lt;code&gt;experiment.id&lt;/code&gt; tag does the control-vs-chaos split for free:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fop9uc2iu692qlnt504b7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fop9uc2iu692qlnt504b7.png" alt="Retry Tax dashboard: 50 LLM calls, 11 retried, 4,827 wasted tokens; cost-by-cohort pie shows chaos $0.00115 vs control $0.00073" width="800" height="976"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The clearest panel is &lt;strong&gt;Cost by Cohort&lt;/strong&gt;: the pink &lt;code&gt;rt-chaos&lt;/code&gt; slice ($0.00115) is over 1.5× the green &lt;code&gt;rt-control&lt;/code&gt; slice ($0.00073) for &lt;em&gt;the exact same ten questions&lt;/em&gt;. The value tiles count retried calls and wasted tokens straight from the span attributes — no code change, just a query. (They read a little above the clean table further up because the dashboard tallies every request in its window, warm-ups included; the table is the controlled 10-vs-10.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing the loop: alert on the tax
&lt;/h2&gt;

&lt;p&gt;A number on a dashboard nobody looks at is worthless. So I made the retry tax &lt;strong&gt;page me&lt;/strong&gt;. SigNoz v0.133 alert rules use the new v5 &lt;code&gt;queries: []&lt;/code&gt; array (each query wrapped in a &lt;code&gt;{type, spec}&lt;/code&gt; envelope) — worth knowing, because the old &lt;code&gt;builderQueries&lt;/code&gt; map returns a flat &lt;em&gt;"alert rule is not valid"&lt;/em&gt;. The rule counts dropped attempts over a window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Agent Retry Tax: LLM responses being retried"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alertType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRACES_BASED_ALERT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compositeQuery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"builder_query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"spec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"traces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aggregations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"count()"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"service.name = 'observable-agent' AND name = 'llm.chat' AND retry.reason = 'response_dropped'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"panelType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"graph"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"queryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"builder"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"above"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matchType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"at_least_once"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;POST /api/v1/rules&lt;/code&gt;, and within a minute it was firing on the chaos data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6oz2nh8kmm5wri6neflo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6oz2nh8kmm5wri6neflo.png" alt="SigNoz Triggered Alerts: 'Agent Retry Tax' firing, severity warning, with the firing-since timestamp" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the moment my agent starts silently re-inferring — dropped responses, guardrail rejects, 429 back-offs — I find out from an alert instead of from a surprise invoice.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's honest about this
&lt;/h2&gt;

&lt;p&gt;I &lt;em&gt;injected&lt;/em&gt; the fault, so don't read the exact 57% as a universal constant — your tax depends on your retry policy and how often reality trips it. But the &lt;strong&gt;mechanism&lt;/strong&gt; is completely real: an LLM call that completes and then gets discarded costs you full tokens and full latency, and by default that cost is invisible because the agent recovers. Genuine sources of the same tax: JSON that won't parse, tool arguments that fail schema validation, content-filter rejections, timeouts, and rate-limit retries — each re-invokes the model, producing the same duplicated &lt;code&gt;llm.chat&lt;/code&gt; span you see here. (This demo wires up the dropped-response path; the others share the identical signature.)&lt;/p&gt;

&lt;p&gt;The takeaway isn't "retries are bad" — retries are how agents stay reliable. It's that &lt;strong&gt;an un-observed retry is a tax you can't see, can't bound, and can't alert on.&lt;/strong&gt; Three OpenTelemetry attributes — &lt;code&gt;llm.attempt&lt;/code&gt;, &lt;code&gt;retry.reason&lt;/code&gt;, and the dropped-attempt's token counts — turn it into a first-class signal you can trace, chart, and page on. In SigNoz, that took one attribute loop and one filter.&lt;/p&gt;

&lt;p&gt;If you can't observe your AI agents, you don't own them — and you definitely don't own their retry bill.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stack: SigNoz v0.133 (self-hosted via Foundry on WSL2 + Docker Engine), OpenTelemetry Python SDK, Ollama &lt;code&gt;llama3.2:1b&lt;/code&gt;. Agent + instrumentation + load driver: ~250 lines of Python. Built for the WeMakeDevs × SigNoz "Agents of SigNoz" hackathon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>observability</category>
      <category>opentelemetry</category>
      <category>signoz</category>
      <category>llm</category>
    </item>
    <item>
      <title>I gave a local Llama agent OpenTelemetry eyes: tracing tokens, cost, and the latency tail in self-hosted SigNoz</title>
      <dc:creator>Gajanan Gitte</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:00:28 +0000</pubDate>
      <link>https://dev.to/gajanangitte/i-gave-a-local-llama-agent-opentelemetry-eyes-tracing-tokens-cost-and-the-latency-tail-in-4380</link>
      <guid>https://dev.to/gajanangitte/i-gave-a-local-llama-agent-opentelemetry-eyes-tracing-tokens-cost-and-the-latency-tail-in-4380</guid>
      <description>&lt;h1&gt;
  
  
  I gave a local Llama agent OpenTelemetry eyes
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you can't observe your AI agents, you don't own them."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That line from the &lt;em&gt;Agents of SigNoz&lt;/em&gt; hackathon stuck with me, so I tested it literally. I took a small SRE "sidekick" agent running on a &lt;strong&gt;local Llama model&lt;/strong&gt; — no OpenAI bill, no vendor dashboard — wired it up with &lt;strong&gt;OpenTelemetry&lt;/strong&gt;, and pointed it at a &lt;strong&gt;self-hosted SigNoz&lt;/strong&gt;. Then I watched what an agent actually does when you ask it a question.&lt;/p&gt;

&lt;p&gt;The short version: the tool calls were never the problem. &lt;strong&gt;One LLM call was 84.5% of the entire request.&lt;/strong&gt; I only know that number because the agent had eyes.&lt;/p&gt;

&lt;p&gt;This post is the narrow, practical walkthrough: self-host SigNoz, instrument an agent with the OpenTelemetry &lt;strong&gt;GenAI semantic conventions&lt;/strong&gt;, and read the trace, tokens, cost, and latency tail that come out the other side.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack (all local, all free)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What I used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Observability backend&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;SigNoz v0.133&lt;/strong&gt; (self-hosted via Foundry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OpenTelemetry Python SDK&lt;/strong&gt; (manual spans + metrics + logs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;OTLP/HTTP → &lt;code&gt;:4318&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; running &lt;code&gt;llama3.2:1b&lt;/code&gt; and &lt;code&gt;llama3.2:3b&lt;/code&gt; on CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;~200 lines of Python: a tool-calling loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Heads-up if you're following an old tutorial: the &lt;code&gt;install.sh&lt;/code&gt; script and the bundled &lt;code&gt;docker-compose&lt;/code&gt; files are &lt;strong&gt;deprecated as of v0.130&lt;/strong&gt;. SigNoz now installs through &lt;strong&gt;Foundry&lt;/strong&gt;, a small "observability-stack-as-code" CLI. It's still three steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. install the Foundry CLI&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash

&lt;span class="c"&gt;# 2. a five-line casting.yaml (this is the whole file)&lt;/span&gt;
&lt;span class="c"&gt;#   apiVersion: v1alpha1&lt;/span&gt;
&lt;span class="c"&gt;#   kind: Installation&lt;/span&gt;
&lt;span class="c"&gt;#   metadata: { name: signoz }&lt;/span&gt;
&lt;span class="c"&gt;#   spec: { deployment: { flavor: compose, mode: docker } }&lt;/span&gt;

&lt;span class="c"&gt;# 3. deploy&lt;/span&gt;
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm on Windows, so I ran everything inside &lt;strong&gt;WSL 2 with Docker Engine&lt;/strong&gt; — not Docker Desktop. That's not a personal quirk: SigNoz's own docs warn that ClickHouse Keeper crash-loops (exit 139, segfaults) under Docker Desktop's VM layer on Windows. I hit exactly that before switching, so consider this your warning too.&lt;/p&gt;

&lt;p&gt;A minute later the UI is on &lt;code&gt;http://localhost:8080&lt;/code&gt;, and its onboarding checklist lights up green the moment your app starts sending data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9ljmbw79vvymtx88y1h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq9ljmbw79vvymtx88y1h.png" alt="SigNoz home — logs, traces, and metrics ingestion all active"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three ticks — &lt;strong&gt;logs, traces, and metrics ingestion active&lt;/strong&gt; — and my service &lt;code&gt;observable-agent&lt;/code&gt; shows up in the services list. That's the whole "did my telemetry land?" question answered in one screen.&lt;/p&gt;




&lt;h2&gt;
  
  
  The agent: a tiny SRE sidekick
&lt;/h2&gt;

&lt;p&gt;The agent answers on-call questions like &lt;em&gt;"Is checkout healthy?"&lt;/em&gt; or &lt;em&gt;"What's my error budget?"&lt;/em&gt;. It has four tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_service_health(service)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list_recent_deploys(service)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calculate_error_budget(slo_percent, actual_error_rate)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search_runbook(topic)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs the classic agent loop: send the question + tool schemas to the model, let the model pick tools, run them, feed results back, and let the model write the final answer. Nothing exotic — which is exactly the point. This is the shape of most "AI agents" in production.&lt;/p&gt;

&lt;p&gt;What makes it &lt;em&gt;observable&lt;/em&gt; is a three-level span tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent.invoke        (SERVER)   ← one per user question
└─ llm.chat         (CLIENT)   ← each model round-trip
└─ tool.&amp;lt;name&amp;gt;      (INTERNAL) ← each tool execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Instrumenting the LLM call the "right" way
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry has &lt;strong&gt;GenAI semantic conventions&lt;/strong&gt; — a standard set of &lt;code&gt;gen_ai.*&lt;/code&gt; attributes so any backend can understand an LLM call. Instead of inventing my own keys, I set the standard ones on each &lt;code&gt;llm.chat&lt;/code&gt; span:&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="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;llm.chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLIENT&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;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;gen_ai.system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&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;gen_ai.operation.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&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;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOL_SCHEMAS&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;gen_ai.response.finish_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;finish_reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# one helper writes tokens + cost to BOTH the span and the metrics:
&lt;/span&gt;    &lt;span class="nf"&gt;record_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;record_llm&lt;/code&gt; helper is where the trick lives — the same numbers land on the span &lt;em&gt;and&lt;/em&gt; on OpenTelemetry metrics in one place:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cost_usd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_tok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;token_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.token.type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;token_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_tok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.token.type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;cost_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;llm_latency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&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_current_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                 &lt;span class="c1"&gt;# the live llm.chat span
&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;gen_ai.usage.input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;in_tok&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;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_tok&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;gen_ai.usage.cost_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So one call feeds two questions: &lt;em&gt;"what happened in this exact request?"&lt;/em&gt; (the span) and &lt;em&gt;"what's happening across all of them?"&lt;/em&gt; (the metrics &lt;code&gt;gen_ai.client.token.usage&lt;/code&gt;, &lt;code&gt;gen_ai.client.cost&lt;/code&gt;, &lt;code&gt;gen_ai.client.operation.duration&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;That's it. No agent framework, no auto-instrumentation magic — just the standard conventions, so SigNoz knows exactly what it's looking at.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on the spec (I checked the docs, as the guide advises).&lt;/strong&gt; The OpenTelemetry GenAI semantic conventions are at &lt;em&gt;Development&lt;/em&gt; stability and recently moved to &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;their own repo&lt;/a&gt;. A few honest deviations in the code above: &lt;code&gt;gen_ai.system&lt;/code&gt; is now &lt;strong&gt;deprecated in favour of &lt;code&gt;gen_ai.provider.name&lt;/code&gt;&lt;/strong&gt;; &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;/&lt;code&gt;output_tokens&lt;/code&gt; are the current names (the &lt;code&gt;prompt_tokens&lt;/code&gt;/&lt;code&gt;completion_tokens&lt;/code&gt; you see are the &lt;em&gt;OpenAI SDK's&lt;/em&gt; field names, which I map onto the span); and &lt;code&gt;gen_ai.response.finish_reasons&lt;/code&gt; is officially &lt;strong&gt;plural&lt;/strong&gt; (a string array). &lt;code&gt;gen_ai.usage.total_tokens&lt;/code&gt;, &lt;code&gt;gen_ai.usage.cost_usd&lt;/code&gt;, and the &lt;code&gt;gen_ai.client.cost&lt;/code&gt; metric are &lt;strong&gt;my own extensions&lt;/strong&gt; — the spec has no cost signal yet. The spec also models &lt;code&gt;gen_ai.client.token.usage&lt;/code&gt; as a Histogram and &lt;code&gt;gen_ai.client.operation.duration&lt;/code&gt; in seconds; I kept a counter and milliseconds for readability. And per the &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md" rel="noopener noreferrer"&gt;span guide&lt;/a&gt; the recommended span name is &lt;code&gt;{operation} {model}&lt;/code&gt; (e.g. &lt;code&gt;chat llama3.2:1b&lt;/code&gt;) — I use &lt;code&gt;llm.chat&lt;/code&gt; because it reads cleaner in a waterfall.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Reading the trace: where did the time actually go?
&lt;/h2&gt;

&lt;p&gt;Here's a single question — &lt;em&gt;"Inventory feels slow. Pull its health, recent deploys, and the right runbook."&lt;/em&gt; — as one distributed trace in SigNoz:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gh4dqo7vx30903kc54m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gh4dqo7vx30903kc54m.png" alt="Trace waterfall: agent.invoke → llm.chat → 3 tools → llm.chat"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Six spans, zero errors. Read top to bottom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;agent.invoke&lt;/code&gt; — the whole request, &lt;strong&gt;1.83 min&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llm.chat&lt;/code&gt; (&lt;strong&gt;17 s&lt;/strong&gt;) — the model reads the question and decides which tools to call.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool.get_service_health&lt;/code&gt;, &lt;code&gt;tool.list_recent_deploys&lt;/code&gt;, &lt;code&gt;tool.search_runbook&lt;/code&gt; — three tools, each &lt;strong&gt;~1 ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llm.chat&lt;/code&gt; (&lt;strong&gt;1.54 min&lt;/strong&gt;) — the model synthesizes the final answer from the tool output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tools are a rounding error. The first LLM call is quick. The &lt;strong&gt;final synthesis call is the whole story&lt;/strong&gt; — and SigNoz labels it for you when you click the span:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbd3m09gyeihj0sxe0j7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbd3m09gyeihj0sxe0j7o.png" alt="Span detail: gen_ai attributes, token counts, cost, and 84.51% of exec time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.54 mins — 84.51% of total exec time.&lt;/strong&gt; Right there in the drawer are the GenAI attributes I set: &lt;code&gt;gen_ai.request.model = llama3.2:1b&lt;/code&gt;, &lt;code&gt;input_tokens = 286&lt;/code&gt;, &lt;code&gt;output_tokens = 120&lt;/code&gt;, &lt;code&gt;total_tokens = 406&lt;/code&gt;, &lt;code&gt;gen_ai.usage.cost_usd&lt;/code&gt;, and &lt;code&gt;finish_reason = stop&lt;/code&gt;. SigNoz even captured the completion text as a span event.&lt;/p&gt;

&lt;p&gt;This is the payoff of the semantic conventions: I didn't build a custom "LLM view." I set standard attributes, and the generic trace UI became an LLM debugger. The lesson — &lt;em&gt;the long-context generation, not the tools, owns your p95&lt;/em&gt; — is the kind of thing you'd never guess from logs alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  From one trace to a fleet: the dashboard
&lt;/h2&gt;

&lt;p&gt;One trace is a debugging tool. To &lt;em&gt;own&lt;/em&gt; the agent I need aggregates, so I built an &lt;strong&gt;LLM &amp;amp; Agent Observability&lt;/strong&gt; dashboard straight from the same span attributes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzlvd8lu3jieta5cv1uyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzlvd8lu3jieta5cv1uyg.png" alt="LLM &amp;amp; Agent Observability dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Across a short load run of varied SRE questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;20 LLM calls · 6,064 input tokens · 1,439 output tokens · $0.00075 total cost&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token usage over time&lt;/strong&gt;, split into input vs. output — input dominates because every tool result is re-fed into the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM latency p95/p99 by model&lt;/strong&gt; — the tail climbs past &lt;strong&gt;1.6 min&lt;/strong&gt; on CPU, which is precisely the cold, uncomfortable truth you want on a dashboard instead of in a user complaint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Span latency p95 by operation&lt;/strong&gt; — the single clearest panel on the board: &lt;code&gt;agent.invoke&lt;/code&gt; and &lt;code&gt;llm.chat&lt;/code&gt; ride high while every &lt;code&gt;tool.*&lt;/code&gt; span sits pinned to the floor. The waterfall lesson, now true across the whole fleet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About that cost: locally it's essentially zero (I compute it from a per-model price table). But the instrumentation is &lt;strong&gt;identical&lt;/strong&gt; to what you'd use against a paid API. Point the provider at &lt;code&gt;openai&lt;/code&gt; or &lt;code&gt;anthropic&lt;/code&gt; instead of &lt;code&gt;ollama&lt;/code&gt; and the same dashboard is now watching real dollars — &lt;em&gt;before&lt;/em&gt; the invoice does.&lt;/p&gt;




&lt;h2&gt;
  
  
  My favorite SigNoz feature
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;trace → span drawer&lt;/strong&gt; won me over. Because SigNoz speaks the OpenTelemetry GenAI conventions natively, a plain span turned into a per-call LLM inspector — model, tokens, cost, finish reason, and the completion event — with &lt;strong&gt;zero custom UI&lt;/strong&gt;. Add the "% of total exec time" on each span and root-causing a slow agent stops being guesswork.&lt;/p&gt;

&lt;p&gt;Runner-up: the metrics query builder let me group the same &lt;code&gt;gen_ai.*&lt;/code&gt; data by model and token type without writing SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instrument the standard, not the vendor.&lt;/strong&gt; OpenTelemetry GenAI attributes made a generic backend understand my agent. No lock-in, no framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents hide their latency in one span.&lt;/strong&gt; My tools were ~1 ms; a single synthesis call was 84.5%. You cannot fix what you cannot see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting SigNoz is a five-line &lt;code&gt;casting.yaml&lt;/code&gt; and one &lt;code&gt;foundryctl cast&lt;/code&gt;.&lt;/strong&gt; Traces, metrics, and logs in one place, on my laptop, for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local models make great observability test rigs.&lt;/strong&gt; CPU inference is slow — which means the interesting tail &lt;em&gt;exists&lt;/em&gt;, so you actually have something to observe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, I'm turning this into a Track 01 project: a self-healing SRE sidekick that reads its own SigNoz traces to decide what to do. But that's another post.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everything here — the agent, the OpenTelemetry wiring, and the dashboard JSON — is reproducible; the commands above are the ones I actually ran.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>observability</category>
      <category>opentelemetry</category>
      <category>signoz</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
