<?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: Arjun Sabu</title>
    <description>The latest articles on DEV Community by Arjun Sabu (@greninja-op).</description>
    <link>https://dev.to/greninja-op</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%2F4032321%2F0f5a7684-1f3a-4e2b-8bf3-245adeef7d10.jpg</url>
      <title>DEV Community: Arjun Sabu</title>
      <link>https://dev.to/greninja-op</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/greninja-op"/>
    <language>en</language>
    <item>
      <title>ChronoLens</title>
      <dc:creator>Arjun Sabu</dc:creator>
      <pubDate>Sun, 26 Jul 2026 08:19:51 +0000</pubDate>
      <link>https://dev.to/greninja-op/chronolens-3b6a</link>
      <guid>https://dev.to/greninja-op/chronolens-3b6a</guid>
      <description>&lt;h1&gt;
  
  
  ChronoLens: proving the outage that never happened
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;How we built a self-preventing reliability loop on SigNoz — and how we made it prove its own results with measured telemetry instead of a pretty chart.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built for the &lt;strong&gt;Agents of SigNoz&lt;/strong&gt; hackathon (Track 01 — AI &amp;amp; Agent Observability).&lt;br&gt;
Repo: &lt;a href="https://github.com/greninja-op/ChronoLens" rel="noopener noreferrer"&gt;github.com/greninja-op/ChronoLens&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&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%2F6eam36jonsi69dxumrp1.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%2F6eam36jonsi69dxumrp1.jpg" alt=" " width="800" height="1133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The problem nobody can demo&lt;/li&gt;
&lt;li&gt;Chrono-Proof: a counterfactual made of measurements&lt;/li&gt;
&lt;li&gt;Blast-radius: which service falls next&lt;/li&gt;
&lt;li&gt;The loop that does the work&lt;/li&gt;
&lt;li&gt;Agent Watch: the same loop, pointed at an AI agent&lt;/li&gt;
&lt;li&gt;Human-in-the-loop: Slack and WhatsApp&lt;/li&gt;
&lt;li&gt;The telemetry stack: OpenTelemetry in, SigNoz out&lt;/li&gt;
&lt;li&gt;What SigNoz's APIs taught us&lt;/li&gt;
&lt;li&gt;MCP: calling the server, not just resembling it&lt;/li&gt;
&lt;li&gt;The demo app and the agent's model&lt;/li&gt;
&lt;li&gt;Reproduce it&lt;/li&gt;
&lt;li&gt;What we cut, and why it matters&lt;/li&gt;
&lt;li&gt;Honest limits&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The problem nobody can demo
&lt;/h2&gt;

&lt;p&gt;Observability tells you what broke. Every SRE tool on the market is excellent at the postmortem&lt;br&gt;
and useless at the ten seconds before it. So we built the obvious thing: predict the breach, act&lt;br&gt;
before it lands, verify the fix.&lt;/p&gt;

&lt;p&gt;Then we hit the problem that kills every prevention tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prevention is invisible.&lt;/strong&gt; When it works, nothing happens. There's no incident, no graph spike,&lt;br&gt;
no war room — just a flat line and an engineer with no way to prove the flat line was earned. The&lt;br&gt;
industry's usual answer is a "what would have happened" chart drawn from a formula, and anyone&lt;br&gt;
technical can dismiss it in one sentence: &lt;em&gt;you made that curve up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We had exactly that in our own codebase. An endpoint called &lt;code&gt;/api/counterfactual&lt;/code&gt; that produced a&lt;br&gt;
beautiful dual-timeline chart out of a hardcoded exponential decay, with no telemetry behind it.&lt;br&gt;
We deleted it.&lt;/p&gt;

&lt;p&gt;What replaced it is the feature this post is really about.&lt;/p&gt;


&lt;h2&gt;
  
  
  Chrono-Proof: a counterfactual made of measurements
&lt;/h2&gt;

&lt;p&gt;The honest way to prove a negative is to be strict about which half of the claim is measured and&lt;br&gt;
which half is estimated — and to say so on the artifact itself.&lt;/p&gt;

&lt;p&gt;Chrono-Proof does five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pulls the &lt;strong&gt;real&lt;/strong&gt; p99 series for the service out of SigNoz (Query Builder v5, &lt;code&gt;time_series&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Splits that series at the moment ChronoLens acted.&lt;/li&gt;
&lt;li&gt;Fits the trend on the &lt;strong&gt;pre-action samples only&lt;/strong&gt;, using the same machinery the forecaster
uses — EWMA smoothing, then Holt's linear trend, sanity-blended with least squares so a bad
Holt initialisation can't dominate — and extends that slope across the post-action window with
a confidence band derived from the residual spread. The slope estimate is careful; the
extrapolation itself is deliberately simple and linear, which is why it's labelled an estimate.&lt;/li&gt;
&lt;li&gt;Overlays the &lt;strong&gt;measured&lt;/strong&gt; post-action reality from SigNoz.&lt;/li&gt;
&lt;li&gt;Quantifies the gap: breach-seconds avoided, peak milliseconds shaved, and SLO-violation area
(error budget) saved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is real output from a live run, not a mock-up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== CHRONO-PROOF: chronolens-store (source: signoz) ===

  MEASURED (SigNoz)   peak      48 ms ·     0s over SLO · final 45 ms
  PROJECTED (est.)    peak    4474 ms ±1108 ·   90s over SLO · trend +15.2 ms/s

  Breach avoided      90s
  Peak shaved         4427 ms
  Error budget saved  306490 ms·s
  Prevented           True  (confidence 71%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every number carries its provenance. The defused arm is &lt;code&gt;measured&lt;/code&gt;. The counterfactual arm is a&lt;br&gt;
&lt;strong&gt;labelled linear extrapolation with an interval&lt;/strong&gt; — an estimate, and the note on the payload says&lt;br&gt;
exactly that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The 'measured' arm is SigNoz data. The 'projected' arm is a linear extrapolation of the measured&lt;br&gt;
pre-action trend (± band) — a labelled estimate, not a measurement.&lt;/p&gt;
&lt;/blockquote&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%2Fhurhw96i14wes58paurs.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%2Fhurhw96i14wes58paurs.png" alt=" " width="799" height="310"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The bug that made it honest
&lt;/h3&gt;

&lt;p&gt;Our first version guessed &lt;em&gt;when&lt;/em&gt; the action happened by taking the series peak. That broke in a&lt;br&gt;
way worth documenting: while a load ramp was still running, p99 climbed again after the fix, the&lt;br&gt;
"peak" landed in the wrong place, and Chrono-Proof reported &lt;strong&gt;"the fix did not hold"&lt;/strong&gt; for a fix&lt;br&gt;
that had held.&lt;/p&gt;

&lt;p&gt;The correct answer was already in the system. Every remediation writes a case file to the ledger&lt;br&gt;
with a timestamp, so the proof now derives the action point from the &lt;strong&gt;recorded action time&lt;/strong&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="n"&gt;idx&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="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_samples&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;step_s&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It skips non-action rows (&lt;code&gt;none&lt;/code&gt;, &lt;code&gt;pre-provision&lt;/code&gt;, &lt;code&gt;suggest:*&lt;/code&gt;), takes the newest real action, and&lt;br&gt;
falls back to the peak heuristic only when no case matches the window — stating which anchor it&lt;br&gt;
used. Six unit tests cover the arithmetic, the rejections and the fallback.&lt;/p&gt;


&lt;h2&gt;
  
  
  Blast-radius: which service falls next
&lt;/h2&gt;

&lt;p&gt;Predicting that &lt;em&gt;one&lt;/em&gt; service will breach is table stakes. The question that actually matters in a&lt;br&gt;
distributed system is the one nobody answers in advance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;payment is about to go — what goes down with it, in what order, and how long do I have?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SigNoz already knows the topology. It derives a service dependency graph from traces and exposes&lt;br&gt;
it at &lt;code&gt;/api/v1/dependency_graph&lt;/code&gt;. We walk that graph upward from the degrading dependency, because&lt;br&gt;
&lt;strong&gt;a caller can never be faster than the thing it waits on&lt;/strong&gt;: a slow child pushes latency into&lt;br&gt;
every ancestor, scaled by how much of the parent's traffic actually depends on that path.&lt;/p&gt;

&lt;p&gt;Live output, three real services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== BLAST-RADIUS FORECAST (topology: signoz-service-map) ===

  Root cause: chronolens-payments-db  [root hop: payment.db_query]

  service                     p99     slope  inherit   breach in
  chronolens-payments-db *  6824ms    +37.3     +0.0         NOW
  chronolens-payments       6824ms    +37.3    +37.3         NOW
  chronolens-store           951ms    +27.6    +27.6         NOW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that make this real rather than decorative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The root is the most-downstream degrading service.&lt;/strong&gt; A service climbing only because its
dependency is slow is a &lt;em&gt;symptom&lt;/em&gt;. Fixing it mutes the alarm and changes nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only services inside the dependency graph can be the root.&lt;/strong&gt; An unconnected sidecar with a
steep slope isn't the cause of a cascade — it has no downstream blast path. (We found this by
watching it confidently blame our demo AI agent, which nothing depends on.)&lt;/li&gt;
&lt;/ul&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%2Fopjkhmj35sdtvw8i4wtb.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%2Fopjkhmj35sdtvw8i4wtb.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  One service can't cascade
&lt;/h3&gt;

&lt;p&gt;This feature was untestable in our own demo at first: the store was a single service, so there was&lt;br&gt;
nothing to chain. We split it into a real three-tier topology, each tier emitting spans under its&lt;br&gt;
own &lt;code&gt;service.name&lt;/code&gt; with the parent context preserved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chronolens-store  ──▶  chronolens-payments  ──▶  chronolens-payments-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;dependency-slow&lt;/code&gt; injected at the deepest tier produces a genuine measured cascade, and SigNoz's&lt;br&gt;
own dependency graph is what ChronoLens reads to trace 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%2F1ltwwo0liajtdrydwut2.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%2F1ltwwo0liajtdrydwut2.jpg" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The loop that does the work
&lt;/h2&gt;

&lt;p&gt;Chrono-Proof and blast-radius are the evidence layer. Underneath is the control loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LEARN → FORESEE → CLASSIFY → CASCADE → GOVERN → PREVENT → VERIFY → COOLDOWN → RECORD
  ▲                                                                              │
  └──────────────────── the ledger becomes LEARN's memory ──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LEARN&lt;/strong&gt; reads past incidents, including time-of-day seasonality. For a repeat offender it
pre-provisions a higher floor &lt;em&gt;before&lt;/em&gt; anything degrades, and it corroborates recurrence against
SigNoz's own alert state rather than trusting only its local ledger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FORESEE&lt;/strong&gt; projects p99 to a time-to-breach behind a &lt;strong&gt;confidence guard&lt;/strong&gt;: enough samples, slope
above a noise floor, and a &lt;em&gt;sustained&lt;/em&gt; rise. An elevated error rate from a second, independent
signal lifts confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLASSIFY&lt;/strong&gt; picks the fix that matches the signal, not always "scale": load → scale,
dependency → circuit-break, pool → resize, memory → restart, errors → roll back. The &lt;code&gt;errors&lt;/code&gt;
signal is cross-checked against a SigNoz &lt;strong&gt;logs&lt;/strong&gt; query, so classification spans two signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASCADE&lt;/strong&gt; names the root hop from a grouped traces query, plus an exemplar trace ID for a
deep link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GOVERN&lt;/strong&gt; is a trust ladder — &lt;code&gt;suggest&lt;/code&gt; (never acts alone), &lt;code&gt;earn&lt;/code&gt; (autonomous after N verified
saves on that service), &lt;code&gt;auto&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PREVENT&lt;/strong&gt; acts behind anti-flap guardrails: a minimum dwell time between actions, a hard
capacity ceiling, an hourly action budget, and a global kill switch. Every action is reversible
and every action stores its precise inverse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VERIFY&lt;/strong&gt; asks SigNoz whether the breach was actually avoided. If it wasn't, it rolls back and
escalates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;COOLDOWN&lt;/strong&gt; returns the capacity once load subsides, so prevention isn't paid for with permanent
over-provisioning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RECORD&lt;/strong&gt; files the receipt that LEARN reads next time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A run against live SigNoz, abridged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[FORESEE ] chronolens-store: p99 2540.8ms, rising 8ms/s → SLO breach NOW (confidence 100%)
[CLASSIFY] Signal: load → reversible fix 'scale'
[CASCADE ] Degradation at 'payment.charge' (p99 1005.1ms) (measured in traces) …
           Exemplar trace: a6c3d99cc80d5efc2dfa9f720cdbe773
[GOVERN  ] autonomy=auto — acting automatically
[PREVENT ] Applied 'scale' (reversible). Rollback: scale back down once load subsides
[VERIFY  ] Confirmed via SigNoz: p99 back to 53.5ms — breach avoided
[COOLDOWN] Load subsided — scaled 8.0 → 2.0, returned 6.0 capacity units (~$3.90)
[GUARD   ] Filed a guarding SigNoz alert + dashboard on chronolens-store p99
[RECORD  ] Case filed: breach avoided · returned 6.0 units (~$3.90)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fks428kntf3uei2ir75mq.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%2Fks428kntf3uei2ir75mq.png" alt=" " width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent Watch: the same loop, pointed at an AI agent
&lt;/h2&gt;

&lt;p&gt;Track 01 is agent observability, so ChronoLens watches an agent the way it watches a service — and&lt;br&gt;
agents fail in ways HTTP status codes never capture.&lt;/p&gt;

&lt;p&gt;The demo agent is a café assistant instrumented with &lt;strong&gt;OpenTelemetry GenAI semantic-convention&lt;br&gt;
attributes&lt;/strong&gt;. Each turn emits an &lt;code&gt;agent.turn&lt;/code&gt; parent span with child &lt;code&gt;gen_ai.chat&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;tool.execute&lt;/code&gt; spans, carrying &lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;output_tokens&lt;/code&gt;, &lt;code&gt;llm.step_count&lt;/code&gt;, &lt;code&gt;llm.cost_usd&lt;/code&gt; and &lt;code&gt;agent.tools&lt;/code&gt;. It runs in three modes —&lt;br&gt;
&lt;code&gt;normal&lt;/code&gt; (the learned baseline), &lt;code&gt;drift&lt;/code&gt;, and &lt;code&gt;loop&lt;/code&gt; — so each failure is reproducible on demand.&lt;/p&gt;

&lt;p&gt;Three analyzers read those spans:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Analyzer&lt;/th&gt;
&lt;th&gt;The failure it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behaviour drift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;After a prompt tweak or model swap the agent still returns 200 OK with normal latency, but it now calls a tool it never used, takes more steps, or writes far longer answers. A fingerprint (tool distribution, model mix, avg steps, avg tokens) is scored against a saved baseline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Loop / cost breaker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent reasons in circles, calling the same tool repeatedly, burning tokens with no crash. It fires on &lt;strong&gt;no progress&lt;/strong&gt; and on a &lt;strong&gt;cost budget&lt;/strong&gt;, not just a clock — so a long but genuinely productive turn is left alone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Answer quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Grades recent answers to separate &lt;em&gt;changed&lt;/em&gt; from &lt;em&gt;worse&lt;/em&gt; — drift is a change signal, not a verdict. The answers are read from &lt;strong&gt;SigNoz logs&lt;/strong&gt;: the agent emits each full response as an OTel log record, because span attributes only carry a truncated preview and you cannot grade what you cannot read.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Detecting from SigNoz, not by poking the agent
&lt;/h3&gt;

&lt;p&gt;This is the part we had to fix to be honest. The analyzers originally called the agent's &lt;code&gt;/chat&lt;/code&gt;&lt;br&gt;
endpoint to get turns — which means "agent observability" that never touched the observability&lt;br&gt;
platform. Now the default path issues a &lt;strong&gt;raw traces query&lt;/strong&gt; (&lt;code&gt;requestType: "raw"&lt;/code&gt;) against SigNoz&lt;br&gt;
for the agent's &lt;code&gt;agent.turn&lt;/code&gt; spans and reconstructs the turns from their attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;turns read FROM SIGNOZ: 16
   gpt-4o-mini  2 steps  ['get_menu', 'place_order']  $9e-05  src=signoz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every response now reports &lt;code&gt;data_source: "signoz"&lt;/code&gt;, and falls back to driving the agent only when&lt;br&gt;
SigNoz has no spans yet (a cold stack) — saying so when it does.&lt;/p&gt;

&lt;p&gt;Quality grading was the last hold-out, and fixing it needed a second signal rather than a cleverer&lt;br&gt;
query: the agent now emits its &lt;strong&gt;complete response as an OTel log record&lt;/strong&gt;, and the judge reads those&lt;br&gt;
bodies back with a &lt;code&gt;requestType:"raw"&lt;/code&gt; logs query. So the drift and loop analyzers read &lt;em&gt;traces&lt;/em&gt;,&lt;br&gt;
the judge reads &lt;em&gt;logs&lt;/em&gt;, and all three are telemetry-driven — verified live on 8 graded answers.&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%2Fj9yjkodo37swu5iaexd1.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%2Fj9yjkodo37swu5iaexd1.png" alt=" " width="800" height="156"&gt;&lt;/a&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%2Fma3euu3job955jd7thr4.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%2Fma3euu3job955jd7thr4.jpg" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Human-in-the-loop: Slack and WhatsApp
&lt;/h2&gt;

&lt;p&gt;The trust ladder's &lt;code&gt;suggest&lt;/code&gt; tier is only meaningful if a human can actually approve something. So&lt;br&gt;
when GOVERN decides ChronoLens may not act alone, it posts an &lt;strong&gt;interactive approval card&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🕳️ &lt;strong&gt;ChronoLens needs your approval&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Service:&lt;/strong&gt; &lt;code&gt;payment&lt;/code&gt; · &lt;strong&gt;Forecast:&lt;/strong&gt; p99 past SLO in ~85s (confidence 86%)&lt;br&gt;
&lt;strong&gt;Signal:&lt;/strong&gt; pool · &lt;strong&gt;Proposed fix:&lt;/strong&gt; &lt;code&gt;pool-resize&lt;/code&gt; — &lt;em&gt;reversible&lt;/em&gt;&lt;br&gt;
&lt;code&gt;[ ✅ Approve ]  [ ✋ Deny ]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tapping &lt;strong&gt;Approve&lt;/strong&gt; runs the real PREVENT → VERIFY → COOLDOWN → RECORD path and then &lt;strong&gt;edits the&lt;br&gt;
same message&lt;/strong&gt; with the SigNoz-verified outcome ("p99 back to 53.5 ms — breach avoided"). Deny&lt;br&gt;
records the decision and stands down. Agent anomalies get their own card with&lt;br&gt;
&lt;strong&gt;Break / pin baseline&lt;/strong&gt;, which pins the agent to its last-good baseline and verifies the next turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every tap answers twice.&lt;/strong&gt; Remediation is a real verify cycle, so it takes tens of seconds — and a&lt;br&gt;
button that goes quiet for half a minute reads as a button that didn't work, so people tap it again.&lt;br&gt;
Each tap now gets an immediate acknowledgement ("⏳ Applying &lt;code&gt;scale_out&lt;/code&gt;…") and then the verified&lt;br&gt;
outcome once SigNoz has confirmed it. On Slack that's one message rewritten twice; on WhatsApp it's&lt;br&gt;
two replies. Unrecognised taps get an answer too — silence is indistinguishable from a broken&lt;br&gt;
integration.&lt;/p&gt;

&lt;p&gt;Both surfaces call the &lt;strong&gt;same approval engine&lt;/strong&gt;, tagged with the surface it came from, so an approval&lt;br&gt;
means exactly the same thing wherever it arrives and the ledger receipt records &lt;em&gt;which&lt;/em&gt; surface&lt;br&gt;
decided. WhatsApp previously re-ran the whole loop instead of executing the approved action, which&lt;br&gt;
meant a tap on your phone and a tap in Slack didn't do quite the same thing. One engine, two doors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need both?&lt;/strong&gt; No — and we're explicit about it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Slack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Socket Mode (an outbound WebSocket), so &lt;strong&gt;no public URL&lt;/strong&gt; is needed. Free tier is enough: a bot token (&lt;code&gt;xoxb-&lt;/code&gt;) plus an app-level token (&lt;code&gt;xapp-&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;The default. Least friction, richest UI (Block Kit buttons), ideal for a team channel and for local development.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WhatsApp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Meta WhatsApp Cloud API webhooks with HMAC-SHA256 signature verification, interactive reply buttons. Needs a public HTTPS endpoint and a Meta business number.&lt;/td&gt;
&lt;td&gt;Reach — an on-call engineer who isn't at a desk. Convenience, not capability.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Either one alone is sufficient. They implement the same contract; Slack is the recommended path and&lt;br&gt;
WhatsApp exists because approving a production fix from a phone lock screen is genuinely useful.&lt;br&gt;
Everything shown in this post is Slack, deliberately — we'd rather describe the WhatsApp path than&lt;br&gt;
screenshot one we aren't actively driving.&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%2Fmxyp9mzdz65y2y6ikbel.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%2Fmxyp9mzdz65y2y6ikbel.png" alt=" " width="800" height="330"&gt;&lt;/a&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%2Fig10rs5rldlxvskwdtkp.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%2Fig10rs5rldlxvskwdtkp.png" alt=" " width="800" height="76"&gt;&lt;/a&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%2Fkuuc4gnpu6jovf14c0a3.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%2Fkuuc4gnpu6jovf14c0a3.png" alt=" " width="800" height="116"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A note on the signature check
&lt;/h3&gt;

&lt;p&gt;Our first implementation of the WhatsApp webhook returned &lt;code&gt;True&lt;/code&gt; when the app secret or signature&lt;br&gt;
header was missing — a development shortcut that accepts unsigned requests. It now &lt;strong&gt;fails closed&lt;/strong&gt;&lt;br&gt;
whenever a secret is configured: a missing signature is rejected. The only permissive case is local&lt;br&gt;
development with no secret set at all, and the README says not to expose the webhook without one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Worth flagging because it's the kind of shortcut that quietly ships.
&lt;/h2&gt;
&lt;h2&gt;
  
  
  The telemetry stack: OpenTelemetry in, SigNoz out
&lt;/h2&gt;

&lt;p&gt;Nothing in ChronoLens has a private data path. Everything it knows arrives as OpenTelemetry and&lt;br&gt;
everything it concludes is written back to SigNoz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we emit (OpenTelemetry):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The demo services export &lt;strong&gt;OTLP traces&lt;/strong&gt; over gRPC (&lt;code&gt;:4317&lt;/code&gt;) with &lt;code&gt;service.name&lt;/code&gt; per service, so
SigNoz derives the dependency graph itself rather than being told about it.&lt;/li&gt;
&lt;li&gt;The demo agent adds &lt;strong&gt;GenAI semantic-convention attributes&lt;/strong&gt; — &lt;code&gt;gen_ai.request.model&lt;/code&gt;,
&lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt; / &lt;code&gt;output_tokens&lt;/code&gt;, plus &lt;code&gt;llm.step_count&lt;/code&gt;, &lt;code&gt;llm.cost_usd&lt;/code&gt;,
&lt;code&gt;agent.tools&lt;/code&gt; — on &lt;code&gt;agent.turn&lt;/code&gt; / &lt;code&gt;gen_ai.chat&lt;/code&gt; / &lt;code&gt;tool.execute&lt;/code&gt; spans.&lt;/li&gt;
&lt;li&gt;ChronoLens instruments &lt;strong&gt;itself&lt;/strong&gt;: each loop stage is a &lt;code&gt;chronolens.stage&lt;/code&gt; span under one loop
trace, and it exports its own &lt;strong&gt;metrics&lt;/strong&gt; (&lt;code&gt;chronolens.prevented_total&lt;/code&gt;, &lt;code&gt;cost_saved_usd&lt;/code&gt;,
&lt;code&gt;seconds_to_breach&lt;/code&gt;). Its guard dashboard then reads those metrics back out of SigNoz — the loop
watching the loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we read (SigNoz):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Query Builder v5 — traces&lt;/strong&gt; (&lt;code&gt;scalar&lt;/code&gt;, &lt;code&gt;time_series&lt;/code&gt;, &lt;code&gt;raw&lt;/code&gt;, &lt;code&gt;group_by&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;p99 per service, p99 series for forecasting and Chrono-Proof, per-span-name breakdown for the cascade root, and raw &lt;code&gt;agent.turn&lt;/code&gt; spans for Agent Watch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query Builder v5 — logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;severity_text='ERROR'&lt;/code&gt; counts that cross-check the &lt;code&gt;errors&lt;/code&gt; classification against a second signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metrics read-back&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ChronoLens's own gauges, so the dashboard closes the circle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service dependency graph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The real topology behind the blast-radius forecast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exemplar trace IDs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep links from a receipt into the exact trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alert state / history&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LEARN confirms recurrence from SigNoz's firing rules, not just its local ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What we write (SigNoz):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Threshold alert rules&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A guard alert per prevented incident, so it stays watched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dashboards&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A guard dashboard with a p99 panel and a panel reading ChronoLens's own metric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Saved views&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A pinned Traces-explorer view for the guarded service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Silences&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Muted while the loop actively remediates, lifted after VERIFY — nobody is paged for a fix already in flight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Notification channels&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Discovered and reused, so ChronoLens routes its notes through the same channel an alert would&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GenAI guard dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auto-filed for the watched agent: cost per turn, steps vs ceiling, output tokens, tool-call mix, and turn latency for contrast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Anomaly alert rule&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A &lt;em&gt;learned&lt;/em&gt; baseline on agent cost per turn, not a fixed threshold — catches "abnormal for this hour" while still inside the budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  What SigNoz's APIs taught us
&lt;/h2&gt;

&lt;p&gt;Writing to an observability platform is a different skill from reading from one, and four of these&lt;br&gt;
cost us real debugging time. None of them produce an error message that names the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anomaly rules only accept &lt;code&gt;METRIC_BASED_ALERT&lt;/code&gt;.&lt;/strong&gt; Pointing one at a traces query —&lt;br&gt;
&lt;code&gt;p99(duration_nano)&lt;/code&gt; grouped by service — is rejected outright:&lt;br&gt;
&lt;code&gt;anomaly_rule can only be used with METRIC_BASED_ALERT&lt;/code&gt;. So "alert when this service's&lt;br&gt;
latency is weird for a Tuesday" isn't directly expressible on spans. The fix was to make&lt;br&gt;
the agent emit its per-turn shape as &lt;strong&gt;metrics&lt;/strong&gt; (&lt;code&gt;chronolens.agent.cost_usd&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;.steps&lt;/code&gt;, &lt;code&gt;.output_tokens&lt;/code&gt;) alongside its spans, and anomaly-alert on the metric. That's&lt;br&gt;
a better design anyway: traces answer "what happened in this turn", a metric gives the&lt;br&gt;
continuous series a seasonal baseline can be learned from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The v2 REST endpoint rejects the v1 rule schema silently.&lt;/strong&gt; Anomaly rules use the older&lt;br&gt;
v1 shape (top-level &lt;code&gt;evalWindow&lt;/code&gt;/&lt;code&gt;frequency&lt;/code&gt;, &lt;code&gt;condition.op&lt;/code&gt;/&lt;code&gt;matchType&lt;/code&gt;/&lt;code&gt;target&lt;/code&gt;/&lt;br&gt;
&lt;code&gt;algorithm&lt;/code&gt;/&lt;code&gt;seasonality&lt;/code&gt;) and our &lt;code&gt;POST /api/v2/rules&lt;/code&gt; returned&lt;br&gt;
&lt;code&gt;{"message":"validation failed","errors":[]}&lt;/code&gt; — an empty error list, no field named.&lt;br&gt;
Rather than keep guessing, we filed it through the &lt;strong&gt;MCP server's &lt;code&gt;signoz_create_alert&lt;/code&gt;&lt;br&gt;
tool&lt;/strong&gt;, which owns that version handling. It worked first time, and it upgraded our MCP&lt;br&gt;
usage from read-only to &lt;strong&gt;read &lt;em&gt;and&lt;/em&gt; write&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The anomaly rule went to &lt;code&gt;state: firing&lt;/code&gt; within a minute of creation — it had already&lt;br&gt;
noticed the cost-per-turn spike from the loop-mode demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dashboard can store every panel and still render blank.&lt;/strong&gt; Our first auto-filed dashboards&lt;br&gt;
looked perfect over the API — five widgets, correct queries — and the UI showed "Welcome to your new&lt;br&gt;
dashboard." Nothing errored. SigNoz positions panels from a &lt;code&gt;layout&lt;/code&gt; array (a react-grid spec keyed&lt;br&gt;
by widget id) and we had sent neither &lt;code&gt;layout&lt;/code&gt; nor widget &lt;code&gt;id&lt;/code&gt;s, so there was nothing to place. The&lt;br&gt;
same class of failure hides in every field the front-end maps over but the API doesn't require:&lt;br&gt;
&lt;code&gt;builder.queryFormulas&lt;/code&gt;, &lt;code&gt;promql&lt;/code&gt;, &lt;code&gt;clickhouse_sql&lt;/code&gt;, &lt;code&gt;selectedLogFields&lt;/code&gt;, &lt;code&gt;selectedTracesFields&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;contextLinks.linksData&lt;/code&gt;, and the dashboard's own &lt;code&gt;variables&lt;/code&gt;. Missing means &lt;code&gt;undefined&lt;/code&gt;, not empty.&lt;br&gt;
We now send empties for all of them and a test asserts every panel carries them, so an empty&lt;br&gt;
dashboard can't ship again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold markers need the long field names.&lt;/strong&gt; &lt;code&gt;{"index","label","value","unit"}&lt;/code&gt; is accepted and&lt;br&gt;
silently draws nothing; the UI reads &lt;code&gt;thresholdValue&lt;/code&gt;, &lt;code&gt;thresholdUnit&lt;/code&gt;, &lt;code&gt;thresholdOperator&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;thresholdFormat&lt;/code&gt; and &lt;code&gt;thresholdColor&lt;/code&gt;. And latency thresholds are &lt;strong&gt;nanoseconds on dashboards but&lt;br&gt;
milliseconds on alerts&lt;/strong&gt; — set the dashboard marker in ms and your SLO line lands in the wrong place&lt;br&gt;
by six orders of magnitude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A metric only emitted during a run looks identical to a broken metric.&lt;/strong&gt; Our "incidents prevented"&lt;br&gt;
panel read &lt;em&gt;No Data&lt;/em&gt; over a 30-minute window even though the ledger had saves in it: the gauge was&lt;br&gt;
only published while a loop was executing, so the newest sample was hours old. Mission Control now&lt;br&gt;
publishes it on a heartbeat, which turns one dot per incident into a continuous line. Worth&lt;br&gt;
generalising — if you write your own metrics back to your observability platform, emit them on a&lt;br&gt;
timer, not only on the event.&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%2Fyxi0xuqp0hr5ivfvr8jb.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%2Fyxi0xuqp0hr5ivfvr8jb.jpg" alt=" " width="800" height="925"&gt;&lt;/a&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%2Fo65m25z7885jjkxuawun.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%2Fo65m25z7885jjkxuawun.jpg" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  MCP: calling the server, not just resembling it
&lt;/h3&gt;

&lt;p&gt;Foundry installs the &lt;strong&gt;SigNoz MCP server&lt;/strong&gt; alongside SigNoz, and it's tempting to call your queries&lt;br&gt;
"MCP-compatible" because they have the same shape. We did exactly that for a while — then admitted it&lt;br&gt;
was a claim about resemblance, not usage, and wrote a real client.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/chronolens/mcp.py&lt;/code&gt; is a dependency-free JSON-RPC client: it performs the &lt;code&gt;initialize&lt;/code&gt; handshake,&lt;br&gt;
sends &lt;code&gt;notifications/initialized&lt;/code&gt;, lists the server's tools, and invokes them with &lt;code&gt;tools/call&lt;/code&gt;.&lt;br&gt;
Against the live server it reports &lt;code&gt;SigNozMCP&lt;/code&gt;, protocol &lt;code&gt;2024-11-05&lt;/code&gt;, and &lt;strong&gt;41 tools&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Four things the protocol taught us, none of them in our first guess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth is mandatory — no &lt;code&gt;SIGNOZ-API-KEY&lt;/code&gt; header gets you &lt;code&gt;401 Authorization or SIGNOZ-API-KEY header required&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Accept&lt;/code&gt; header must allow &lt;strong&gt;both&lt;/strong&gt; &lt;code&gt;application/json&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;text/event-stream&lt;/code&gt;, or the
Streamable-HTTP transport refuses the request.&lt;/li&gt;
&lt;li&gt;A reply may arrive as an SSE frame (&lt;code&gt;data: {...}&lt;/code&gt;) instead of a JSON body, so the parser handles both.&lt;/li&gt;
&lt;li&gt;Tool results are &lt;strong&gt;JSON nested inside a text block&lt;/strong&gt; (&lt;code&gt;result.content[].text&lt;/code&gt;) — a second decode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The co-pilot then routes a plain-English question to real tool calls, and the UI lists every call with&lt;br&gt;
its arguments and row count, so the answer is auditable rather than asserted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q: which services are slowest right now?      → signoz_list_services            (5 rows)
Q: are any alerts firing?                     → signoz_list_alert_rules         (10 rows)
Q: any error logs in the last hour?           → signoz_search_logs              (1 row)
Q: top operations for chronolens-store        → signoz_get_service_top_operations (4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Routing is rule-based on purpose: intent here is a small closed set, an LLM would add latency and a&lt;br&gt;
failure mode for no accuracy gain, and a reviewer can read the table and check it. We also removed an&lt;br&gt;
LLM "phrasing" pass that had been rewriting correct answers into generic remediation prose — it turned&lt;br&gt;
a true answer into a plausible-sounding wrong one, which is the exact failure this project exists to catch.&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%2F8yn0w0xm0g62pord7m5t.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%2F8yn0w0xm0g62pord7m5t.png" alt=" " width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reads above still go through the REST Query Builder on the hot path, because the control loop&lt;br&gt;
needs tight latency and deterministic shapes; MCP is how ChronoLens &lt;em&gt;answers questions&lt;/em&gt;, and both&lt;br&gt;
paths hit the same SigNoz.&lt;/p&gt;


&lt;h2&gt;
  
  
  The demo app and the agent's model
&lt;/h2&gt;

&lt;p&gt;Two things are watched, and it matters that we're precise about what's real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The demo store&lt;/strong&gt; is a synthetic three-tier app (&lt;code&gt;chronolens-store&lt;/code&gt; → &lt;code&gt;chronolens-payments&lt;/code&gt; →&lt;br&gt;
&lt;code&gt;chronolens-payments-db&lt;/code&gt;, each its own &lt;code&gt;service.name&lt;/code&gt;) with two jobs: emit believable traces&lt;br&gt;
SigNoz can derive a dependency graph from, and expose &lt;em&gt;reversible levers&lt;/em&gt; — scale,&lt;br&gt;
pool-resize, circuit-break, restart, roll back, reset. Its faults ramp gradually&lt;br&gt;
(&lt;code&gt;traffic-ramp&lt;/code&gt;, &lt;code&gt;dependency-slow&lt;/code&gt;, &lt;code&gt;pool-leak&lt;/code&gt;, &lt;code&gt;memory-leak&lt;/code&gt;, &lt;code&gt;error-spike&lt;/code&gt;) because a step&lt;br&gt;
function isn't forecastable and a forecast you can't test isn't a forecast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent's model is simulated by default, and we say so.&lt;/strong&gt; The café assistant's three modes&lt;br&gt;
(&lt;code&gt;normal&lt;/code&gt; / &lt;code&gt;drift&lt;/code&gt; / &lt;code&gt;loop&lt;/code&gt;) produce deterministic token counts, tool sequences and costs from a&lt;br&gt;
price table. That's a deliberate choice: a drift demo has to be &lt;em&gt;reproducible&lt;/em&gt;, and a real model&lt;br&gt;
that happens to answer consistently proves nothing. The spans it emits are real OpenTelemetry&lt;br&gt;
GenAI spans either way, which is what the analyzers consume.&lt;/p&gt;

&lt;p&gt;For real inference, &lt;code&gt;LLM_PROVIDER&lt;/code&gt; switches the explanation layer and the agent to a live backend —&lt;br&gt;
&lt;code&gt;openai&lt;/code&gt;, &lt;code&gt;bedrock&lt;/code&gt; (AWS), &lt;code&gt;azure&lt;/code&gt;, or &lt;code&gt;gemini&lt;/code&gt; — via one API key. With no key set, ChronoLens runs&lt;br&gt;
end-to-end on a rule-based explainer. Nothing in the loop requires an LLM to function; the LLM only&lt;br&gt;
makes the &lt;em&gt;narration&lt;/em&gt; nicer, which is the right place for a non-deterministic component.&lt;/p&gt;


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

&lt;p&gt;The hackathon asks for a reproducible install, so the repo ships &lt;code&gt;casting.yaml&lt;/code&gt; +&lt;br&gt;
&lt;code&gt;casting.yaml.lock&lt;/code&gt; and &lt;strong&gt;Foundry&lt;/strong&gt; brings up SigNoz &lt;em&gt;and&lt;/em&gt; its MCP server in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/bringup.sh          &lt;span class="c"&gt;# SigNoz UI :8080 · OTLP :4317/:4318 · MCP :8000/mcp&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env             &lt;span class="c"&gt;# add SIGNOZ_API_KEY&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python &lt;span class="nt"&gt;-m&lt;/span&gt; demo_store.store       &lt;span class="c"&gt;# the watched services  :8090&lt;/span&gt;
python demo_agent/agent.py       &lt;span class="c"&gt;# the watched agent      :8091&lt;/span&gt;
python app.py                    &lt;span class="c"&gt;# Mission Control        :8095&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then drive it from the UI, or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; chronolens.cli respond   &lt;span class="c"&gt;# one full loop&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; chronolens.cli proof     &lt;span class="c"&gt;# the measured counterfactual&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; chronolens.cli blast     &lt;span class="c"&gt;# who falls next&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; chronolens.cli guard     &lt;span class="c"&gt;# file the agent dashboard + alerts (incl. the anomaly rule via MCP)&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; chronolens.cli slack     &lt;span class="c"&gt;# the approval listener&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;python scripts/demo_check.py&lt;/code&gt; runs 23 end-to-end checks (stack up, services reporting into SigNoz,&lt;br&gt;
each API surface answering) and prints what's missing rather than failing obscurely. Both demo&lt;br&gt;
services are self-driving — the store at ~6 rps, the agent at ~0.5 turns/s — so a fresh clone shows&lt;br&gt;
populated dashboards instead of an empty SigNoz, which is a failure mode we hit ourselves.&lt;/p&gt;

&lt;p&gt;If you'd rather not run anything, both dashboards are committed as importable JSON in&lt;br&gt;
&lt;code&gt;dashboards/&lt;/code&gt;: in SigNoz go &lt;strong&gt;Dashboards → + New dashboard → Import JSON&lt;/strong&gt; and paste the file. They&lt;br&gt;
are generated from the same code that files them over the API (&lt;code&gt;scripts/export_dashboards.py&lt;/code&gt;), so&lt;br&gt;
they can't drift from what the project actually creates.&lt;/p&gt;

&lt;p&gt;The test suite is &lt;strong&gt;179 tests&lt;/strong&gt; — unit plus property-based with Hypothesis — and most of the&lt;br&gt;
interesting ones are regressions for bugs found against live telemetry rather than in CI: the&lt;br&gt;
negative projection, the noise-level slope that hijacked the cascade root, the blank dashboard, the&lt;br&gt;
approval that answered too late.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we cut, and why it matters
&lt;/h2&gt;

&lt;p&gt;Late on, we audited our own repo and deleted six features: a synthetic "counterfactual" chart that&lt;br&gt;
drew a curve from a formula, a "stress test" that ran a hardcoded array and claimed to read SigNoz,&lt;br&gt;
a tool circuit-breaker rebuilt on every request so it forgot instantly, an executive ROI report that&lt;br&gt;
asserted "100% verification rate" as a string literal, a translation layer unrelated to&lt;br&gt;
observability, and a token throttle nothing read.&lt;/p&gt;

&lt;p&gt;They demoed fine. That's the problem — each one was a place where a judge opening the file would&lt;br&gt;
find a claim the code didn't support, which poisons trust in the features that &lt;em&gt;are&lt;/em&gt; real. Two&lt;br&gt;
docstrings claimed "real-time OpenTelemetry span metrics from SigNoz" in modules containing no&lt;br&gt;
SigNoz call at all.&lt;/p&gt;

&lt;p&gt;The same pass fixed a webhook that accepted unsigned requests, an endpoint that returned its own&lt;br&gt;
verify token, and a real-looking phone number committed as a default in source.&lt;/p&gt;

&lt;p&gt;Fewer features, each of which survives being read. That's the trade we'd make again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;projected&lt;/strong&gt; arm of Chrono-Proof is a linear extrapolation of a measured trend, not a
measurement. Real systems plateau under saturation, so a long projection is an upper bound. Every
field is labelled by provenance and the confidence score falls as the pre-action trend gets noisier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp needs a public callback URL to close its loop.&lt;/strong&gt; Outbound cards send without one, but
the button tap is a Meta webhook, and Meta access tokens expire — so Slack (Socket Mode, outbound
only) is the path we demo and recommend. The code is surface-agnostic; the operational burden is
not.&lt;/li&gt;
&lt;li&gt;All three Agent Watch analyzers now read SigNoz. Grading was the last hold-out: span attributes
carry only a truncated preview of the response, so the agent ships each &lt;strong&gt;full answer as an OTel
log record&lt;/strong&gt; and the judge reads it back with a &lt;code&gt;requestType:"raw"&lt;/code&gt; logs query
(&lt;code&gt;data_source: "signoz"&lt;/code&gt;, verified on 8 graded answers). If the logs pipeline is cold it falls
back to driving the agent &lt;em&gt;and labels itself&lt;/em&gt; &lt;code&gt;agent-driven&lt;/code&gt; rather than implying telemetry.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;agent is simulated by default&lt;/strong&gt; (see above).&lt;/li&gt;
&lt;li&gt;The blast radius is only as good as the dependency graph — with a single service there is nothing
to chain, and it says &lt;code&gt;topology_source: unavailable&lt;/code&gt; rather than inventing edges.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;AWS serverless template is a scaffold that has never been deployed.&lt;/strong&gt; It's a complete,
reviewable SAM stack (Lambda + EventBridge + DynamoDB + Bedrock), and &lt;code&gt;infra/README.md&lt;/code&gt; says
plainly that it's unproven — ChronoLens was built and demoed entirely against a local
Foundry-installed SigNoz, with a rule-based explainer that needs no cloud.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What we'd build next
&lt;/h2&gt;

&lt;p&gt;Three things, in order of how much they'd change the product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rank the blast radius by business impact, not time-to-breach.&lt;/strong&gt; "Checkout falls in 40s" and
"the recommendations sidecar falls in 40s" are not the same incident, and the ordering should
know that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let LEARN tune its own confidence guard.&lt;/strong&gt; The noise floor and minimum-samples thresholds are
configured today; the ledger already records every false start, so the guard could calibrate
itself against its own history instead of a constant we picked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the AWS scaffold and prove it.&lt;/strong&gt; It's a reviewable SAM stack today and nothing more,
which is why the blog says so rather than implying otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else we'd change is plumbing. The idea we keep coming back to is the one this project&lt;br&gt;
started from: &lt;strong&gt;an outage that never happened should still leave evidence.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;ChronoLens&lt;/strong&gt; was built for the &lt;em&gt;Agents of SigNoz&lt;/em&gt; hackathon, Track 1 — AI &amp;amp; Agent Observability.&lt;br&gt;
Code, &lt;code&gt;casting.yaml&lt;/code&gt; and the importable dashboards:&lt;br&gt;
&lt;a href="https://github.com/greninja-op/ChronoLens" rel="noopener noreferrer"&gt;github.com/greninja-op/ChronoLens&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI assistants were used during this build — an agentic IDE for implementation, tests and drafting,&lt;br&gt;
including driving the SigNoz MCP server to create and inspect dashboards and alert rules. The&lt;br&gt;
architecture, the honesty rules the project is built on, every acceptance decision and everything&lt;br&gt;
that got cut were mine. Each claim in this post was verified against a live SigNoz deployment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>observability</category>
      <category>opentelemetry</category>
      <category>signoz</category>
      <category>ai</category>
    </item>
    <item>
      <title>ChronoLens</title>
      <dc:creator>Arjun Sabu</dc:creator>
      <pubDate>Sun, 26 Jul 2026 08:02:46 +0000</pubDate>
      <link>https://dev.to/greninja-op/afafdf-2197</link>
      <guid>https://dev.to/greninja-op/afafdf-2197</guid>
      <description>&lt;p&gt;DDSDSD&lt;/p&gt;

</description>
    </item>
    <item>
      <title>foresight</title>
      <dc:creator>Arjun Sabu</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:04:25 +0000</pubDate>
      <link>https://dev.to/greninja-op/blog-foresight-oi2</link>
      <guid>https://dev.to/greninja-op/blog-foresight-oi2</guid>
      <description>&lt;h1&gt;
  
  
  I built an agent that writes its own SigNoz dashboards after an incident
&lt;/h1&gt;

&lt;p&gt;The same incident always gets you twice.&lt;/p&gt;

&lt;p&gt;It blows up on a Tuesday, you scramble, you patch it. Three weeks later a cousin of that same incident strolls right past your monitoring because nobody went back and added an alert. The fix was "obvious" in hindsight. It just never got written down.&lt;/p&gt;

&lt;p&gt;So over a hackathon I built &lt;strong&gt;FORESIGHT&lt;/strong&gt; to kill that gap. It watches live SigNoz telemetry, predicts a breach before it fires, explains the cause in plain English, and then does the part I actually care about: it &lt;strong&gt;writes the SigNoz dashboard, alert rule, and runbook for you&lt;/strong&gt;, so the same thing gets caught next time. Observability that writes itself.&lt;/p&gt;

&lt;p&gt;Here's what actually worked, the SigNoz bits I leaned on hard, and a few things that ate an hour each and aren't in any doc.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Four stages, on a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FORESEE  → poll SigNoz trends, predict an SLO breach before it fires
EXPLAIN  → pull traces/metrics, produce a plain-English root cause
CODIFY   → auto-create a SigNoz dashboard + alert + runbook
LEARN    → measure its own MTTR from alert history, remember the runbook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I needed something realistic to break, so the monitored app is a tiny &lt;strong&gt;AI coffee-shop chatbot&lt;/strong&gt;. It emits OpenTelemetry spans with GenAI attributes, and it's got a "break knob" so I can inject faults live in a demo — a latency ramp, a token-cost ramp, a tool-error spike.&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%2Fkfj835clzy94qq86hhcl.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%2Fkfj835clzy94qq86hhcl.jpg" alt=" " width="798" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Right after I inject a fault: FORESEE calls the breach, EXPLAIN posts the cause, CODIFY reports the SigNoz stuff it just created — every step with a clickable link.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The GenAI telemetry (and one honest catch)
&lt;/h2&gt;

&lt;p&gt;Each chat request emits one server span, plus a span per tool call. The attribute names follow OpenTelemetry's &lt;strong&gt;GenAI semantic conventions&lt;/strong&gt;. Heads up: they're still &lt;em&gt;experimental&lt;/em&gt;. They live under &lt;code&gt;gen_ai.*&lt;/code&gt; and aren't stable yet, so pin a version. The ones I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gen_ai.request.model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gen_ai.operation.name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gen_ai.tool.name&lt;/code&gt; (on tool spans, with an OK/ERROR status)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the catch that tripped me up: &lt;strong&gt;there's no standard &lt;code&gt;gen_ai.usage.cost&lt;/code&gt; attribute.&lt;/strong&gt; The conventions cover the model and token counts. Cost? That's on you. I derive it from the token counts and attach it as a custom attribute — and I don't pretend it's part of the spec. If you're building cost dashboards, that number is yours to own.&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;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;SERVER&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# real or simulated LLM
&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;result&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;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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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;cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# derived, not standard
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One early decision paid off big: the app runs with &lt;strong&gt;no API key at all&lt;/strong&gt;, on a simulated LLM that emits the exact same telemetry shape. I could build and demo the whole pipeline offline, then swap in a real model only when I wanted to.&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%2Fzr616hjnt20g3cxknh0a.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%2Fzr616hjnt20g3cxknh0a.jpg" alt=" " width="798" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One &lt;code&gt;/chat&lt;/code&gt; request in SigNoz Traces. The `gen_ai.&lt;/em&gt;&lt;code&gt; attributes ride along on the span. &lt;/code&gt;gen_ai.usage.cost` is the one I added myself.*&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading and writing SigNoz: MCP first, REST as backup
&lt;/h2&gt;

&lt;p&gt;SigNoz ships an &lt;strong&gt;MCP server&lt;/strong&gt; (Model Context Protocol) that exposes services, traces, dashboards, and alerts as tools. FORESIGHT reaches for MCP first, every read and every write, and only drops to the REST API if MCP's not around. Both go through one error wrapper, so a SigNoz hiccup shows up cleanly instead of taking down the whole loop.&lt;/p&gt;

&lt;p&gt;Wiring MCP to a local SigNoz is one JSON block. The gotcha: the auth header is &lt;code&gt;SIGNOZ-API-KEY&lt;/code&gt;, hyphenated — &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;Authorization&lt;/code&gt;. I burned a few minutes on 401s before I actually read that line.&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;"mcpServers"&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;"signoz"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8000/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"SIGNOZ-API-KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-editor-api-key&amp;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="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;Here's the bit that matters most: &lt;strong&gt;reads go through the Query Builder, not raw SQL.&lt;/strong&gt; Every latency/error/request query is a builder request (&lt;code&gt;queryType: "builder"&lt;/code&gt;) with an aggregation like &lt;code&gt;p99(duration_nano)&lt;/code&gt; and a filter like &lt;code&gt;service.name = '&amp;lt;svc&amp;gt;'&lt;/code&gt;. Why bother? Because the same query definition drives both the FORESEE polling &lt;em&gt;and&lt;/em&gt; the panels CODIFY generates later. The alert threshold and the dashboard panel end up sharing the exact same query. One source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I care about: CODIFY writes the guards
&lt;/h2&gt;

&lt;p&gt;When an incident resolves, CODIFY builds three things — each on its own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;service-scoped dashboard&lt;/strong&gt; — p99, error rate, request rate. Plus gen_ai panels if it was an AI incident.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;threshold alert rule&lt;/strong&gt; — "above", target = the SLO, filtered to the service.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;runbook&lt;/strong&gt; — the story, the evidence, the likely cause, and links to the guards it just made.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The decision I'm happiest with: &lt;strong&gt;each artifact is attempted on its own.&lt;/strong&gt; If the alert fails to create, you still get the dashboard and the runbook, and the failure gets logged per-artifact instead of nuking the whole run. The only thing that stops everything is if the &lt;em&gt;failure recorder itself&lt;/em&gt; throws — the one case where pushing on would hide a real error.&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;for&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dashboard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_dashboard&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;alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_alert&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;runbook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_runbook&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                       &lt;span class="c1"&gt;# each returns an id + a SigNoz link on success
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;record_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# recorded, loop continues
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fgt073e7mmeqcph88li17.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%2Fgt073e7mmeqcph88li17.jpg" alt=" " width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nobody hand-built this. CODIFY created the dashboard and a matching p99 alert through the SigNoz API the moment the incident cleared.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it trustworthy: property tests caught real bugs
&lt;/h2&gt;

&lt;p&gt;I didn't want a demo that only survives the happy path. So the decision logic — slope prediction, breach math, health classification, the codify failure isolation — is covered by &lt;strong&gt;property-based tests&lt;/strong&gt; with Hypothesis, at least 100 generated cases each. This is where I learned the most, because the generators found bugs I'd never have typed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows filenames.&lt;/strong&gt; The runbook writer names files after the service. Hypothesis cheerfully generated a "service name" full of control characters — a perfectly legal SigNoz label, an illegal Windows filename. The write exploded. Real service names are fine, so I constrained the generator to realistic labels. Example-based tests would've sailed right past it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A wording mismatch.&lt;/strong&gt; One test asserted the cause string "token-cost spike" showed up in the signals list. The signal actually read "gen_ai token cost is $X/call." Same meaning, different words — the kind of disagreement only a fuzzer bothers to find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things to know if you pair Hypothesis with pytest fixtures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a function-scoped fixture inside &lt;code&gt;@given&lt;/code&gt; and Hypothesis throws a health check. It's fine when each example re-seeds the fixture — silence it with &lt;code&gt;@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the suite from the project root, not a single file. My &lt;code&gt;pytest.ini&lt;/code&gt; sets &lt;code&gt;pythonpath = src&lt;/code&gt;, and a few tests import a shared &lt;code&gt;conftest&lt;/code&gt; that only resolves on full-directory collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full circle: the agent watches itself
&lt;/h2&gt;

&lt;p&gt;FORESIGHT emits its own OpenTelemetry spans too — one per stage, linked under a single loop trace, each tagged with a &lt;code&gt;foresight.stage&lt;/code&gt; attribute and an outcome. So the predict→explain→codify run shows up in SigNoz, right next to the app it's monitoring.&lt;/p&gt;

&lt;p&gt;The emitter fails open. If the exporter's down, it logs and moves on. The last thing you want is your observability agent crashing on its own telemetry.&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%2F63t7d4ut01rpmqcx4c2y.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%2F63t7d4ut01rpmqcx4c2y.jpg" alt=" " width="798" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The watcher, watched. FORESIGHT's own investigation is a trace in SigNoz — one span per stage, tagged with &lt;code&gt;foresight.stage&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP first, REST underneath.&lt;/strong&gt; MCP kept the code clean; the REST fallback meant a hiccup never killed a demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is your number, not the spec's.&lt;/strong&gt; Don't sit around waiting for a &lt;code&gt;gen_ai.usage.cost&lt;/code&gt; convention that doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the failure-isolation before the happy path.&lt;/strong&gt; Bolting "keep going if one thing fails" on afterward is way more painful than building it in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let a fuzzer name your files.&lt;/strong&gt; The property tests earned their keep just by picking inputs I never would.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Small idea, but it stuck with me: the most valuable thing an incident produces isn't the fix — it's the &lt;em&gt;guard&lt;/em&gt; that stops the next one. And that guard almost never gets written. Hand that step to an agent, grounded in the same SigNoz queries a human would run, and it turns out to be both doable and genuinely useful.&lt;/p&gt;

&lt;p&gt;Want to try the pieces? The OpenTelemetry GenAI conventions live at &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" rel="noopener noreferrer"&gt;opentelemetry.io/docs/specs/semconv/gen-ai&lt;/a&gt;, and SigNoz's MCP server and Query Builder are what made the read/write half possible. Start by instrumenting one app with &lt;code&gt;gen_ai.*&lt;/code&gt; attributes and watching the traces land. Everything else builds from there.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked the OpenTelemetry convention details against the official semantic-conventions docs; the cost caveat reflects that there's no stable &lt;code&gt;gen_ai.usage.cost&lt;/code&gt; attribute at the time of writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>signoz</category>
    </item>
  </channel>
</rss>
