<?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: Nischay Sood</title>
    <description>The latest articles on DEV Community by Nischay Sood (@nischay_sood_127f4236b817).</description>
    <link>https://dev.to/nischay_sood_127f4236b817</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%2F4035561%2Fbae1002b-267b-4664-a1f0-b0843c1a41e6.png</url>
      <title>DEV Community: Nischay Sood</title>
      <link>https://dev.to/nischay_sood_127f4236b817</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nischay_sood_127f4236b817"/>
    <language>en</language>
    <item>
      <title>Using Sygnoz for my Project</title>
      <dc:creator>Nischay Sood</dc:creator>
      <pubDate>Fri, 24 Jul 2026 11:04:39 +0000</pubDate>
      <link>https://dev.to/nischay_sood_127f4236b817/using-sygnoz-for-my-project-4276</link>
      <guid>https://dev.to/nischay_sood_127f4236b817/using-sygnoz-for-my-project-4276</guid>
      <description>&lt;h1&gt;
  
  
  Testing AI Agents by Reading Their Traces: Building TraceCheck on Self-Hosted SigNoz
&lt;/h1&gt;

&lt;p&gt;"I have successfully issued a refund for order 123." Confident, polite, completely correct-sounding. Also, in this particular run, the agent had just called &lt;code&gt;lookup_order&lt;/code&gt; six times in a row before giving up and doing it anyway — a hard loop I'd seeded on purpose to prove a point. If I'd only been grading that final sentence, I would never have known.&lt;/p&gt;

&lt;p&gt;That's the exact problem I built TraceCheck to solve for the Agents of SigNoz hackathon: a testing platform where OpenTelemetry traces stored in self-hosted SigNoz are the ground truth for whether an AI agent actually behaved correctly, not just whether it said something plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap I kept running into
&lt;/h2&gt;

&lt;p&gt;Most ways of testing an LLM agent boil down to: send it an input, check the output. That's black-box testing, and it has a real blind spot. An agent can call the same tool in a loop, skip a step it was supposed to do, or quietly blow through a token budget — and still land on a reply that reads fine. The failure happens in the &lt;em&gt;process&lt;/em&gt;, not the final text, and if you never look at the process, you never catch it.&lt;/p&gt;

&lt;p&gt;I'd been reading about OpenTelemetry and figured: if every LLM call and every tool call gets recorded as a span, and SigNoz stores that trace, then a test runner could pull the trace back out and check the actual span tree — which tools ran, in what order, how many times, how many tokens, how long it took — right alongside the usual output check. White-box and black-box assertions, from the same real trace. That's TraceCheck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting SigNoz running was its own lesson
&lt;/h2&gt;

&lt;p&gt;Before any of the actual testing logic, I had to get a real agent sending real spans into a real SigNoz instance, self-hosted via Foundry per the hackathon rules. This alone taught me more than I expected.&lt;/p&gt;

&lt;p&gt;First attempt: &lt;code&gt;foundryctl cast -f casting.yaml&lt;/code&gt; died at almost exactly 300 seconds, twice, pulling different amounts of Docker images each time. That consistency (same timeout, different progress) told me it wasn't a flaky network — it was a hardcoded timeout on the pull step. Fix: pre-pull the five images (&lt;code&gt;signoz/signoz&lt;/code&gt;, &lt;code&gt;signoz/signoz-otel-collector&lt;/code&gt;, &lt;code&gt;clickhouse/clickhouse-server&lt;/code&gt;, &lt;code&gt;clickhouse/clickhouse-keeper&lt;/code&gt;, &lt;code&gt;postgres:16&lt;/code&gt;) manually with plain &lt;code&gt;docker pull&lt;/code&gt;, which has no such limit. Once cached, &lt;code&gt;cast&lt;/code&gt; finished in about 20 seconds.&lt;/p&gt;

&lt;p&gt;Second lesson, this time on the agent side: Gemini's free tier gave me &lt;code&gt;Quota exceeded ... limit: 0&lt;/code&gt; even with a freshly generated key. Turns out "free tier" in the current Gemini API means something specific — you still need billing linked to that exact Google Cloud project to get real (if free) quota, not just a verified account. Once I linked billing at &lt;code&gt;aistudio.google.com/spend&lt;/code&gt;, the zero-quota errors disappeared instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent actually looks like
&lt;/h2&gt;

&lt;p&gt;The demo agent is a small customer-support bot with three tools — &lt;code&gt;lookup_order&lt;/code&gt;, &lt;code&gt;search_faq&lt;/code&gt;, &lt;code&gt;issue_refund&lt;/code&gt; — using a plain manual tool-calling loop against Gemini (&lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;, after two other model names failed for two unrelated reasons — always check a provider's live model list, not a six-month-old blog post). Every LLM call and tool call is wrapped in an OpenTelemetry 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.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;as&lt;/span&gt; &lt;span class="n"&gt;llm_span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&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;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&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;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&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_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;llm_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;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_token_count&lt;/span&gt; &lt;span class="ow"&gt;or&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;llm_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;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;candidates_token_count&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using OpenTelemetry's standard &lt;code&gt;gen_ai.*&lt;/code&gt; attribute names (instead of inventing my own) means SigNoz recognizes these as LLM-call spans out of the box, no custom mapping needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing tests as YAML, grading against the real trace
&lt;/h2&gt;

&lt;p&gt;A test scenario is just plain YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;scenario&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;#123"&lt;/span&gt;
&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;want&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refund&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;123,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;arrived&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;broken"&lt;/span&gt;
&lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tool_called&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lookup_order&lt;/span&gt;
      &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;issue_refund&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;max_tool_repeats&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;max_total_tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;max_duration_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30000&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;judge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confirms&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refund&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;apologizes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;broken&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner sends the input to the real agent, gets back a trace ID, and polls SigNoz's &lt;code&gt;/api/v5/query_range&lt;/code&gt; API until the trace actually lands (spans are batch-exported, so there's a short delay). Then it checks the real span tree against every rule — did &lt;code&gt;lookup_order&lt;/code&gt; happen before &lt;code&gt;issue_refund&lt;/code&gt;, did any tool repeat past the limit, what were the total tokens, how long did it take — plus one black-box check, an LLM-as-judge call grading the final reply against a plain-English rubric.&lt;/p&gt;

&lt;p&gt;To prove the assertions actually catch real failures and not just pass trivially, I kept a correct agent on &lt;code&gt;main&lt;/code&gt; and created three branches, each with one deliberately seeded bug:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bug/tool-loop&lt;/code&gt;: a prompt change that made the agent re-verify the same order repeatedly. Real result: &lt;code&gt;lookup_order&lt;/code&gt; called 6 times against a limit of 2 — caught, while the final reply still read completely normally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bug/skips-lookup&lt;/code&gt;: told the agent the lookup step was "optional." Result: it skipped straight to &lt;code&gt;issue_refund&lt;/code&gt;, caught by the tool-ordering assertion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bug/token-blowup&lt;/code&gt;: padded the system prompt with ~4,400 tokens of filler. Result: total usage jumped to 10,059 tokens against an 8,000 limit — caught, with every other assertion still passing, isolating exactly the one thing that broke.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The CI pipeline taught me the most
&lt;/h2&gt;

&lt;p&gt;Wiring this into GitHub Actions — so a real regression blocks a real pull request — turned out to be the hardest part, and where I learned the most about how SigNoz's auth actually works.&lt;/p&gt;

&lt;p&gt;My first approach created a service account and API key via SigNoz's API for the CI run. It kept hitting &lt;code&gt;403 Forbidden&lt;/code&gt; on the trace-query endpoint. Root cause: a freshly created service account has zero permissions until a role is assigned, normally done by hand in the SigNoz UI — something I couldn't replicate over the API on a brand-new, scripted instance. The fix was simpler than I expected: use the logged-in admin's JWT directly as a bearer token, since that's the exact same credential the SigNoz UI itself uses when you browse the Traces page. No role assignment needed.&lt;/p&gt;

&lt;p&gt;Once auth was solved, I hit a second, sneakier issue: the OTel collector's port would accept a plain TCP connection before the actual gRPC service behind it was really wired up to handle traffic — so a simple "is the port open" check passed, while real span exports still got reset. The real fix was pushing an actual dummy span through the whole pipeline — SDK → collector → ClickHouse → query API — and retrying that full round trip until it genuinely came back out, before trusting the pipeline was ready. That's a very specific lesson: a listening socket is not the same thing as a working service, and health checks should prove the thing you actually depend on, not a proxy for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my past self
&lt;/h2&gt;

&lt;p&gt;Pre-pull your Docker images before running an installer with a pull step baked in. Link billing to your specific Gemini project even for the free tier. Test both halves of agent testing — the trace and the output — because they catch genuinely different failure classes; the tool-loop bug was invisible to output grading entirely. And when you're proving a service is "ready" in CI, prove it by using the service the way your real code will, not by checking a port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;TraceCheck ended up as: a real agent instrumented with OpenTelemetry, a self-hosted SigNoz instance as the actual source of truth (not a bolt-on dashboard), an assertion engine covering four white-box checks plus an LLM-judge black-box check, three proven seeded-bug branches, a GitHub Action that provisions a fresh SigNoz and blocks a bad merge on every push, and a small live dashboard to watch it all happen. Open source, self-hosted, and framework-agnostic — any agent that emits OpenTelemetry spans is testable this way.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/nischaysood/SIGNOZ-HACK" rel="noopener noreferrer"&gt;https://github.com/nischaysood/SIGNOZ-HACK&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
