<?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: Thirumalaiboobathi B</title>
    <description>The latest articles on DEV Community by Thirumalaiboobathi B (@thirumalaiboobathi_b_2b20).</description>
    <link>https://dev.to/thirumalaiboobathi_b_2b20</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%2F3955723%2Fa912b054-6413-4173-9ace-4a317b3afe46.jpg</url>
      <title>DEV Community: Thirumalaiboobathi B</title>
      <link>https://dev.to/thirumalaiboobathi_b_2b20</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thirumalaiboobathi_b_2b20"/>
    <language>en</language>
    <item>
      <title>Everything Was Green. Nothing Was Listening. — Self-Hosting SigNoz for MCP Tool-Error Traces</title>
      <dc:creator>Thirumalaiboobathi B</dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:17:34 +0000</pubDate>
      <link>https://dev.to/thirumalaiboobathi_b_2b20/everything-was-green-nothing-was-listening-self-hosting-signoz-for-mcp-tool-error-traces-148p</link>
      <guid>https://dev.to/thirumalaiboobathi_b_2b20/everything-was-green-nothing-was-listening-self-hosting-signoz-for-mcp-tool-error-traces-148p</guid>
      <description>&lt;p&gt;Everything was green. &lt;code&gt;signoz-signoz-0&lt;/code&gt; was healthy. The UI answered on &lt;code&gt;localhost:8080&lt;/code&gt; with a clean HTTP 200. The collector logs said, in so many words, &lt;em&gt;everything is ready&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Nothing was listening on 4318.&lt;/p&gt;

&lt;p&gt;This is the story of two days spent self-hosting SigNoz to validate a library I'd published a week earlier, and what I found instead. Spoiler: the library works. The traces are shaped correctly. I still don't have a screenshot of them in the SigNoz UI, and I'd rather tell you why than pretend otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I was trying to prove
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://www.npmjs.com/package/opentel-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;opentel-mcp&lt;/code&gt;&lt;/a&gt;, a one-line OpenTelemetry instrumentation library for Model Context Protocol servers. Its one genuinely uncommon behaviour: MCP's &lt;code&gt;CallToolResult&lt;/code&gt; can come back with &lt;code&gt;isError: true&lt;/code&gt; while the JSON-RPC call itself succeeds. The transport is fine. The tool failed. Most instrumentation sees a 200-equivalent and moves on.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;opentel-mcp&lt;/code&gt; catches that case and marks the span &lt;code&gt;status: ERROR&lt;/code&gt; with &lt;code&gt;error.type: tool_error&lt;/code&gt;. As far as I can tell it's the only Node library doing this, though I'd be happy to be corrected.&lt;/p&gt;

&lt;p&gt;That's the claim. A claim like that deserves a real backend behind it, not just spans printed to stderr. SigNoz is OpenTelemetry-native and self-hostable, so it seemed like the honest test: point the exporter at a real ingester, open the trace explorer, and see whether the error actually surfaces the way I say it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo
&lt;/h2&gt;

&lt;p&gt;Small on purpose. One &lt;code&gt;McpServer&lt;/code&gt;, two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;echo&lt;/code&gt; — returns cleanly. Should produce an OK span.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch_weather&lt;/code&gt; — always returns &lt;code&gt;isError: true&lt;/code&gt;. Should produce the ERROR span with &lt;code&gt;error.type: tool_error&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instrumented in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;instrumentMcpServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;setupNodeSdk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exporterUrl&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One note that matters more than it looks: &lt;code&gt;opentel-mcp&lt;/code&gt; only ships &lt;code&gt;@opentelemetry/exporter-trace-otlp-http&lt;/code&gt;. There's no gRPC exporter. So &lt;code&gt;exporterUrl&lt;/code&gt; has to be the OTLP/HTTP URL &lt;strong&gt;with the full path&lt;/strong&gt; — &lt;code&gt;http://localhost:4318/v1/traces&lt;/code&gt; — not the bare 4317 gRPC port. Every backend guide lists both ports side by side and it's an easy thing to get wrong at 1am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting SigNoz: four things nobody told me
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;deploy/docker/docker-compose.yaml&lt;/code&gt; doesn't exist anymore
&lt;/h3&gt;

&lt;p&gt;Most of the guides you'll find (including the one in my own head) say clone the repo, &lt;code&gt;cd deploy/docker&lt;/code&gt;, &lt;code&gt;docker compose up -d&lt;/code&gt;. That path is gone. SigNoz has moved to Foundry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash
&lt;span class="c"&gt;# write a casting.yaml&lt;/span&gt;
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not worse, just different, and different in a way that silently invalidates every blog post written before it. Which is a nice bit of irony for a post like this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. My machine was under spec and Docker didn't say so
&lt;/h3&gt;

&lt;p&gt;7.28GB total RAM. Docker Desktop's WSL2 VM had helped itself to 3.47GB of it. SigNoz's documented minimum is 4GB.&lt;/p&gt;

&lt;p&gt;What that looks like in practice is not an error message. It looks like &lt;code&gt;signoz-signoz-0&lt;/code&gt; dying partway through a migration with &lt;code&gt;unexpected EOF&lt;/code&gt;. I bumped the VM to 5GB via &lt;code&gt;.wslconfig&lt;/code&gt; and &lt;code&gt;wsl --shutdown&lt;/code&gt;, which left Windows about 2.3GB to think about its life choices. Tight, but it got the container up.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The crash left a lock behind, and the lock outlived the container
&lt;/h3&gt;

&lt;p&gt;Here's the fun part. That first crashed migration wrote a row into &lt;code&gt;migration_lock&lt;/code&gt;. Postgres data lives on a volume, so the lock survived every restart afterwards and blocked each new boot. The container wasn't failing for the original reason anymore — it was failing because of the corpse of the original reason.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;signoz-metastore-postgres-0 &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-U&lt;/span&gt; signoz &lt;span class="nt"&gt;-d&lt;/span&gt; signoz &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"DELETE FROM migration_lock;"&lt;/span&gt;
docker restart signoz-signoz-0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had to do this twice. Restarting Docker Desktop doesn't clear it. The volume persists, so the lock persists.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Two self-inflicted wounds worth naming
&lt;/h3&gt;

&lt;p&gt;I ran &lt;code&gt;npm install&lt;/code&gt; from inside the example directory instead of the repo root. That gave the demo its own second copy of &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; and &lt;code&gt;@opentelemetry/api&lt;/code&gt;. Spans stopped printing. Not an error — just silence, which is the worst possible failure mode when you're debugging an observability tool. Classic dual-package hazard. Fixed by registering the example as a proper workspace member so everything shares one copy.&lt;/p&gt;

&lt;p&gt;Then: &lt;code&gt;BatchSpanProcessor&lt;/code&gt;'s flush timer is &lt;code&gt;unref()&lt;/code&gt;'d. For a short-lived script like this demo, the process can exit before the batched export ever fires, dropping every span silently. stderr output is unaffected because it's synchronous — so it &lt;em&gt;looks&lt;/em&gt; fine locally while nothing reaches your backend. Fixed by flushing on stdin close.&lt;/p&gt;

&lt;p&gt;Both of those are on me, not on SigNoz. Both are exactly the kind of thing that would have had me blaming the backend for an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where it works
&lt;/h2&gt;

&lt;p&gt;With all that cleared, the demo produces exactly what it should:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  name: 'tools/call echo',
  attributes: {
    'mcp.method.name': 'tools/call',
    'gen_ai.operation.name': 'execute_tool',
    'gen_ai.tool.name': 'echo',
    'mcp.tool.argument_count': 1,
    'jsonrpc.request.id': '1'
  },
  status: { code: 1 }
}
{
  name: 'tools/call fetch_weather',
  attributes: {
    'mcp.method.name': 'tools/call',
    'gen_ai.operation.name': 'execute_tool',
    'gen_ai.tool.name': 'fetch_weather',
    'mcp.tool.argument_count': 1,
    'jsonrpc.request.id': '2',
    'error.type': 'tool_error'
  },
  status: { code: 2 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status: { code: 1 }&lt;/code&gt; is OK. &lt;code&gt;status: { code: 2 }&lt;/code&gt; is ERROR, carrying &lt;code&gt;error.type: 'tool_error'&lt;/code&gt;. The tool failure got caught. The claim holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall
&lt;/h2&gt;

&lt;p&gt;Spans generated correctly. Exporter configured correctly. Backend healthy, UI up, collector satisfied with itself.&lt;/p&gt;

&lt;p&gt;Export times out.&lt;/p&gt;

&lt;p&gt;Before blaming anything I checked whether the port was actually open, from inside the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; signoz-network curlimages/curl:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://signoz-ingester-1:4318/v1/traces &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"resourceSpans":[]}'&lt;/span&gt;
&lt;span class="c"&gt;# HTTP 000 — connection refused&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, because container networking lies to you often enough to warrant it, straight from the process table inside the ingester:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;signoz-ingester-1 sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"cat /proc/net/tcp"&lt;/span&gt;
&lt;span class="c"&gt;# no listener on 4318&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No listener. Not a firewall, not a DNS thing, not my exporter. The OTLP receiver simply was never bound.&lt;/p&gt;

&lt;p&gt;The on-disk &lt;code&gt;ingester.yaml&lt;/code&gt; is correct — the receiver is right there in the config file. But the ingester runs with &lt;code&gt;--manager-config=/etc/opamp-config.yaml&lt;/code&gt;, which means it doesn't use that file as its source of truth. It fetches its &lt;em&gt;effective&lt;/em&gt; config remotely via OpAMP from the &lt;code&gt;signoz&lt;/code&gt; service. That handshake kept erroring, and on eventually connecting it appears to apply a fallback default config with no &lt;code&gt;otlp&lt;/code&gt; receiver in it.&lt;/p&gt;

&lt;p&gt;So the failure mode is: correct config on disk, ignored; broken config delivered over the wire; every health check green; nothing listening. Observability platform, unobservable.&lt;/p&gt;

&lt;p&gt;My leading hypothesis is version skew between two independently-tagged &lt;code&gt;:latest&lt;/code&gt; images — the ingester and the OpAMP server disagreeing about config schema, with the ingester falling back rather than failing loudly. I want to be clear about my confidence here: I'm certain about the symptom, and I've verified it three ways. The root cause is my best reading of the evidence, not a confirmed diagnosis. If someone from SigNoz reads this and tells me I cast a bad &lt;code&gt;casting.yaml&lt;/code&gt;, that's a completely plausible outcome and I'd like to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped anyway
&lt;/h2&gt;

&lt;p&gt;When the flush hit an unreachable ingester, my demo died with an uncaught exception and a stack trace out of &lt;code&gt;http-transport-utils.js&lt;/code&gt;. Ugly, and it points the finger at the wrong place — someone hitting that would reasonably assume the library broke.&lt;/p&gt;

&lt;p&gt;So I wrapped the shutdown path. Now it exits 0 with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;opentel-mcp: failed to flush spans to SigNoz: socket hang up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Correct blame. A backend being unreachable is a normal condition, not a crash, and a library that instruments error handling should probably model decent error handling.&lt;/p&gt;

&lt;p&gt;That fix is the one lasting artifact of two days of infrastructure debugging, and honestly it's a better outcome than the screenshot I came for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature I liked most, which I never got to use
&lt;/h2&gt;

&lt;p&gt;An awkward heading for a post that ends without a trace explorer screenshot. But my honest answer is the thing that caused the failure: &lt;strong&gt;OpAMP-managed remote configuration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think about what it's for. Fleets of collectors, centrally configured, no redeploy to change what you're collecting. That's a genuinely good idea and it's why the on-disk config was being ignored — the system was working exactly as designed, just with the wrong config on the other end of the wire. Static-config observability stacks don't have this failure mode, but they also don't let you retune a hundred collectors from one place.&lt;/p&gt;

&lt;p&gt;The lesson I'm taking is narrower and more useful than "SigNoz is broken," because I don't think it is: &lt;strong&gt;when your config is delivered rather than mounted, "the config file is correct" stops being evidence of anything.&lt;/strong&gt; Check the listener, not the YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repro, if you want to try
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml

&lt;span class="c"&gt;# if signoz-signoz-0 crash-loops:&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;signoz-metastore-postgres-0 &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-U&lt;/span&gt; signoz &lt;span class="nt"&gt;-d&lt;/span&gt; signoz &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"DELETE FROM migration_lock;"&lt;/span&gt;
docker restart signoz-signoz-0

&lt;span class="c"&gt;# from opentel-mcp repo root, NOT the example dir:&lt;/span&gt;
npm &lt;span class="nb"&gt;install
cd &lt;/span&gt;examples/signoz-demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; | node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And before you trust the trace explorer's emptiness to mean anything about your own code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;signoz-ingester-1 sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"cat /proc/net/tcp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full friction notes are in &lt;a href="https://github.com/Thirumalaiboobathi/opentel-mcp/blob/main/examples/signoz-demo/README.md" rel="noopener noreferrer"&gt;&lt;code&gt;examples/signoz-demo/README.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you've self-hosted SigNoz via Foundry recently and your ingester &lt;em&gt;did&lt;/em&gt; bind its OTLP receiver, I'd genuinely like to hear what your &lt;code&gt;casting.yaml&lt;/code&gt; looked like — there's a decent chance the answer is embarrassing and mine.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;opentel-mcp&lt;/code&gt; is on &lt;a href="https://www.npmjs.com/package/opentel-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt; and &lt;a href="https://github.com/Thirumalaiboobathi/opentel-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>node</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building AgriNova AI Assistant – My First Step into GenAI + Cloud</title>
      <dc:creator>Thirumalaiboobathi B</dc:creator>
      <pubDate>Thu, 28 May 2026 04:57:19 +0000</pubDate>
      <link>https://dev.to/thirumalaiboobathi_b_2b20/building-agrinova-ai-assistant-my-first-step-into-genai-cloud-2a1e</link>
      <guid>https://dev.to/thirumalaiboobathi_b_2b20/building-agrinova-ai-assistant-my-first-step-into-genai-cloud-2a1e</guid>
      <description>&lt;p&gt;Recently, I built AgriNova AI Assistant🌾🤖 — an AI-powered agriculture assistant designed to support farmers and agriculture-related guidance using Generative AI.&lt;/p&gt;

&lt;p&gt;This project became a major learning milestone for me because it combined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☁️ Cloud&lt;/li&gt;
&lt;li&gt;🤖 Generative AI&lt;/li&gt;
&lt;li&gt;⚙️ DevOps concepts&lt;/li&gt;
&lt;li&gt;🧠 AI agents&lt;/li&gt;
&lt;li&gt;🌐 Full Stack integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 What AgriNova AI Can Do&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provide agriculture-related guidance&lt;/li&gt;
&lt;li&gt;Answer farming queries using AI&lt;/li&gt;
&lt;li&gt;Generate smart responses using LLMs&lt;/li&gt;
&lt;li&gt;Work as an AI assistant/chat system&lt;/li&gt;
&lt;li&gt;Explore privacy-focused AI workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📚 What I Learned&lt;/p&gt;

&lt;p&gt;This project helped me understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How AI assistants actually work&lt;/li&gt;
&lt;li&gt;Basics of prompt engineering&lt;/li&gt;
&lt;li&gt;Integrating AI models into applications&lt;/li&gt;
&lt;li&gt;Running local LLMs with Ollama&lt;/li&gt;
&lt;li&gt;Connecting cloud + AI workflows&lt;/li&gt;
&lt;li&gt;Importance of scalable architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Biggest Takeaway&lt;/p&gt;

&lt;p&gt;Earlier, I used to spend more time only consuming content and scrolling.&lt;/p&gt;

&lt;p&gt;Now I’m trying to build consistently, learn publicly, and improve through real projects.&lt;/p&gt;

&lt;p&gt;This is just the beginning of my AI + Cloud journey ✨&lt;/p&gt;

&lt;p&gt;Big thanks to the AWS and developer communities that motivated me to start building and sharing publicly 🙌&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>genai</category>
      <category>awsnova</category>
    </item>
  </channel>
</rss>
