<?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: Jesutofunmi</title>
    <description>The latest articles on DEV Community by Jesutofunmi (@codebreak_er).</description>
    <link>https://dev.to/codebreak_er</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%2F4029363%2F8dc7ecd3-9787-401f-aa0a-4504eed41b0c.jpg</url>
      <title>DEV Community: Jesutofunmi</title>
      <link>https://dev.to/codebreak_er</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codebreak_er"/>
    <language>en</language>
    <item>
      <title>The 500 That Told Me Nothing — Until I Self-Hosted SigNoz</title>
      <dc:creator>Jesutofunmi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 20:08:04 +0000</pubDate>
      <link>https://dev.to/codebreak_er/the-500-that-told-me-nothing-until-i-self-hosted-signoz-3mk9</link>
      <guid>https://dev.to/codebreak_er/the-500-that-told-me-nothing-until-i-self-hosted-signoz-3mk9</guid>
      <description>&lt;p&gt;A few days ago, a route in one of my side projects started returning this:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"unhandled"&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="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"HTTPError"&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;That's it. That's the whole error. No stack trace in the response, nothing useful in the handler — my own &lt;code&gt;try/catch&lt;/code&gt; never even fired. The browser got a 500, my code swore it did nothing wrong, and I spent longer than I'd like to admit staring at both.&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%2Fev8f18i7h0748soapkcn.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%2Fev8f18i7h0748soapkcn.png" alt="The opaque 500 response in the browser" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I eventually found the cause (more on that later — it's a genuinely sneaky one), but the experience exposed a bigger problem: I could only see what happened inside my handler. Once the request left my code and passed back through the framework, I was blind. &lt;strong&gt;If an error can happen outside my own code, &lt;code&gt;console.log&lt;/code&gt; will never be enough.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I needed was a way to follow the entire request — through my handler, the framework, and every operation in between — and see exactly where it failed. That is what distributed tracing is built for. To try it properly, I chose SigNoz: an open-source, OpenTelemetry-native observability platform I could self-host for free. Then I built a tiny reproduction app, instrumented it, and pointed it at my local SigNoz instance.&lt;/p&gt;

&lt;p&gt;This post is what actually happened — including the parts where things didn't work, because those turned out to be the most instructive bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug, revisited — what tracing could and couldn't see
&lt;/h2&gt;

&lt;p&gt;Once the reproduction app was sending traces, I went straight back to the problem that started all of this: what &lt;em&gt;was&lt;/em&gt; my opaque 500? In TanStack Start, this innocent-looking handler is broken:&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="nx"&gt;GET&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;request&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="nf"&gt;setCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demo_session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;httpOnly&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&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;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;/&lt;/span&gt;&lt;span class="dl"&gt;'&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;302&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;&lt;code&gt;Response.redirect()&lt;/code&gt; returns a Response whose headers are &lt;strong&gt;immutable&lt;/strong&gt; — that's the Fetch spec, not a framework quirk. The framework then tries to append the &lt;code&gt;Set-Cookie&lt;/code&gt; header to the returned response &lt;em&gt;after the handler has returned&lt;/em&gt;. Appending to immutable headers throws &lt;code&gt;TypeError: immutable&lt;/code&gt;, the framework converts it to a generic 500, and your &lt;code&gt;try/catch&lt;/code&gt; never sees a thing, because the explosion happens outside your code. (The fix: build the redirect by hand with &lt;code&gt;new Response(null, { status: 302, headers: { location } })&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Here's the part that taught me something about observability. In SigNoz, the &lt;code&gt;/bug&lt;/code&gt; requests show up as &lt;strong&gt;error spans&lt;/strong&gt; — they're why the error rate chart works. But the &lt;strong&gt;Exceptions tab stayed empty&lt;/strong&gt;. Why? Because nothing ever called &lt;code&gt;recordException&lt;/code&gt;. The framework swallowed the &lt;code&gt;TypeError&lt;/code&gt; and returned a 500; the span was marked as an error by its status code, but no exception &lt;em&gt;event&lt;/em&gt; was ever attached to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status-code errors and recorded exceptions are different signals.&lt;/strong&gt; If your code never sees the error, neither does your exception tracking — only the trace does.&lt;/p&gt;

&lt;p&gt;To prove the contrast, I added &lt;code&gt;/crash&lt;/code&gt;, a failure that is instrumented the way failures should be:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cannot read properties of undefined (reading 'amount')&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&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;recordException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpanStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment processing failed&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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 trace view shows it as a red, failed span:&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%2F5l0sskvywtmgqmaa6fpo.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%2F5l0sskvywtmgqmaa6fpo.png" alt="Crash trace with error span" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And &lt;em&gt;this&lt;/em&gt; one shows up in the Exceptions tab, grouped by type, with a count and timeline:&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%2Fqoise1p334l1zaarpnxi.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%2Fqoise1p334l1zaarpnxi.png" alt="Exceptions tab showing the recorded TypeError" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same status code as &lt;code&gt;/bug&lt;/code&gt;. Completely different visibility. That contrast — an invisible failure next to an instrumented one, in the same service, on the same screen — is the clearest argument for tracing I've come across.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else SigNoz showed me
&lt;/h2&gt;

&lt;p&gt;Once I understood the original bug, I explored what the same instrumentation revealed across the rest of the app. A few minutes of clicking around, and the Services page already had opinions:&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%2Fcv6t1nbrm8ag9x4kc5xr.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%2Fcv6t1nbrm8ag9x4kc5xr.png" alt="Services overview with error rate" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;P99 latency, throughput, and an &lt;strong&gt;error rate&lt;/strong&gt; — computed entirely from auto-instrumented spans. I never wrote separate metrics code. The 500s from &lt;code&gt;/bug&lt;/code&gt; were already there as a percentage on a chart, which made the service-level impact of the failure visible before I opened an individual trace.&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%2Fcsvfywruvspudx22hfsd.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%2Fcsvfywruvspudx22hfsd.png" alt="Service detail charts" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service detail page also has a Key Operations table that ranks every span by latency — including my hand-written &lt;code&gt;query-database&lt;/code&gt; and &lt;code&gt;load-user-session&lt;/code&gt; spans, sitting alongside the auto-captured HTTP and TLS spans, with P50/P95/P99 for each. Zero dashboard configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following a slow request
&lt;/h3&gt;

&lt;p&gt;Opening a single &lt;code&gt;/slow&lt;/code&gt; request shows the anatomy of the whole 3.56 seconds:&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%2F0vss6qhxhrzku7u360qb.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%2F0vss6qhxhrzku7u360qb.png" alt="Trace waterfall for the slow route" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GET request contains &lt;code&gt;load-user-session&lt;/code&gt; (120ms), then &lt;code&gt;query-database&lt;/code&gt; (340ms), then the TCP + TLS handshake and the outbound GitHub API call. Every "why is this page slow?" conversation I've ever had would have been shorter with this view.&lt;/p&gt;

&lt;p&gt;A detail I didn't expect: each trace contained &lt;strong&gt;two&lt;/strong&gt; GET server spans. Vite's dev mode runs application code in a worker process behind a small internal proxy — and OpenTelemetry's context propagation stitched the proxy process and the worker process into a single coherent trace, automatically, across process boundaries. That's the machinery working exactly as designed, visible on a laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature I like most
&lt;/h2&gt;

&lt;p&gt;After a few hours of poking at dashboards, alerts, logs, and explorers, the feature I keep coming back to is the &lt;strong&gt;trace detail view&lt;/strong&gt; — the flamegraph plus span waterfall.&lt;/p&gt;

&lt;p&gt;Everything else in SigNoz is aggregate: charts, percentiles, rates. All useful, all things other tools also do. But the trace view answers the question aggregates can't: &lt;em&gt;what happened to this one request?&lt;/em&gt; It turned my invisible framework bug into a red span with a timestamp and a duration. It showed me my dev server was secretly two processes. It put my hand-written spans, the framework's HTTP handling, and a third-party API call on one timeline with no effort beyond starting the SDK.&lt;/p&gt;

&lt;p&gt;And because SigNoz is OpenTelemetry-native, getting there required nothing proprietary — one exporter URL. If I ever want to point the same app somewhere else, it's a one-line change. That's the right kind of dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you if you're trying this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the SigNoz account before debugging "broken" OTLP ports.&lt;/strong&gt; The collector's receivers don't start until an org exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter dev-server noise&lt;/strong&gt; with &lt;code&gt;ignoreIncomingRequestHook&lt;/code&gt;, or your traces are unreadable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;recordException&lt;/code&gt; is not optional&lt;/strong&gt; if you want the Exceptions view to mean anything. Span status alone won't do it.&lt;/li&gt;
&lt;li&gt;Windows works fine via the Docker Compose files, even though the shiny new installer says otherwise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole demo app is a handful of files — an &lt;code&gt;instrumentation.mjs&lt;/code&gt;, four routes, and a docker-compose you didn't have to write. For a free, self-hosted stack, the distance between "nothing" and "I can see every request my app makes" turned out to be about an afternoon — most of which was me learning these pitfalls so you don't have to.&lt;/p&gt;

&lt;p&gt;Those are the lessons I would carry into the next project. If you want to reproduce the experiment yourself, here is the exact setup that produced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows 11, Docker Desktop (WSL2 backend)&lt;/li&gt;
&lt;li&gt;A minimal &lt;a href="https://tanstack.com/start" rel="noopener noreferrer"&gt;TanStack Start&lt;/a&gt; app (React + file-based routing with server routes), Node 24&lt;/li&gt;
&lt;li&gt;SigNoz v0.99.0, self-hosted with Docker Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo app has four routes, each designed to produce a different kind of telemetry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/slow&lt;/code&gt; — a deliberately multi-step request: two fake I/O waits wrapped in manual spans, plus a real outbound call to the GitHub API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/bug&lt;/code&gt; — a faithful reproduction of my opaque 500&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fixed&lt;/code&gt; — the same logic as &lt;code&gt;/bug&lt;/code&gt;, done correctly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/crash&lt;/code&gt; — a failure that is &lt;em&gt;properly&lt;/em&gt; instrumented&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-hosting SigNoz (the Windows detour)
&lt;/h2&gt;

&lt;p&gt;SigNoz's current docs push a new installer called Foundry — which plainly states that &lt;strong&gt;Windows is not officially supported&lt;/strong&gt;. The older Docker Compose files still ship in the repo though, and the last compose-based release is the &lt;em&gt;current&lt;/em&gt; release, so on Windows this works fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; https://github.com/SigNoz/signoz.git
&lt;span class="nb"&gt;cd &lt;/span&gt;signoz
git sparse-checkout &lt;span class="nb"&gt;set &lt;/span&gt;deploy
git checkout v0.99.0
&lt;span class="nb"&gt;cd &lt;/span&gt;deploy/docker
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run pulls a few gigabytes (ClickHouse, ZooKeeper, the SigNoz server, and the OTel collector), then the stack boots in order: ClickHouse → schema migrator → SigNoz server → collector. A few minutes later the UI is up at &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One honest note from the logs: the server warns loudly if &lt;code&gt;SIGNOZ_JWT_SECRET&lt;/code&gt; isn't set. Fine for a laptop demo, but set it if you deploy this anywhere real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup pitfall: the OTLP ports are dead until you create an account
&lt;/h3&gt;

&lt;p&gt;This one cost me actual debugging time, so I'm writing it down for the next person.&lt;/p&gt;

&lt;p&gt;After the stack came up, the collector logged &lt;code&gt;Everything is ready. Begin running and processing data.&lt;/code&gt; — and yet nothing could connect to the OTLP endpoints on ports 4317/4318. Connection refused, even container-to-container. The ports were mapped, the config said &lt;code&gt;0.0.0.0:4318&lt;/code&gt;, the collector claimed to be alive.&lt;/p&gt;

&lt;p&gt;The answer was in the &lt;em&gt;SigNoz server's&lt;/em&gt; logs, not the collector's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to find or create agent ... cannot create agent without orgId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SigNoz's collector runs in &lt;strong&gt;managed mode&lt;/strong&gt;: it fetches its live configuration from the SigNoz server over OpAMP, and it won't start its receivers until it registers. Registration requires an organization to exist — and the organization is only created &lt;strong&gt;when you set up the first admin account in the UI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the fix for "my OTLP ports won't open" was: open &lt;code&gt;localhost:8080&lt;/code&gt;, create the login. Thirty seconds later the collector registered and ports 4317/4318 came alive. Every span my app had sent before that moment was silently dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumenting a TanStack Start app
&lt;/h2&gt;

&lt;p&gt;With SigNoz running, the last step is connecting the application to it. The Node OpenTelemetry SDK gets preloaded into the dev server via &lt;code&gt;NODE_OPTIONS&lt;/code&gt;. The whole setup is one file:&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="c1"&gt;// instrumentation.mjs&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="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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NOISY_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;@/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/node_modules/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;vite/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/favicon/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;css/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;js&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&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;serviceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;otel-playground&lt;/span&gt;&lt;span class="dl"&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/instrumentation-fs&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;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/instrumentation-http&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;ignoreIncomingRequestHook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;NOISY_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;??&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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env NODE_OPTIONS=--import=./instrumentation.mjs vite dev --port 3001"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;ignoreIncomingRequestHook&lt;/code&gt; matters in dev. Vite serves a storm of module and asset requests; without the filter, your traces drown in &lt;code&gt;/@vite/client&lt;/code&gt; noise.&lt;/li&gt;
&lt;li&gt;Custom spans are just a few lines inside a server function:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;otel-playground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&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;query-database&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;simulateDbCall&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire integration. No agent daemon, no vendor SDK — the OTLP endpoint is the only SigNoz-specific thing in the app, which means none of this is lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo code&lt;/strong&gt; (the app, all four routes, and the instrumentation file): &lt;a href="https://github.com/devtofunmi/otel-playground" rel="noopener noreferrer"&gt;github.com/devtofunmi/otel-playground&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz self-hosting docs&lt;/strong&gt;: &lt;a href="https://signoz.io/docs/install/self-host/" rel="noopener noreferrer"&gt;signoz.io/docs/install/self-host&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry JS getting started&lt;/strong&gt;: &lt;a href="https://opentelemetry.io/docs/languages/js/" rel="noopener noreferrer"&gt;opentelemetry.io/docs/languages/js&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Written for the &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz hackathon&lt;/a&gt; early-blog challenge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Transparency note: I used an AI assistant (Claude) to help draft and edit this post. Every install step, error message, trace, and screenshot is from my own machine — including the hours those pitfalls cost me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>signoz</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
