<?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>One governed loop, three SigNoz tracks: an agent that heals, prices its own carbon, and gates accessibility</title>
      <dc:creator>Gajanan Gitte</dc:creator>
      <pubDate>Sat, 25 Jul 2026 13:53:23 +0000</pubDate>
      <link>https://dev.to/gajanangitte/one-governed-loop-three-signoz-tracks-an-agent-that-heals-prices-its-own-carbon-and-gates-2kn9</link>
      <guid>https://dev.to/gajanangitte/one-governed-loop-three-signoz-tracks-an-agent-that-heals-prices-its-own-carbon-and-gates-2kn9</guid>
      <description>&lt;p&gt;Every "AI plus observability" demo I had seen stopped in the same place. A&lt;br&gt;
beautiful trace of the thing going wrong. The dashboard lights up, the tokens are&lt;br&gt;
gone, and then a human reads it and fixes it by hand.&lt;/p&gt;

&lt;p&gt;Observability tells you the agent is on fire. It does not put the fire out.&lt;/p&gt;

&lt;p&gt;So for the Agents of SigNoz hackathon I built &lt;code&gt;observable-agent&lt;/code&gt;, a local model on&lt;br&gt;
my own laptop wired to self hosted SigNoz, and I set myself one rule. Do not just&lt;br&gt;
watch the agent. Act on its own telemetry, safely, and prove the fix in telemetry&lt;br&gt;
afterwards. Then I pushed that single idea across all three tracks, because the&lt;br&gt;
same primitives that heal a reliability breach also price its carbon and gate its&lt;br&gt;
accessibility.&lt;/p&gt;

&lt;p&gt;Here is the whole project, and exactly how SigNoz holds it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The primitives, and the foundation they sit on
&lt;/h2&gt;

&lt;p&gt;The design rests on three moves that repeat in every track:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Record raw facts as OpenTelemetry, never a summary.&lt;/li&gt;
&lt;li&gt;Grade those facts with a fail closed, three state verdict (PASS, BREACH, or
UNKNOWN, never a comforting false zero).&lt;/li&gt;
&lt;li&gt;Alert on the verdict in SigNoz, and where it is safe, act on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The foundation is deliberately boring and reproducible. SigNoz v0.133 runs on my&lt;br&gt;
laptop, stood up with Foundry (WSL2 plus Docker), and the repo ships the exact&lt;br&gt;
&lt;code&gt;casting.yaml&lt;/code&gt; and &lt;code&gt;casting.yaml.lock&lt;/code&gt; so a judge can re-run the same deployment.&lt;br&gt;
The agent runs on a local Ollama model (&lt;code&gt;qwen2.5:3b&lt;/code&gt;), no cloud and no API keys.&lt;br&gt;
Every request emits an &lt;code&gt;agent.invoke&lt;/code&gt; to &lt;code&gt;llm.chat&lt;/code&gt; to &lt;code&gt;tool.*&lt;/code&gt; span tree using the&lt;br&gt;
OpenTelemetry GenAI semantic conventions, with tokens, cost, and latency dual&lt;br&gt;
written to metrics.&lt;/p&gt;

&lt;p&gt;In the loop, SigNoz plays four roles, and because all four read the same&lt;br&gt;
telemetry, they share one source of truth: sensor, diagnostic surface (through the&lt;br&gt;
SigNoz MCP server), scoreboard, and trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track 01: the self healing SRE sidekick
&lt;/h2&gt;

&lt;p&gt;The first breach I chased is the retry tax. A fault drops the first completed LLM&lt;br&gt;
response, so the agent runs the inference again and spends the tokens twice. It is&lt;br&gt;
a perfect target: it is real telemetry (a duplicate &lt;code&gt;llm.chat&lt;/code&gt; span tagged&lt;br&gt;
&lt;code&gt;retry.reason = "response_dropped"&lt;/code&gt;), it is measurable as an SLO (dropped divided&lt;br&gt;
by total per cohort), and there is more than one valid fix, so the model has a&lt;br&gt;
genuine decision to make.&lt;/p&gt;

&lt;p&gt;One command runs the whole thing, and the entire cycle is a single distributed&lt;br&gt;
trace.&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%2Fxium8a5hvl4sqdl1dcym.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%2Fxium8a5hvl4sqdl1dcym.png" alt="The agent.heal closed loop as one trace" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Detection and verification are deterministic &lt;code&gt;signoz_aggregate_traces&lt;/code&gt; queries. No&lt;br&gt;
LLM is involved in judging. The model is only asked to decide what to do about a&lt;br&gt;
confirmed breach. Its first move is a &lt;code&gt;read_incident&lt;/code&gt; tool call, backed by two live&lt;br&gt;
queries over the MCP server, and the structured evidence it gets back (a retry&lt;br&gt;
rate, a &lt;code&gt;dropped_and_retried&lt;/code&gt; count, and a root cause) comes straight out of the&lt;br&gt;
traces the workload had just emitted.&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%2Fcslx0yacadmh8e9o7tj0.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%2Fcslx0yacadmh8e9o7tj0.png" alt="read_incident: evidence pulled from SigNoz via MCP" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given that evidence, the local model chose &lt;code&gt;disable_fault_injection&lt;/code&gt; on its own and&lt;br&gt;
called it. Retry rate went from 40 percent to 0 percent, and every number in that&lt;br&gt;
sentence is a SigNoz query, not a print statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real alert is the trigger, not a cron job
&lt;/h3&gt;

&lt;p&gt;The hands off path does not poll on a timer. A real SigNoz alert rule fires the&lt;br&gt;
heal. When the retry rate crosses the SLO, SigNoz flips the rule to firing, a&lt;br&gt;
bridge catches it over an Alertmanager webhook, opens a &lt;code&gt;heal.trigger&lt;/code&gt; span, and&lt;br&gt;
launches the governed loop. The same alert flipping back to resolved is how the&lt;br&gt;
loop knows it is done. The alert and the heal are one distributed trace.&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%2Fp8zaudh49xuel7s5m4yp.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%2Fp8zaudh49xuel7s5m4yp.png" alt="The SigNoz alert that triggers the heal, then resolves" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The part that makes it safe: a policy gate
&lt;/h3&gt;

&lt;p&gt;Letting software act on production is only reasonable if the action is&lt;br&gt;
constrained, and this is the bit most autonomous agent demos skip. Every actuator&lt;br&gt;
mutation routes through a policy gate first. Autonomy runs on four levels (observe,&lt;br&gt;
suggest, approve, auto), and each action carries a risk, a reversibility, and a&lt;br&gt;
blast radius. In auto with a low risk cap, a low risk reversible action applies&lt;br&gt;
itself, while a riskier action like &lt;code&gt;switch_model&lt;/code&gt; is held for a human. Every&lt;br&gt;
decision writes an audit line as a &lt;code&gt;heal.policy&lt;/code&gt; metric, and if an applied fix does&lt;br&gt;
not clear the breach, the loop restores a snapshot it took beforehand.&lt;/p&gt;

&lt;p&gt;The second incident shows the gate earning its keep. A runaway agent stuck in a&lt;br&gt;
loop runs up an unbounded bill. The sensor measures calls per request from the&lt;br&gt;
traces against an SLO ceiling of 6, the model reads the runaway from SigNoz and&lt;br&gt;
arms a &lt;code&gt;set_cost_budget&lt;/code&gt; circuit breaker, and here is my favourite frame of the&lt;br&gt;
whole project: the entire policy decision recorded on 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%2Fyd2m2ak27nzip4gxjqha.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%2Fyd2m2ak27nzip4gxjqha.png" alt="set_cost_budget: the action the model chose, cleared by the policy gate" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That breaker is a structural cut, not a warning. When a request crosses the budget&lt;br&gt;
the agent stops issuing LLM calls and tags the span &lt;code&gt;agent.request.severed = true&lt;/code&gt;.&lt;br&gt;
Spend per request dropped from 0.000700 dollars to 0.000122 dollars, about an 82&lt;br&gt;
percent cut, which the plug and play economics layer prices at roughly 1,734&lt;br&gt;
dollars a month at 100,000 requests a day.&lt;/p&gt;

&lt;p&gt;One more thing the loop learns: the fix is written to a verified memory in SigNoz,&lt;br&gt;
so the next time the same incident recurs it heals from memory with no model call&lt;br&gt;
at all, which is both faster and calibration free. Running it again while writing&lt;br&gt;
this, a fresh 33 percent retry breach healed straight from memory (proven four&lt;br&gt;
times over now), no model call, MTTR 149 seconds, trace&lt;br&gt;
&lt;code&gt;d0c14392999aef86af95032a8dd080e0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track 02: an MCP Contract Lab, three signals, and a zero code proxy
&lt;/h2&gt;

&lt;p&gt;Track 02 is about instrumentation and dashboards, so I aimed the same primitives at&lt;br&gt;
the Model Context Protocol itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp2_cert&lt;/code&gt; is an active probe that certifies an MCP server against a contract. It&lt;br&gt;
exercises &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, and &lt;code&gt;tools/call&lt;/code&gt;, grades each result with the&lt;br&gt;
same three state verdict, and emits every exchange as an &lt;code&gt;mcp.*&lt;/code&gt; span with a stable&lt;br&gt;
fingerprint. Against my SigNoz MCP server it returns CERTIFIED with fingerprint&lt;br&gt;
&lt;code&gt;ca5859fcf24f5e03&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I built the passive companion: an always on instrumentation proxy&lt;br&gt;
(&lt;code&gt;mcp-proxy&lt;/code&gt;). It sits transparently between any MCP client and server and turns&lt;br&gt;
their JSON RPC traffic into &lt;code&gt;mcp.&amp;lt;method&amp;gt;&lt;/code&gt; spans and &lt;code&gt;mcp.client.*&lt;/code&gt; metrics with&lt;br&gt;
zero code changes to either side. To prove it is faithful, I drove the full&lt;br&gt;
certification battery through the proxy. The result was still CERTIFIED with the&lt;br&gt;
identical fingerprint &lt;code&gt;ca5859fcf24f5e03&lt;/code&gt;, while the proxy independently emitted&lt;br&gt;
correctly graded spans for every call. You can instrument an MCP integration you do&lt;br&gt;
not own, and lose nothing.&lt;/p&gt;

&lt;p&gt;All of that lands on a three signal dashboard (traces, metrics, and logs together)&lt;br&gt;
built through the SigNoz v5 Query Builder API. Every panel self verifies against&lt;br&gt;
the &lt;code&gt;query_range&lt;/code&gt; API before the dashboard is created, so a panel that cannot&lt;br&gt;
resolve never ships.&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%2Fsyex9g4ehu9irseaxgmn.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%2Fsyex9g4ehu9irseaxgmn.png" alt="One of the self verifying SigNoz dashboards" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Track 03: energy, carbon, and accessibility as telemetry
&lt;/h2&gt;

&lt;p&gt;Track 03 says observe anything, so I observed two things nobody usually treats as&lt;br&gt;
telemetry.&lt;/p&gt;

&lt;p&gt;WattTrace GreenOps measures joules and grams of CO2e per verified answer. It prices&lt;br&gt;
every token to energy on a deterministic token basis, folds a run to its weakest&lt;br&gt;
provenance (so a fallback estimate is never shown as a hardware reading), and&lt;br&gt;
grades it fail closed against a budget. Running a control cohort against a retry&lt;br&gt;
fault, the same verified answers cost 753 J against 1101 J, a 46 percent energy&lt;br&gt;
regression, most of it pure waste on the retried calls. The retry tax, reframed as&lt;br&gt;
a sustainability regression you can alert on.&lt;/p&gt;

&lt;p&gt;AccessTrace applies the exact same fail closed primitives to web accessibility. A&lt;br&gt;
real headless browser walks a WCAG journey (page load, navigation, main content,&lt;br&gt;
signup form), runs axe-core scoped at each stage, and emits the journey as a trace:&lt;br&gt;
&lt;code&gt;access.suite&lt;/code&gt; to &lt;code&gt;access.journey&lt;/code&gt; to &lt;code&gt;access.step&lt;/code&gt;. The killer view is that the&lt;br&gt;
&lt;code&gt;access.step&lt;/code&gt; spans localise which part of the user flow breaks. On a broken demo&lt;br&gt;
page the verdict is BREACH with a weighted debt of 40 (two critical and four&lt;br&gt;
serious issues, at the exact failing stage), and its fixed twin scores 0. That is a&lt;br&gt;
100 percent reduction, and two live SigNoz alerts fire on the breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cross track finale: carbon becomes a heal sensor
&lt;/h2&gt;

&lt;p&gt;This is where the three tracks stop being three projects. The WattTrace energy&lt;br&gt;
verdict from Track 03 becomes a heal sensor for the Track 01 self healer, read over&lt;br&gt;
the Track 02 MCP protocol. The self healer detects a carbon SLO breach, defined as&lt;br&gt;
the share of a cohort's inference energy wasted on dropped and retried calls, and&lt;br&gt;
closes it through the same governed loop.&lt;/p&gt;

&lt;p&gt;The calibration behind that is the interesting part. A healthy multi call workload&lt;br&gt;
sits around 867 J per answer, only about 4 percent under the 900 J single call&lt;br&gt;
reference budget, so gating the verify step on the absolute cap would roll back a&lt;br&gt;
perfectly good fix on ordinary token noise. Gating on wasted fraction instead is&lt;br&gt;
calibration free: a healthy cohort wastes zero, so a verified fix is never rolled&lt;br&gt;
back. In a live carbon heal the footprint per answer dropped from 0.3275 gCO2e to&lt;br&gt;
0.1183 gCO2e (2649 J to 958 J), and the fix was learned to memory. One loop, three&lt;br&gt;
tracks, one trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I used SigNoz, concretely
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Traces: the full &lt;code&gt;agent.heal&lt;/code&gt;, &lt;code&gt;watt.suite&lt;/code&gt;, &lt;code&gt;access.suite&lt;/code&gt;, and &lt;code&gt;mcp.*&lt;/code&gt; span
trees, every step correlated under one trace id.&lt;/li&gt;
&lt;li&gt;Metrics: &lt;code&gt;heal.slo.breach&lt;/code&gt;, &lt;code&gt;heal.mttr&lt;/code&gt;, &lt;code&gt;heal.policy&lt;/code&gt;,
&lt;code&gt;watttrace.energy.consumed&lt;/code&gt;, &lt;code&gt;watttrace.carbon.emitted&lt;/code&gt;, and more, dual written
alongside the traces.&lt;/li&gt;
&lt;li&gt;Logs: trace correlated, structured heal lifecycle logs, so the loop shows up in
all three signals.&lt;/li&gt;
&lt;li&gt;Query Builder v5: every detector and every dashboard panel is a programmatic
SigNoz &lt;code&gt;query_range&lt;/code&gt; query. Detection, diagnosis, and verification are all just
SigNoz queries, so the loop is built on the query surface, not merely pointed at
it.&lt;/li&gt;
&lt;li&gt;Dashboards: five importable dashboards (self healing, three signal reliability,
MCP Contract Lab, WattTrace GreenOps, and AccessTrace), each self verified before
creation, with their uuids checked into the repo.&lt;/li&gt;
&lt;li&gt;Alerts: ten live rules across the tracks, and a real alert is the trigger for the
heal, not just a notification.&lt;/li&gt;
&lt;li&gt;The SigNoz MCP server: the model reads its own incidents through it, and the
proxy instruments it.&lt;/li&gt;
&lt;li&gt;Service accounts and API keys for programmatic access, plus the Foundry
&lt;code&gt;casting.yaml&lt;/code&gt; and &lt;code&gt;casting.yaml.lock&lt;/code&gt; so the whole thing reproduces.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;p&gt;The hackathon asks for real experience, so here is what actually bit me. Small&lt;br&gt;
models need a firm hand to tool call: &lt;code&gt;llama3.2:3b&lt;/code&gt; kept emitting a malformed tool&lt;br&gt;
call, so I moved to &lt;code&gt;qwen2.5:3b&lt;/code&gt;, and even then the prompt has to spell out that&lt;br&gt;
&lt;code&gt;read_incident&lt;/code&gt; only reads and must be followed by a remediation. Keep the LLM out&lt;br&gt;
of the reliability hot path: detection and verification stay deterministic, the&lt;br&gt;
model only decides, and SigNoz judges. The observer effect is real, so the healer&lt;br&gt;
and the workload run as separate services and every query is scoped to a single&lt;br&gt;
cohort tag. CPU inference is genuinely slow, 10 to 110 seconds per call, which&lt;br&gt;
means the latency tail is real and MTTR actually means something. And everything is&lt;br&gt;
fail closed: a run that cannot be judged is UNKNOWN, never a false PASS, because a&lt;br&gt;
zero energy reading is a broken measurement, not free work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;It is one command per incident, the repo ships the exact &lt;code&gt;casting.yaml&lt;/code&gt; so you can&lt;br&gt;
stand up the same SigNoz deployment, and 234 network free unit tests cover the pure&lt;br&gt;
grading logic offline:&lt;br&gt;
&lt;a href="https://github.com/gajanangitte/observable-agent" rel="noopener noreferrer"&gt;github.com/gajanangitte/observable-agent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Observability that acts, across reliability, protocol, carbon, and accessibility.&lt;br&gt;
It turns out the hard part was never the acting. It was proving, in telemetry, that&lt;br&gt;
the fire is out, the bill is capped, the carbon is cut, and the front door is open&lt;br&gt;
to everyone.&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>ai</category>
      <category>observability</category>
    </item>
    <item>
      <title>The agent that heals itself: closing the loop with SigNoz</title>
      <dc:creator>Gajanan Gitte</dc:creator>
      <pubDate>Sun, 19 Jul 2026 06:17:17 +0000</pubDate>
      <link>https://dev.to/gajanangitte/the-agent-that-heals-itself-closing-the-loop-with-signoz-2i34</link>
      <guid>https://dev.to/gajanangitte/the-agent-that-heals-itself-closing-the-loop-with-signoz-2i34</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the fourth and final part of a series where I gave a local Llama agent&lt;br&gt;
real observability with SigNoz running on my own laptop. First I gave it&lt;br&gt;
&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;OpenTelemetry eyes&lt;/a&gt;,&lt;br&gt;
then I measured &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;the retry tax&lt;/a&gt;&lt;br&gt;
its silent retries cost, then I wired in the&lt;br&gt;
&lt;a href="https://dev.to/gajanangitte/i-gave-my-local-llama-agent-the-signoz-mcp-server-and-asked-it-to-debug-itself-5909"&gt;SigNoz MCP server so it could read its own traces&lt;/a&gt;.&lt;br&gt;
This time it does more than watch itself. It heals itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every "AI plus observability" demo I had seen stopped in the same place: a&lt;br&gt;
beautiful trace of the thing going wrong. The dashboard lights up, the tokens are&lt;br&gt;
gone, and then a human reads it and fixes it by hand.&lt;/p&gt;

&lt;p&gt;Observability tells you the agent is on fire. It does not put the fire out.&lt;/p&gt;

&lt;p&gt;So for the Agents of SigNoz hackathon I set myself a harder goal. Close the loop.&lt;br&gt;
Detect a reliability breach, diagnose it, act on it, and then prove the fix worked.&lt;br&gt;
All with a local model, all on a laptop, and all grounded in SigNoz rather than the&lt;br&gt;
model's word. No cloud, no API keys.&lt;/p&gt;

&lt;p&gt;Here is what I learned building it.&lt;/p&gt;
&lt;h2&gt;
  
  
  SigNoz as the sensor, not the dashboard
&lt;/h2&gt;

&lt;p&gt;The whole design rests on one idea. SigNoz plays three roles in a single loop, and&lt;br&gt;
because all three read the same telemetry, they share one source of truth.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sensor. A detector asks SigNoz "did this rollout breach the SLO?" with a
&lt;code&gt;signoz_aggregate_traces&lt;/code&gt; query. Deterministic. No LLM involved.&lt;/li&gt;
&lt;li&gt;Diagnostic surface. When there is a breach, the model reads the incident
evidence out of the same traces, through the SigNoz MCP server.&lt;/li&gt;
&lt;li&gt;Scoreboard. After the fix, the loop queries SigNoz again. Breach cleared means
healed, and that is what sets the MTTR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is the one I care about most. Detection and verification are&lt;br&gt;
deterministic SigNoz queries. The model is only asked to decide what to do about a&lt;br&gt;
confirmed breach. If it hallucinates, the decision might be wrong, but the breach&lt;br&gt;
and the healed verdict are always facts pulled from telemetry. That is the line&lt;br&gt;
between a party trick and something you would actually let run.&lt;/p&gt;

&lt;p&gt;Two services show up in SigNoz, exactly as they would in production: the&lt;br&gt;
&lt;code&gt;observable-agent&lt;/code&gt; workload (the thing that gets sick) and the &lt;code&gt;self-healer&lt;/code&gt; (the&lt;br&gt;
loop that watches it, decides, and acts).&lt;/p&gt;
&lt;h2&gt;
  
  
  Incident number one: the retry tax
&lt;/h2&gt;

&lt;p&gt;The first breach I chased is the retry tax from part two of the series. A fault&lt;br&gt;
drops the first completed LLM response, so the agent runs the inference again and&lt;br&gt;
spends the tokens twice. It is a perfect target for self healing. It is real&lt;br&gt;
telemetry (a duplicate &lt;code&gt;llm.chat&lt;/code&gt; span tagged &lt;code&gt;retry.reason = "response_dropped"&lt;/code&gt;),&lt;br&gt;
it is measurable as an SLO (dropped divided by total per cohort), and there is more&lt;br&gt;
than one way to fix it, so the model has a genuine decision to make.&lt;/p&gt;

&lt;p&gt;One command, &lt;code&gt;python self_heal.py&lt;/code&gt;, runs the whole thing, and the entire cycle is a&lt;br&gt;
single distributed trace.&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%2Fxium8a5hvl4sqdl1dcym.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%2Fxium8a5hvl4sqdl1dcym.png" alt="The agent.heal closed loop as one trace" width="799" height="571"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent.heal                                 2.93 min   service = self-healer
  heal.canary.pre     32.9s   BREAK IT    roll out the workload under the fault
  heal.detect          2.2s   DETECT      3x signoz_aggregate_traces (via MCP)
  heal.decide         51.7s   DIAGNOSE and DECIDE  (local qwen2.5:3b)
     llm.chat            26.5s
     tool.read_incident   1.6s   2x signoz_aggregate_traces  (evidence via MCP)
     llm.chat            13.4s
     tool.disable_fault_injection   2ms   the remediation, chosen by the model
  heal.canary.post    86.0s   VERIFY      roll out again under the fixed config
  heal.verify          2.3s   3x signoz_aggregate_traces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model's first move is a &lt;code&gt;read_incident&lt;/code&gt; tool call, backed by two live&lt;br&gt;
&lt;code&gt;signoz_aggregate_traces&lt;/code&gt; calls over MCP. The structured evidence it gets back,&lt;br&gt;
retry rate &lt;code&gt;0.333&lt;/code&gt;, &lt;code&gt;dropped_and_retried: 2&lt;/code&gt;, and a root cause, comes straight out&lt;br&gt;
of the traces the workload had just emitted.&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%2Fcslx0yacadmh8e9o7tj0.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%2Fcslx0yacadmh8e9o7tj0.png" alt="read_incident: evidence pulled from SigNoz via MCP" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given that, &lt;code&gt;qwen2.5:3b&lt;/code&gt; chose &lt;code&gt;disable_fault_injection&lt;/code&gt; on its own and called it.&lt;br&gt;
Retry rate went from 40% to 0%, MTTR 141 seconds, and every number in that sentence&lt;br&gt;
is a SigNoz query, not a print statement.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that makes it safe: a policy gate
&lt;/h2&gt;

&lt;p&gt;Letting software act on production is only reasonable if the action is constrained.&lt;br&gt;
This is the bit most "autonomous agent" demos skip, and it is the bit I think&lt;br&gt;
actually matters. So every actuator mutation routes through a policy gate before it&lt;br&gt;
can touch anything.&lt;/p&gt;

&lt;p&gt;Autonomy levels: &lt;code&gt;observe&lt;/code&gt;, &lt;code&gt;suggest&lt;/code&gt;, &lt;code&gt;approve&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;. In &lt;code&gt;auto&lt;/code&gt; the gate applies&lt;br&gt;
changes itself. In &lt;code&gt;approve&lt;/code&gt; it holds them for a human. In &lt;code&gt;suggest&lt;/code&gt; or &lt;code&gt;observe&lt;/code&gt; it&lt;br&gt;
only proposes.&lt;/p&gt;

&lt;p&gt;Per action risk, reversibility, and blast radius. In &lt;code&gt;auto&lt;/code&gt; with a low risk cap, low&lt;br&gt;
risk reversible actions apply automatically. A riskier action like &lt;code&gt;switch_model&lt;/code&gt; is&lt;br&gt;
held for approval instead of being forced through.&lt;/p&gt;

&lt;p&gt;An audit line per decision: allowed, held, or denied, with the reason, recorded as a&lt;br&gt;
&lt;code&gt;heal.policy&lt;/code&gt; metric.&lt;/p&gt;

&lt;p&gt;And when an applied fix does not clear the breach, the loop rolls back. It snapshots&lt;br&gt;
the control plane before each attempt and restores it on a failed verify. The retry&lt;br&gt;
incident heals on the first try, so it never needs the rollback, but the machinery&lt;br&gt;
is there, and it is what separates acting from acting safely.&lt;/p&gt;
&lt;h2&gt;
  
  
  Incident number two: the bill shock kill switch
&lt;/h2&gt;

&lt;p&gt;The retry tax wastes tokens a few at a time. The failure that actually scares&lt;br&gt;
finance teams is a runaway agent, one stuck in a loop, issuing LLM call after LLM&lt;br&gt;
call, running up an unbounded bill. Same governed loop, a different sensor and a&lt;br&gt;
different fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python self_heal.py &lt;span class="nt"&gt;--scenario&lt;/span&gt; cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sensor measures calls per request from the traces, with an SLO ceiling of 6. The&lt;br&gt;
model read the runaway from SigNoz, "28 &lt;code&gt;llm.chat&lt;/code&gt; calls across 2 requests, 14 per&lt;br&gt;
request, versus a max of 6", and chose &lt;code&gt;set_cost_budget&lt;/code&gt;, arming a per request cost&lt;br&gt;
circuit breaker.&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%2Fcmnwefs2adluvxxrxhs3.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%2Fcmnwefs2adluvxxrxhs3.png" alt="read_incident: the cost diagnosis pulled from SigNoz via MCP" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That breaker is a real structural cut, not a warning. When a request's cumulative&lt;br&gt;
cost crosses the budget, the agent stops issuing LLM calls and tags the span&lt;br&gt;
&lt;code&gt;agent.request.severed = true&lt;/code&gt;. Spend per request dropped from $0.000700 to&lt;br&gt;
$0.000123, about an 82% cut, calls per request from 13.5 to 2.5, MTTR 177 seconds.&lt;/p&gt;

&lt;p&gt;And here is my favourite frame of the whole project, the &lt;code&gt;set_cost_budget&lt;/code&gt; span,&lt;br&gt;
with the entire policy decision recorded right on it.&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%2Fyd2m2ak27nzip4gxjqha.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%2Fyd2m2ak27nzip4gxjqha.png" alt="set_cost_budget: the action the model chose, cleared by the policy gate" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;heal.policy.risk = low&lt;/code&gt;, &lt;code&gt;heal.policy.reversible = true&lt;/code&gt;, &lt;code&gt;heal.policy.autonomy =&lt;br&gt;
auto&lt;/code&gt;, so &lt;code&gt;heal.policy.allow = true&lt;/code&gt;. The fix is low risk and reversible, so the gate&lt;br&gt;
lets it through automatically. A riskier action would have stopped and waited for a&lt;br&gt;
human. That is the difference between an agent that can act and one you would let.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest bits
&lt;/h2&gt;

&lt;p&gt;The hackathon asks for real experience, so here is what actually bit me.&lt;/p&gt;

&lt;p&gt;Small models need a firm hand to tool call. &lt;code&gt;llama3.2:3b&lt;/code&gt; kept emitting a malformed&lt;br&gt;
tool call. &lt;code&gt;qwen2.5:3b&lt;/code&gt;, the same size, does reliable OpenAI format tool calling on&lt;br&gt;
Ollama, so I switched. Even then, the prompt has to spell out that &lt;code&gt;read_incident&lt;/code&gt;&lt;br&gt;
only reads and must be followed by a remediation, and the output token cap has to&lt;br&gt;
leave room for the decision and the tool call, or the remediation gets truncated.&lt;/p&gt;

&lt;p&gt;Keep the LLM out of the reliability hot path. Detection and verification are&lt;br&gt;
deterministic. The model decides. SigNoz judges.&lt;/p&gt;

&lt;p&gt;The observer effect is real. The healer's own spans would pollute the workload's SLO&lt;br&gt;
math, so the two run as separate services and every query is scoped to a single&lt;br&gt;
cohort tag.&lt;/p&gt;

&lt;p&gt;CPU inference is slow, and that is honest. Each &lt;code&gt;llm.chat&lt;/code&gt; runs 10 to 110 seconds on&lt;br&gt;
CPU, so a full heal takes a few minutes. The latency tail is real, which means there&lt;br&gt;
is genuinely something to observe, and MTTR means something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it on SigNoz
&lt;/h2&gt;

&lt;p&gt;I could not have built this on a dashboard I could only look at. I needed a backend I&lt;br&gt;
could query programmatically, inside the loop, and one the model could reach through&lt;br&gt;
a standard interface. SigNoz gave me both, the trace store and the MCP server,&lt;br&gt;
runnable on my own laptop, for free. Detection, diagnosis, and verification are all&lt;br&gt;
just SigNoz queries. The loop is built on SigNoz's query surface, not merely pointed&lt;br&gt;
at it.&lt;/p&gt;

&lt;p&gt;If you want to run it, it is one command per incident, and the repo ships the exact&lt;br&gt;
&lt;code&gt;casting.yaml&lt;/code&gt; so you can stand up the same SigNoz deployment:&lt;br&gt;
&lt;a href="https://github.com/gajanangitte/observable-agent" rel="noopener noreferrer"&gt;github.com/gajanangitte/observable-agent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Observability that acts. Turns out the hard part is not the acting. It is proving, in&lt;br&gt;
telemetry, that the fire is actually out.&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>ai</category>
      <category>sre</category>
    </item>
    <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 rediscovering, 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." Those numbers 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. 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;. 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). This is 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 inferring again, dropped responses, guardrail rejects, 429 backoffs, 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 invokes the model again, 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;a retry you do not observe 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 token counts for the dropped attempt, 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;, with no OpenAI bill and 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. That 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>
