<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>OpenTelemetry on CtrlSpice</title><link>https://ctrlspice.codes/tags/opentelemetry/</link><description>Recent content in OpenTelemetry on CtrlSpice</description><generator>Hugo</generator><language>en-ca</language><lastBuildDate>Mon, 24 Aug 2026 12:00:00 -0700</lastBuildDate><atom:link href="https://ctrlspice.codes/tags/opentelemetry/index.xml" rel="self" type="application/rss+xml"/><item><title>I rebuilt otel-desktop-viewer on top of DuckDB</title><link>https://ctrlspice.codes/rebuilding-otel-desktop-viewer/</link><pubDate>Mon, 24 Aug 2026 12:00:00 -0700</pubDate><guid>https://ctrlspice.codes/rebuilding-otel-desktop-viewer/</guid><description>and all I got was locally searchable traces, metrics, and logs.</description><content:encoded><![CDATA[<p>The project crossed 1,000 stars on GitHub yesterday, and <a href="https://github.com/CtrlSpice/otel-desktop-viewer/releases/tag/v0.5.0">v0.5.0</a> just went out the door, full of bug fixes!
Together, that felt like a good excuse for a proper reintroduction, three years and one DuckDB rewrite after I shipped the first version.</p>
<h2 id="whats-an-otel-desktop-viewer-anyway">What&rsquo;s an otel-desktop-viewer anyway?</h2>
<p><img alt="A trace waterfall in otel-desktop-viewer: thirty spans across six services, each service in its own colour, with the span detail pane open on the right." loading="lazy" src="/rebuilding-otel-desktop-viewer/hero.png"></p>
<p><code>otel-desktop-viewer</code> is a single binary that lets you search and query your OpenTelemetry traces, metrics, and logs locally in your browser.
Inside, a lightweight Collector receives your telemetry, an embedded <a href="https://duckdb.org/">DuckDB</a> stores and queries the data, and a Svelte UI puts it on screen.
It <a href="https://github.com/CtrlSpice/otel-desktop-viewer#getting-started">installs in a single step</a>, and there&rsquo;s no backend to run, no compose file, and no storage to configure.</p>
<p>Fundamentally, a local debugger has different needs than a tool built for production.
A production tool&rsquo;s job is to help you find relevant telemetry in a big ol&rsquo; pile of data.
Some of that gets in the way when working locally.</p>
<p>Think about being dropped into a query builder first thing.
What do I even type in?
We&rsquo;re running locally, and we might only have four traces total.
We shouldn&rsquo;t need to search for them at all.</p>
<p>So <code>otel-desktop-viewer</code> shows you everything as it arrives, the way <code>tail -f</code> does for a log file.
It&rsquo;s all right there, and you can search it once there&rsquo;s enough data to be worth narrowing down.</p>
<h2 id="why-duckdb">Why DuckDB</h2>
<p>Storage has the same problem.
OTLP data needs somewhere to go, and every backend you could send it to wants its own deployment.
DuckDB is an embeddable columnar database, so it gets compiled into the binary and telemetry goes straight into it.</p>
<p>Here&rsquo;s what that bought, and how:</p>
<p><img alt="Typing a query into the search box: the text is syntax highlighted as it goes, and submitting it filters the trace list down to matching traces." loading="lazy" src="/rebuilding-otel-desktop-viewer/search.gif"></p>
<ul>
<li>
<p><strong>When you do need to search, you can query your telemetry meaningfully instead of just scrolling through it.</strong>
The same syntax works on traces, metrics, and logs, and you can find a span by an event inside it or by the span it links to.
Some examples of things you can type into the search box:</p>
<pre tabindex="0"><code>event.name = exception
service.name = cart AND statusCode = Error
duration &gt; 1000000000
name CONTAINS checkout
</code></pre></li>
<li>
<p><strong>A trace with thousands of spans opens fast and stays smooth while you scroll.</strong>
Span trees are built by the database: a recursive CTE walks the parent-child links and hands back rows already in the order the waterfall draws them, so nothing in the browser has to assemble a tree.
Let DuckDB handle it.</p>
</li>
<li>
<p><strong>Drawing a chart doesn&rsquo;t stall the UI.</strong>
Quantiles, histogram merges, and cumulative and delta handling all happen in the query, and so does getting a few thousand datapoints down to chart size.
Y&rsquo;all, metrics maths with DuckDB is so cool, and this is me teasing the next blog post with the subtlety of a possum.</p>
</li>
<li>
<p><strong>The store stays small.</strong>
Attributes live in a content-addressed dictionary shared across spans, logs, metrics, and everything else, so each distinct key/value pair is stored once no matter how many things carry it.
On my test capture that turns 723,692 attribute rows into 267 dictionary entries.</p>
</li>
<li>
<p><strong>Jump between logs, metrics, and traces using trace context. They&rsquo;re all connected.</strong>
All three signals live in the same store, so following a trace ID across them is a lookup.</p>
</li>
<li>
<p><strong>Your data can be a file.</strong>
By default nothing is written to disk: you look at your data, close the tab, and it&rsquo;s gone.
Pass <code>--db</code> and you get a DuckDB file you can come back to, send to a colleague, or hand to an agent.</p>
</li>
</ul>
<h2 id="traces">Traces</h2>
<p><img alt="The three-pane traces view: the trace list on the left with span count badges, a waterfall of coloured span bars in the middle, and the span detail pane on the right." loading="lazy" src="/rebuilding-otel-desktop-viewer/traces-three-pane.png"></p>
<p>Traces open as a waterfall, with all the usual waterfall things.
Collapse subtrees, or move through them from the keyboard.
The list is on the left, span details on the right, and this checkout trace runs 74 spans.</p>
<p>Errors are designed to stand out.
Span bars get a colour per service, and anything carrying an <code>Error</code> status or an exception event drops out of that rotation and takes the error colour instead.
Red always means the same thing, no matter how many services are in play.</p>
<figure>
    <img loading="lazy" src="/rebuilding-otel-desktop-viewer/traces-errors.png"
         alt="The same waterfall with the sidebar collapsed: red error spans stand out among the coloured ones, and the exception event is open in the detail pane showing its message, stacktrace, and type."/> <figcaption>
            <p>8 red spans out of 93.</p>
        </figcaption>
</figure>

<p>Events and links are both clickable.
Event dots sit on the span bar, and clicking one puts the span and the event in the URL, so you can bookmark the exact thing you were looking at and come back to it.
Links take you to the linked span, in whatever trace it lives in.</p>
<p><img alt="The Links panel open for a consumer span, showing the linked trace ID and span ID as clickable links." loading="lazy" src="/rebuilding-otel-desktop-viewer/traces-links.png"></p>
<h2 id="metrics">Metrics</h2>
<p><img alt="The metrics drawer filtered to five metrics, showing gauge, counter, up-down counter, and histogram badges with last values, and a rate chart open for the selected counter." loading="lazy" src="/rebuilding-otel-desktop-viewer/metrics-instruments.png"></p>
<p>All five OTel instrument shapes are supported: gauges, counters, up-down counters, histograms, and exponential histograms, though the demo app in these screenshots only emits the first four.
The chart you get depends on which one you picked, and you&rsquo;re only offered the aggregations that mean something for it.
I learned a lot of metrics maths so you don&rsquo;t have to.</p>
<p>Histograms get three views of the same data: a heatmap, a quantile view you can flip between p50, p95, and p99, and the bucket distribution itself.
Or ignore all that and lookit the pretty graphs!</p>
<p><img alt="A latency histogram on the heatmap view, with the heatmap, quantiles, and histogram tabs at the top and the per-series list on the right." loading="lazy" src="/rebuilding-otel-desktop-viewer/metrics-heatmap.png"></p>
<p><img alt="The same histogram on the quantiles view with p99 selected, showing stepped per-series quantile lines." loading="lazy" src="/rebuilding-otel-desktop-viewer/metrics-quantiles.png"></p>
<p>Anything with more than one series overlays by default, with a legend, per-series sparklines, and min/max/average overlays you can switch on.
I have spent way too long toggling those overlays on and off for funsies, because they&rsquo;re pretty and my squirrel brain has needs.</p>
<p><img alt="A six-series counter on the rate view with a pinned crosshair: a per-series value table, min and max markers, a slope readout, and per-series sparklines in the panel on the right." loading="lazy" src="/rebuilding-otel-desktop-viewer/metrics-series.png"></p>
<p>Some datapoints arrive carrying exemplars: pointers back to the spans that produced the number.
Those say so, and clicking one takes you straight there.</p>
<p><img alt="A histogram datapoint expanded to show five exemplars, each with its value, timestamp, and clickable trace and span links." loading="lazy" src="/rebuilding-otel-desktop-viewer/metrics-exemplars.png"></p>
<h2 id="logs">Logs</h2>
<p><img alt="The logs list filtered by severityNumber &gt;= 17, with error badges on each record and the selected record&rsquo;s detail pane showing an exception message and stacktrace." loading="lazy" src="/rebuilding-otel-desktop-viewer/logs-severity.png"></p>
<p>Logs are searchable on every field, including severity as a number, so <code>severityNumber &gt;= 17</code> gets you ERROR and above without guessing whether the emitter wrote <code>Error</code>, <code>ERROR</code>, or <code>err</code>.</p>
<p>A record that arrived with trace context shows its trace and span IDs, and clicking either takes you to that span in its trace.
No copying an ID out of one pane and pasting it into another.</p>
<p><img alt="The detail pane for a log record that arrived with trace context: the trace ID and span ID render as underlined links above the record&rsquo;s attributes." loading="lazy" src="/rebuilding-otel-desktop-viewer/logs-trace-context.png"></p>
<h2 id="coming-up">Coming up</h2>
<p><strong>Sharing:</strong>
You can already move the store around, since it&rsquo;s a file.
I still want to build a way to export a slice of what you&rsquo;re looking at, and a way to load someone else&rsquo;s back in.
That waits on the schema settling down, because asking people to trade files whose layout changes every month would be rude.</p>
<p><strong>Agents:</strong>
The world of observability looks different from when I started this project in 2023.
Developer tools now have to serve agents as well as people, and an agent could drive the same query surface the UI uses.
Instead of handing you a summary you have to take on faith, it can show you exactly where to look in your own data, so you can see the problem for yourself.</p>
<p>More on that soon.</p>
<h2 id="try-it-out">Try it out</h2>
<p>Point an OTLP exporter at it and your telemetry shows up in the browser.
<a href="https://github.com/CtrlSpice/otel-desktop-viewer#getting-started">The README</a> has install instructions for Homebrew, Docker, <code>go install</code>, and prebuilt binaries.
Issues and PRs are welcome, as always.</p>
]]></content:encoded></item></channel></rss>