. -->
Instrumenting my espresso machine with OpenTelemetry | ClickHouse<br>Skip to content
Open searchOpen region selectorEnglish<br>Japanese<br>Korean<br>Chinese<br>French<br>Spanish<br>Portuguese<br>Arabic
48.9kSign inGet Started
->Scroll to top<br>BackBlog<br>Engineering<br>Copy pageCopied!More actionsView as Markdown Open this page in Markdown<br>Open in ChatGPT Ask questions about this page<br>Open in Claude Ask questions about this page<br>Open in v0 Ask questions about this page
Instrumenting my espresso machine with OpenTelemetry
Jordan Simonovski<br>Jul 29, 2026 · 23 minutes read
Most observability stories start with a production incident. This one starts with a bad shot of espresso.
If you've never chased good espresso, here's the problem in one paragraph. Starting out, it feels simple: grind the beans, tamp them flat, run hot water through at pressure. But the moment coffee stops being purely functional and becomes something you want to perfect, you discover the entire universe is working against you. Humidity changes how your beans grind overnight. Grind size changes how fast water moves through the puck. Pressure shapes which flavors you extract, and extraction time decides whether you land on sweetness or on something bitter and burnt. The beans themselves are a moving target, aging week to week. Every one of these variables affects the others, and your only feedback is a taste and a vague memory of yesterday's attempt.
What I'm trying to do here is what you'd do to any misbehaving system: measure as many of those variables as I can, store them somewhere I can query, and start controlling them one at a time.
I have a Gaggia espresso machine running GaggiMate, an open-source ESP32 controller that replaces the stock brain with a touchscreen, a PID loop, and pressure profiling. It's a genuinely impressive bit of kit. But when a shot came out sour or gushed through the puck, I had no idea why. Was the grind too coarse? Did the boiler temperature sag mid-pull? Did water channel through one side of the puck instead of extracting evenly?
These are exactly the questions observability is built to answer, for distributed systems anyway. So I treated the espresso machine like any other service I'd want to debug in production: I instrumented it with OpenTelemetry, shipped the telemetry to ClickHouse Cloud, and put ClickStack on top of it.
It turns out an espresso machine is a surprisingly honest distributed system, and the same stack that runs observability for real infrastructure handles a microcontroller pulling a 30-second shot without breaking a sweat. This is the nerdy version of that story: the protobuf, the FreeRTOS scheduling, the ring buffers, the custom collector, and the agent that ties it together.
Why ClickStack for an ESP32? #
ClickStack is ClickHouse's observability stack: an OpenTelemetry collector for ingest, ClickHouse for storage and queries, and HyperDX as the UI for logs, metrics, traces, and session replay. The whole point is that it's schema-agnostic and OTel-native. It doesn't care whether telemetry comes from a Kubernetes fleet or a coffee machine, as long as it speaks OTLP.
That last part is what made this project viable. I didn't have to invent a data model. A shot of espresso maps cleanly onto concepts ClickStack already understands:
Metrics: boiler temperature and target, boiler pressure (bar), pump flow rate, puck resistance, scale weight (grams), plus cumulative counters for total shots, brew time, water consumed, and connectivity events.
Traces: each shot is a parent span, with child spans for each brew phase (pre-infusion, ramp, extraction). Each shot span carries 20+ attributes: peak pressure, flow rates, temperature stability, puck resistance variance.
Not everything belongs in a span, though. Temperature, pressure, and flow move too quickly during a shot to be captured accurately in a single event, so the fast-moving state ships as metrics and the shot itself ships as a trace. A shot is a request. The phases are child spans. The quality numbers are span attributes. Once you see it that way, the espresso machine is just a service with a very short, very tasty trace.
A trace outlining each of the phases configured in my extraction profile. Each phase is a span: pre-infusion, bloom, ramp, hold, decline.
Getting OTLP onto a microcontroller #
The hard part with the implementation of OTel was the hardware. An ESP32 has a couple of hundred kilobytes of usable RAM and a real-time control loop it cannot abandon. Three problems showed up immediately.
1. The OTLP protobuf schema is too big for an ESP32 #
OTLP is defined as protobuf, which is great: there's a mature embedded protobuf library, nanopb, built exactly for microcontrollers. But feeding it the full upstream OTLP definitions produced this at compile time:
1#error Enable PB_FIELD_32BIT to support messages exceeding 64kB in size:<br>2 otlp_ScopeMetrics, otlp_ResourceMetrics, otlp_ExportMetricsServiceRequestCopy command
The full schema carries...