<?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: Siddhant </title>
    <description>The latest articles on DEV Community by Siddhant  (@sid12701).</description>
    <link>https://dev.to/sid12701</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%2F4047389%2F71eb9a52-377c-4094-8d46-4c624d97e0a3.jpg</url>
      <title>DEV Community: Siddhant </title>
      <link>https://dev.to/sid12701</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sid12701"/>
    <language>en</language>
    <item>
      <title>CI said green. Production said otherwise: building an AI change flight recorder on SigNoz</title>
      <dc:creator>Siddhant </dc:creator>
      <pubDate>Sat, 25 Jul 2026 23:57:10 +0000</pubDate>
      <link>https://dev.to/sid12701/ci-said-green-production-said-otherwise-building-an-ai-change-flight-recorder-on-signoz-315k</link>
      <guid>https://dev.to/sid12701/ci-said-green-production-said-otherwise-building-an-ai-change-flight-recorder-on-signoz-315k</guid>
      <description>&lt;p&gt;A one-line config change passed all eight of my CI checks. It was reviewed and&lt;br&gt;
shipped. Production p95 on the affected endpoint then rose &lt;strong&gt;7.3×&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing was broken in a way CI could see. The value parsed, the tests passed, the&lt;br&gt;
container built. The failure lived in the gap between "the pipeline is green" and&lt;br&gt;
"the change is safe."&lt;/p&gt;

&lt;p&gt;This post is about closing that gap with SigNoz, and about the five mistakes that&lt;br&gt;
collapsed the first time I ran it end to end. If you are wiring CI data into an&lt;br&gt;
observability backend, the mistakes are the part worth your time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sid12701/greenlight-ai-change-flight-recorder" rel="noopener noreferrer"&gt;Source code and reproducible setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=QiWLpvP3vXc" rel="noopener noreferrer"&gt;2:25 demo video&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The one-line regression
&lt;/h2&gt;

&lt;p&gt;Here is the change, on a real commit&lt;br&gt;
&lt;a href="https://github.com/sid12701/greenlight-ai-change-flight-recorder/commit/2fa6e2861eabf162a26af0d0ef012124865811df" rel="noopener noreferrer"&gt;&lt;code&gt;2fa6e28&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;   "data_source": {
     "max_open_conns": 20,
&lt;span class="gd"&gt;-    "max_idle_conns": 5
&lt;/span&gt;&lt;span class="gi"&gt;+    "max_idle_conns": 5,
+    "conn_max_lifetime": 1000000
&lt;/span&gt;   },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads like ordinary connection-pool tuning. But the service is&lt;br&gt;
&lt;a href="https://github.com/blnkfinance/blnk" rel="noopener noreferrer"&gt;Blnk&lt;/a&gt;, written in Go, and a JSON number&lt;br&gt;
decoded into &lt;code&gt;time.Duration&lt;/code&gt; is &lt;strong&gt;nanoseconds&lt;/strong&gt;. &lt;code&gt;1000000&lt;/code&gt; is not sixteen&lt;br&gt;
minutes. It is &lt;strong&gt;one millisecond&lt;/strong&gt;. The pool threw away every PostgreSQL&lt;br&gt;
connection almost as soon as it opened it.&lt;/p&gt;

&lt;p&gt;Under measured traffic on &lt;code&gt;/balances&lt;/code&gt;, p95 moved from &lt;strong&gt;1.44 ms to 10.45 ms&lt;/strong&gt;. A&lt;br&gt;
later revert measured 2.1 ms.&lt;/p&gt;

&lt;p&gt;I did not write this bug into a toy service. Blnk is a third-party Apache-2.0&lt;br&gt;
ledger, checksum-verified at &lt;code&gt;v0.15.1&lt;/code&gt;, which knows nothing about my tooling. A&lt;br&gt;
regression your own demo was built to detect proves little.&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%2F721t3i2org6i70novnh3.jpg" 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%2F721t3i2org6i70novnh3.jpg" alt="Regression receipt showing the 7.3x p95 change" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The part SigNoz does not do out of the box
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions exports no OpenTelemetry.&lt;/strong&gt; A workflow run is REST metadata —&lt;br&gt;
timestamps, conclusions, job names — not spans. So a CI result and a production&lt;br&gt;
trace live in two systems and cannot be asked one question.&lt;/p&gt;

&lt;p&gt;So I reconstruct each run as a real trace: one root span per run, one child per&lt;br&gt;
job, with real start and end times, exported over OTLP as &lt;code&gt;greenlight-ci&lt;/code&gt;. Once&lt;br&gt;
CI, deployments and request traffic are all spans in one backend, "which run&lt;br&gt;
approved this version, and what did it do to latency?" is one query.&lt;/p&gt;

&lt;p&gt;The unit of comparison is the &lt;strong&gt;immutable deployed version&lt;/strong&gt;, not wall-clock&lt;br&gt;
time. Every deployment reports its commit SHA as &lt;code&gt;service.version&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service.name = 'blnk-loan-workload'
  AND service.version = '&amp;lt;commit sha&amp;gt;'
  AND deployment.environment.name = 'hackathon-demo'
  AND http.route = '/balances'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes rollbacks and overlapping deploys unambiguous. A baseline captured&lt;br&gt;
hours earlier still refers to exactly one version.&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%2Fqgq5ifcy6n3r0y6jr6hc.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%2Fqgq5ifcy6n3r0y6jr6hc.png" alt="Architecture: how one change becomes evidence" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Which session wrote this line?
&lt;/h2&gt;

&lt;p&gt;An agent wrote that regression. That is ordinary now, and it changes the question&lt;br&gt;
you ask after an incident: not just &lt;em&gt;what changed&lt;/em&gt;, but &lt;em&gt;which session produced&lt;br&gt;
it, and what was it told to do?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude Code emits OpenTelemetry, and with &lt;code&gt;CLAUDE_CODE_PROPAGATE_TRACEPARENT=1&lt;/code&gt;&lt;br&gt;
it sets &lt;code&gt;TRACEPARENT&lt;/code&gt; for the commands it runs — so a &lt;code&gt;prepare-commit-msg&lt;/code&gt; hook&lt;br&gt;
can write that context into the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI-Traceparent: 00-86b83e0039724d54b250693de0e7cba7-95cf03c6c3e1413b-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On sync, the worker resolves that exact trace &lt;strong&gt;and&lt;/strong&gt; span against&lt;br&gt;
&lt;code&gt;service.name = 'claude-code'&lt;/code&gt;. If it resolves, the receipt reads&lt;br&gt;
&lt;strong&gt;&lt;code&gt;AI link: verified&lt;/code&gt;&lt;/strong&gt;. If a trailer names a span SigNoz does not hold, it reads&lt;br&gt;
&lt;code&gt;failed&lt;/code&gt; — distinct from &lt;code&gt;missing&lt;/code&gt;, which means no trailer at all. A commit that&lt;br&gt;
claimed a session and could not back it up is a stronger statement than one that&lt;br&gt;
never claimed anything.&lt;/p&gt;

&lt;p&gt;Enabling &lt;code&gt;OTEL_LOG_USER_PROMPTS&lt;/code&gt; took this further: the receipt now shows that&lt;br&gt;
session's prompts, so the instruction, the commit and the latency graph are one&lt;br&gt;
chain. Tool arguments and output stay off — they carry file contents and command&lt;br&gt;
output — and CI fails the build if either is enabled.&lt;/p&gt;

&lt;p&gt;Two caveats. Anything typed into an instrumented session is recorded, so it is no&lt;br&gt;
place for a credential. And the trailer proves a commit was made &lt;strong&gt;from&lt;/strong&gt; a&lt;br&gt;
session, not that a model wrote the code. Session provenance, not authorship.&lt;/p&gt;
&lt;h2&gt;
  
  
  Five things that broke when I ran it for real
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. My load generator was measuring itself.&lt;/strong&gt; It took a duration flag but never&lt;br&gt;
paced requests. Hundreds of calls finished instantly, Blnk's rate limiter&lt;br&gt;
rejected a chunk, and my "healthy" baseline showed an error rate the test tool&lt;br&gt;
had manufactured. If your baseline looks unhealthy before you change anything,&lt;br&gt;
suspect the harness first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. My latency threshold silently exempted fast endpoints.&lt;/strong&gt; The policy required&lt;br&gt;
p95 to rise 1.5× &lt;em&gt;and&lt;/em&gt; by at least 250 ms — a number I picked because it is&lt;br&gt;
roughly what a user notices. On a 1.44 ms route, that means reaching 251 ms. &lt;strong&gt;A&lt;br&gt;
174× regression would not have qualified.&lt;/strong&gt; I replaced it with a 2 ms floor, a&lt;br&gt;
measurement-resolution guard rather than a perception one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. My first demo measured two things at once.&lt;/strong&gt; I injected a PostgreSQL outage&lt;br&gt;
inside the candidate's measured window, so the verdict fired on an error rate&lt;br&gt;
unrelated to the config change. The fix was splitting the clean chain and the&lt;br&gt;
dependency-failure scenario into two explicitly labelled runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A trace is not a session.&lt;/strong&gt; I read a session's prompts by filtering on&lt;br&gt;
&lt;code&gt;trace_id&lt;/code&gt;, which seemed obviously correct. But trace context propagates &lt;em&gt;into&lt;/em&gt;&lt;br&gt;
nested sessions: an agent started from inside another inherits its parent's&lt;br&gt;
&lt;code&gt;TRACEPARENT&lt;/code&gt; and joins the same trace. One trace ended up holding three&lt;br&gt;
sessions, and the receipt was ready to show an unrelated throwaway prompt as the&lt;br&gt;
explanation for a database commit. Attribution now resolves the trailer's span to&lt;br&gt;
its owning &lt;code&gt;session.id&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. My validator could never pass where I told people to run it.&lt;/strong&gt; It checked&lt;br&gt;
that &lt;code&gt;OTEL_*&lt;/code&gt; exports were set, and the docs said to run it from inside the coding&lt;br&gt;
session — but Claude Code strips &lt;code&gt;OTEL_*&lt;/code&gt; from the environment it gives the&lt;br&gt;
commands it runs. It reported a broken pipeline while spans arrived the whole&lt;br&gt;
time.&lt;/p&gt;

&lt;p&gt;That third one is why every receipt carries this, and why it is load-bearing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deployment correlation is evidence of temporal and version association, not&lt;br&gt;
proof that every observed failure was caused by the commit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  How each SigNoz signal earns its place
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traces decide the verdict.&lt;/strong&gt; Two Query Builder v5 queries per window: one&lt;br&gt;
returns count, p90 and p95, the other counts spans with &lt;code&gt;has_error = true&lt;/code&gt; over&lt;br&gt;
the same scope. One detail cost me an hour — SigNoz answers an empty window with&lt;br&gt;
a zero count and &lt;em&gt;null&lt;/em&gt; percentiles, and &lt;code&gt;Number(null)&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;. Coerced naively,&lt;br&gt;
"no traffic" reads as "zero latency".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dashboards make versions comparable.&lt;/strong&gt; The panel that matters groups p95 by&lt;br&gt;
&lt;code&gt;service.version&lt;/code&gt;, so baseline, candidate and recovery are three series and the&lt;br&gt;
step between them &lt;em&gt;is&lt;/em&gt; the deployment.&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%2Fdc8a5fth988uwbhxz138.jpg" 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%2Fdc8a5fth988uwbhxz138.jpg" alt="SigNoz Deployment Impact dashboard" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics describe decisions, which traces cannot.&lt;/strong&gt; A verdict is not a request&lt;br&gt;
and queue depth is a state, not an event, so both are custom instruments —&lt;br&gt;
&lt;code&gt;greenlight.regression.verdicts&lt;/code&gt; by status, plus gauges observed on the export&lt;br&gt;
interval so an idle-but-healthy system still reports.&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%2Fn6u63ymrjcukuwb7d6dn.jpg" 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%2Fn6u63ymrjcukuwb7d6dn.jpg" alt="Custom verdict metric in SigNoz" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alerts follow the service, not a version.&lt;/strong&gt; Two rules — p95, and a true error&lt;br&gt;
rate computed as &lt;code&gt;A/B*100&lt;/code&gt; over errored spans and all spans. Deliberately not&lt;br&gt;
pinned to &lt;code&gt;service.version&lt;/code&gt;: a version-pinned alert can only describe a version&lt;br&gt;
that existed when you wrote the rule.&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%2Fxoip91o702ddo6jg8uwx.jpg" 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%2Fxoip91o702ddo6jg8uwx.jpg" alt="Observed p95 alert history" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs carry the commit.&lt;/strong&gt; API and worker logs ship over OTLP with trace context,&lt;br&gt;
and any job about a change carries &lt;code&gt;commit_sha&lt;/code&gt; — an investigator arrives holding&lt;br&gt;
a commit, not a queue job ID.&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%2Fhq0mdhm4np7f5lru7kj2.jpg" 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%2Fhq0mdhm4np7f5lru7kj2.jpg" alt="Commit-correlated worker logs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP answers the same question agent-natively.&lt;/strong&gt; I ask the SigNoz MCP server to&lt;br&gt;
compare two versions over streamable HTTP, with no direct-query fallback. Over a&lt;br&gt;
wide 15-hour window it reported 1.58 ms vs 9.39 ms, corroborating the receipt's&lt;br&gt;
narrower 1.44 → 10.45 ms. Error rates differ (0% vs 9.13%) because the wide&lt;br&gt;
window also contains the dependency-failure rehearsal — same version, different&lt;br&gt;
windows, different correct answers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Node 24, Docker Compose v2, SigNoz Foundry &lt;code&gt;v0.2.16&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sid12701/greenlight-ai-change-flight-recorder
&lt;span class="nb"&gt;cd &lt;/span&gt;greenlight-ai-change-flight-recorder
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; .env.demo.example .env.demo
npm run demo:up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It pauses once, on purpose: SigNoz does not expose an API key through automation,&lt;br&gt;
so you create a service-account key in the UI and re-run.&lt;/p&gt;

&lt;p&gt;My last clean run passed lint, type-checking, every build, &lt;strong&gt;245 tests&lt;/strong&gt;&lt;br&gt;
(13 skipped), 24 receipt links, six image-digest checks and three MCP trace&lt;br&gt;
resolutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself at the start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify end to end before you believe any number.&lt;/strong&gt; Every failure above
survived unit tests. Two were invisible until a real coding session ran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure one thing at a time&lt;/strong&gt;, or you have measured nothing you can attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say what you did not observe.&lt;/strong&gt; My alert rules fired and resolved, but I never
watched SigNoz deliver the webhook end to end, so I do not claim it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Boundaries: the service map is empty because this workload has no cross-service&lt;br&gt;
parent/child spans, and the demo is local rather than hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Observability cannot prove a commit caused an incident, and I would not trust a&lt;br&gt;
tool that claimed otherwise. What it can do is make the evidence resolvable,&lt;br&gt;
version-scoped, and honest enough that a human — or an agent — starts from facts&lt;br&gt;
instead of guesswork. When the thing writing your code also emits telemetry, that&lt;br&gt;
evidence reaches back to the instruction that started it.&lt;/p&gt;

&lt;p&gt;The code is &lt;a href="https://github.com/sid12701/greenlight-ai-change-flight-recorder" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;,&lt;br&gt;
MIT licensed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI assistance disclosure:&lt;/strong&gt; Codex/ChatGPT and Claude Code were used for&lt;br&gt;
planning, implementation, review and submission preparation, as allowed by the&lt;br&gt;
&lt;a href="https://www.wemakedevs.org/hackathons/signoz/rules" rel="noopener noreferrer"&gt;hackathon rules&lt;/a&gt;. All commits&lt;br&gt;
are reviewed and authored under my own Git identity. See&lt;br&gt;
&lt;a href="https://github.com/sid12701/greenlight-ai-change-flight-recorder/blob/main/PROVENANCE.md" rel="noopener noreferrer"&gt;PROVENANCE.md&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
