<?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: Tarun Prajapati</title>
    <description>The latest articles on DEV Community by Tarun Prajapati (@tarun_prajapati_b2b3f4b39).</description>
    <link>https://dev.to/tarun_prajapati_b2b3f4b39</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%2F3397249%2F8b4b2474-0c30-44fd-9529-c85c0c0aecea.png</url>
      <title>DEV Community: Tarun Prajapati</title>
      <link>https://dev.to/tarun_prajapati_b2b3f4b39</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tarun_prajapati_b2b3f4b39"/>
    <language>en</language>
    <item>
      <title>GuardrailOps: What Happens After Your AI Guardrail Blocks Something?</title>
      <dc:creator>Tarun Prajapati</dc:creator>
      <pubDate>Sun, 26 Jul 2026 17:50:28 +0000</pubDate>
      <link>https://dev.to/tarun_prajapati_b2b3f4b39/guardrailops-what-happens-after-your-ai-guardrail-blocks-something-3hmg</link>
      <guid>https://dev.to/tarun_prajapati_b2b3f4b39/guardrailops-what-happens-after-your-ai-guardrail-blocks-something-3hmg</guid>
      <description>&lt;p&gt;Repo: &lt;a href="https://github.com/tarunprajapati88/GuardrailOps" rel="noopener noreferrer"&gt;github.com/tarunprajapati88/GuardrailOps&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: guardrails that protect but don't observe
&lt;/h2&gt;

&lt;p&gt;Picture a companion AI or mental-health chatbot at 2 AM. A user types something like &lt;em&gt;"I don't want to be here anymore."&lt;/em&gt; A decent guardrail will catch that and block the raw response from reaching the user unfiltered.&lt;/p&gt;

&lt;p&gt;But then what? In most setups — nothing. The block happens silently, in application memory, and disappears. Nobody on the team knows a user was in crisis. Nobody knows if the same user is hammering the system with jailbreak attempts five minutes later. The guardrail &lt;em&gt;protected&lt;/em&gt; the interaction, but nobody was &lt;em&gt;watching&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;That distinction — protection vs. observability — is the whole reason GuardrailOps exists. Existing tools like NeMo Guardrails, Guardrails AI, and &lt;code&gt;@openai/guardrails&lt;/code&gt; all answer "is this text bad?" None of them answer "when your AI fails a vulnerable user at 3 AM, how do you know — and what do you do about it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What GuardrailOps actually does
&lt;/h2&gt;

&lt;p&gt;GuardrailOps is a drop-in wrapper around your existing LLM client (OpenAI, Ollama, vLLM, Groq, DeepSeek — anything OpenAI-compatible). It sits between your app and the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wrapWithGuardrailOps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guardrailops&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;llmClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./llm-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapWithGuardrailOps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;llmClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mental-health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abuse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jailbreak&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;illegal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;classifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llama-guard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Meta Llama Guard 3 via Ollama&lt;/span&gt;
  &lt;span class="na"&gt;llamaGuard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:11434&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llama-guard3:1b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;otel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-chatbot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exporterEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:4318&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-llm-model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every call now gets classified before it reaches the model. Safe traffic flows through untouched. Anything flagged gets blocked with a safe fallback, and — this is the part other guardrail libraries skip — every decision gets emitted as telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Here's the full request path, from user message to SRE getting paged:&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%2Fc9gpgk0dquacr2hnw030.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%2Fc9gpgk0dquacr2hnw030.png" alt=" " width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Six moving pieces, none of them optional:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The SDK proxy&lt;/strong&gt; wraps your existing client — your application code barely changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A two-layer classifier&lt;/strong&gt; — Llama Guard 3 as the primary model, plus a sub-millisecond regex pre-filter as a fast-path net for obvious jailbreak/persona-hijack attempts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stateless severity scorer&lt;/strong&gt; decides BLOCK vs ALLOW and whether the event is serious enough to page someone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An OTel span emitter&lt;/strong&gt; ships every decision — not the raw message — to an OpenTelemetry Collector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Collector scrubs PII&lt;/strong&gt; before anything touches SigNoz's ClickHouse store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz's Alert Engine and MCP Server&lt;/strong&gt; turn raw trace data into Slack pages and natural-language triage.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The two-layer classifier
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 — Primary&lt;/td&gt;
&lt;td&gt;Meta Llama Guard 3 (1B), local via Ollama (or NVIDIA NIM / OpenAI Moderation as swappable backends)&lt;/td&gt;
&lt;td&gt;13 MLCommons safety categories (S1–S13)&lt;/td&gt;
&lt;td&gt;~480ms&lt;/td&gt;
&lt;td&gt;$0 — nothing leaves your infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — Fast path&lt;/td&gt;
&lt;td&gt;Heuristic regex pre-filter&lt;/td&gt;
&lt;td&gt;DAN prompts, persona hijacks, base64-encoded jailbreak attempts&lt;/td&gt;
&lt;td&gt;&amp;lt;1ms&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The privacy/latency tradeoff is explicit and intentional: running Llama Guard 3 locally costs roughly 10x the latency of a cloud moderation API call, in exchange for zero data leaving your infrastructure and zero per-request fees. For a mental-health or companion-AI use case, that tradeoff is usually worth it.&lt;/p&gt;

&lt;p&gt;Llama Guard 3's S1–S13 taxonomy gets mapped down to five developer-friendly domains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MLCommons category&lt;/th&gt;
&lt;th&gt;GuardrailOps domain&lt;/th&gt;
&lt;th&gt;Default action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S11 (Suicide &amp;amp; Self-Harm)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mental-health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK + 988 Lifeline + page SRE, &lt;strong&gt;+0 threat points&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S5, S7, S10, S12 (Defamation, Privacy, Hate, Sexual)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;abuse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK + flag user (+10 pts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S1, S2, S3, S4, S9 (Violent, Non-Violent, Sex, CSAM, CBRN)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;illegal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK + flag + push alert (+25 pts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layer-2 regex hits&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jailbreak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK + flag + push alert (+15 pts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S6, S8, S13 (Specialized advice, IP, Elections)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;off-topic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLOCK (+5 pts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;mental-health&lt;/code&gt; row is the core design decision in the whole project: &lt;strong&gt;crisis users are protected, not penalized.&lt;/strong&gt; They get zero threat points added to their account, so a genuine crisis never escalates someone into a &lt;code&gt;RESTRICTED&lt;/code&gt; or &lt;code&gt;BLOCKED&lt;/code&gt; state. The Slack alert exists to bring a human into the loop, not to punish the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing it work: the demo app
&lt;/h2&gt;

&lt;p&gt;The repo ships a MindBot demo chatbot with preset buttons for each scenario — Distress, Crisis, Abuse, Jailbreak — so you can watch the pipeline react without writing a test harness.&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%2Fkbxmp2srvmlabbzstw44.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%2Fkbxmp2srvmlabbzstw44.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each blocked response shows exactly what fired — domain, category, and which engine caught it, down to the millisecond. In this run, a "give me instructions to synthesize meth" prompt was caught by Llama Guard 3 in 6ms and tagged &lt;code&gt;illegal / violent_crimes&lt;/code&gt;, and the Slack SRE alert badge confirms the page went out — all without the raw prompt ever leaving the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lands: SigNoz
&lt;/h2&gt;

&lt;p&gt;Every one of those blocked events becomes a standard OpenTelemetry GenAI span with custom &lt;code&gt;guardrail.*&lt;/code&gt; attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gen_ai.system                    = "guardrailops"
gen_ai.request.model             = "llama-guard3:1b"
guardrail.triggered              = true
guardrail.action                 = "BLOCKED"
guardrail.domain                 = "illegal"
guardrail.category               = "violent_crimes"
guardrail.crisis.severity        = "CRITICAL"
guardrail.classifier             = "llama-guard"
guardrail.classifier.latency_ms  = 480
guardrail.user.id                = "usr_sha256_e3b0c442"
guardrail.session_id             = "sess_demo_100"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those spans feed a pre-built SigNoz dashboard (ships as &lt;code&gt;dashboard.json&lt;/code&gt;, one-click import) that turns raw blocks into something an SRE can actually read at a glance:&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%2Fme7c6umsq8krnszno13y.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%2Fme7c6umsq8krnszno13y.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a glance: which threat domains are spiking, which classifier engine is catching what, the breakdown of violation categories (suicide/self-harm, prompt injection, violent crimes), and — critically — a repeat-offender leaderboard by hashed user ID. That last panel is what turns "we got jailbreak attempts today" into "this specific user has hit us 14 times."&lt;/p&gt;

&lt;p&gt;User identifiers are SHA-256 hashed before they're ever exported, and the raw prompt/response text is stripped at the OTel Collector layer — SigNoz only ever sees the metadata, never the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  From block to page: Slack
&lt;/h2&gt;

&lt;p&gt;When something crosses a severity threshold, SigNoz's Alert Engine fires a webhook to a relay service, which posts a Block Kit card straight into &lt;code&gt;#alerts&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;On-call SREs don't have to leave Slack to investigate, either — a Slack MCP bot lets you query live SigNoz trace data conversationally:&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%2Flteocny34wla39mfz7ru.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%2Flteocny34wla39mfz7ru.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@GuardrailOpsBot show trace for session sess_ms1umloe_7fw5ma&lt;/code&gt; pulls the session's classification history straight from ClickHouse, no dashboard click-through required. &lt;code&gt;@GuardrailOpsBot summary of today&lt;/code&gt; gives a fleet-wide block-rate and incident count for the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately doesn't store
&lt;/h2&gt;

&lt;p&gt;Given the domains involved (mental health, abuse, illegal content), the privacy story matters as much as the safety story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ The user's actual message text&lt;/li&gt;
&lt;li&gt;❌ The LLM's response content&lt;/li&gt;
&lt;li&gt;❌ Chat history or conversation context&lt;/li&gt;
&lt;li&gt;❌ Raw user identifiers (email, name, IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GuardrailOps also ships GDPR-oriented APIs out of the box — &lt;code&gt;clearUser()&lt;/code&gt; for right-to-be-forgotten requests, and &lt;code&gt;setThreatTTL()&lt;/code&gt; so threat scores decay automatically rather than persisting indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;If you're building a companion AI, a mental-health support bot, or really any consumer-facing chatbot handling sensitive input, the pitch is simple: your guardrail should not be a black box that silently swallows the worst moments in a user's day. GuardrailOps is a proof of concept for making that visible — who triggered what, when, and whether it was a bad actor or someone who needed help.&lt;/p&gt;

&lt;p&gt;It's a solo hackathon build (developed with AI-assisted coding, disclosed per the SigNoz "Agents of SigNoz" hackathon rules — architecture and safety logic are original work), not a clinically validated medical tool. It's not a replacement for professional mental health care. But as a pattern for wiring safety decisions into observability infrastructure that already exists in most teams' stacks, it's a starting point worth building on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full source, setup instructions, and the complete OTel span reference are in the README:&lt;/strong&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/tarunprajapati88/GuardrailOps" rel="noopener noreferrer"&gt;github.com/tarunprajapati88/GuardrailOps&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Couldn't See What My AI App Was Doing — So I Added OpenTelemetry and SigNoz in One Afternoon</title>
      <dc:creator>Tarun Prajapati</dc:creator>
      <pubDate>Sat, 18 Jul 2026 16:42:25 +0000</pubDate>
      <link>https://dev.to/tarun_prajapati_b2b3f4b39/i-couldnt-see-what-my-ai-app-was-doing-so-i-added-opentelemetry-and-signoz-in-one-afternoon-3ihc</link>
      <guid>https://dev.to/tarun_prajapati_b2b3f4b39/i-couldnt-see-what-my-ai-app-was-doing-so-i-added-opentelemetry-and-signoz-in-one-afternoon-3ihc</guid>
      <description>&lt;p&gt;Nebva is an AI content-generation SaaS I'm building on Node.js, TypeScript, MongoDB, and Redis, with Gemini doing the actual generation work. Until recently, the only telemetry it had was a homemade buffer that wrote events into MongoDB and hoped I'd go query them later. When a request was slow, or a generation got blocked, I had no real way to find out why — just logs and guesses. Here's what changed in one afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js/Express backend (swap for your own stack)&lt;/li&gt;
&lt;li&gt;MongoDB + Redis running&lt;/li&gt;
&lt;li&gt;A Gemini API key (or whichever LLM you're calling)&lt;/li&gt;
&lt;li&gt;SigNoz running locally (Docker Compose or Foundry)&lt;/li&gt;
&lt;li&gt;No prior OpenTelemetry experience required&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Was Flying Blind On
&lt;/h2&gt;

&lt;p&gt;Nebva's only telemetry used to be &lt;code&gt;TelemetryProxyProvider.ts&lt;/code&gt; — a manual buffer that wrote events straight into MongoDB. It worked, in the sense that events got written. What it couldn't do was connect anything: an LLM call, the storage write that followed it, and the notification that went out after — three separate entries with no shared ID between them. If a request felt slow, I had no way to tell whether the delay was Gemini, MongoDB, or my own code. There was no dashboard and no alerting, just documents sitting in a collection I'd query by hand when something broke.&lt;/p&gt;

&lt;p&gt;A single content-generation request in Nebva actually moves through several distinct stages — pulling signal, generating an embedding, calling Gemini for the actual content, writing the result to storage. None of that structure was visible anywhere. It was just "the request," one opaque block of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One File That Changed Everything
&lt;/h2&gt;

&lt;p&gt;Auto-instrumentation is the fastest win in the whole migration, and it lives entirely in one file: &lt;code&gt;instrumentation.ts&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// instrumentation.ts — REPLACE the body below with your actual file&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NodeSDK&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/sdk-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getNodeAutoInstrumentations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/auto-instrumentations-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OTLPTraceExporter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/exporter-trace-otlp-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/resources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SemanticResourceAttributes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/semantic-conventions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NodeSDK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;SemanticResourceAttributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nebva-backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;traceExporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OTLPTraceExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4318/v1/traces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;instrumentations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;getNodeAutoInstrumentations&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ This file has to load before anything else in your app — before your routes, before your business logic. OpenTelemetry's own docs are explicit about this: import it after your other modules and the instrumentations silently do nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it for infrastructure. Every Express route, every Mongoose query, every outgoing HTTP call, and every Redis operation now shows up as a span, without touching a single line of business code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping My AI Provider Without Touching It
&lt;/h2&gt;

&lt;p&gt;Auto-instrumentation covers infrastructure. It has no idea what "generating a blog post" means, or that a guardrail just blocked an output — those are Nebva-specific concepts, and there's no way for a generic instrumentation library to invent attributes for things it doesn't know about.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;TelemetryProxyProvider&lt;/code&gt; is for. It implements the same &lt;code&gt;IAIProvider&lt;/code&gt; interface every other part of the codebase already calls, so nothing upstream had to change. Underneath, every call gets wrapped in a span tagged with OpenTelemetry's GenAI attributes — &lt;code&gt;gen_ai.usage.total_tokens&lt;/code&gt;, &lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;, model name, and so on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TelemetryProxyProvider.ts — REPLACE with your actual implementation&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TelemetryProxyProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;IAIProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IAIProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GenerationInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActiveSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.request.model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inner&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.usage.total_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalTokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gen_ai.usage.output_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputTokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part I actually think is worth stealing: I used the same pattern for Nebva's content-safety checks. Every time a guardrail blocks a generation — a soft refusal, a hate-speech match, whatever the reason — that's now also a span attribute: &lt;code&gt;gen_ai.guardrail.triggered&lt;/code&gt; and &lt;code&gt;gen_ai.guardrail.reason&lt;/code&gt;, tagged against the user who triggered it. It didn't need a separate system. Same Decorator, same spans, one more attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Looks Like in SigNoz
&lt;/h2&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%2Ft2kk96tvv03g05b2qly9.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%2Ft2kk96tvv03g05b2qly9.png" alt=" " width="799" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once real traffic runs through it, the Services page breaks the whole pipeline into stages, each with its own P50/P95/P99:&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%2Fy949ux1yxw17cupqzsd1.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%2Fy949ux1yxw17cupqzsd1.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd assumed the pipeline was roughly evenly slow across the board. It isn't — image generation alone is the outlier, and errors cluster entirely on the write path.&lt;/p&gt;

&lt;p&gt;The guardrail attributes turned into a dashboard with no extra plumbing — same span data, a different query:&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%2Fc4vwj0z0i4a4j6rz7lbw.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%2Fc4vwj0z0i4a4j6rz7lbw.png" alt=" " width="800" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Things I'd Tell My Past Self
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto-instrumentation gets you infrastructure for free. It will never know your business.&lt;/strong&gt; Express, Mongoose, HTTP, Redis — all free. "This generation got blocked for a safety reason" is not a concept OpenTelemetry has ever heard of, and it's worth tagging that from day one instead of bolting it on later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch your async queues. Nebva uses BullMQ for background jobs. By default, BullMQ jobs show up as completely disconnected traces in SigNoz. Auto-instrumentation doesn't automatically propagate the OTel context across the Redis queue boundary unless you explicitly inject the trace context into the job payload and extract it on the worker side. If you don't do this, your trace waterfall breaks the moment a job hits the queue.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;One file for infrastructure, one Decorator for business logic, and a MongoDB buffer I could finally delete. If you're running any LLM behind an API and flying blind on what it's actually doing, this is the smallest version of the fix. &lt;/p&gt;

&lt;p&gt;If I had more time, the next things I'd add: alerting on guardrail-trigger spikes per user, a cost dashboard broken out by generation type instead of just totals, and tracing through the queue side of the pipeline to see whether async jobs stay connected to the request that started them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opentelemenetry</category>
      <category>signoz</category>
      <category>node</category>
    </item>
  </channel>
</rss>
