A.ILIEVSKI/BLOG

JUNKYARD V2 · #22026-10-12~7 min read

A spec-compliant OTLP receiver, brought to you by a dead SSD

This was supposed to be the promised post about writing a spec-compliant OTLP receiver without embedding the OpenTelemetry Collector. Then my laptop, running Windows 11, decided to corrupt its entire SSD, and it briefly became a post about data recovery. It is now about both. The OTLP receiver exists. My old data does not. I am an unhappy but wiser new customer of OneDrive, and this post is three days before the deadline. Yes, I know. But as Mike Tyson said once, everyone has a plan and then they get punched in the SSD.

The dog ate my SSD

One night after school my laptop simply declined to boot. Not a blue screen, not a sad face with a QR code, those I could have worked with / had someone to blame. Instead I get a lovely message saying that there is no boot partition on my SSD. In disbelief, knowing that a mere 90 minutes ago the laptop was running fine and dandy, I tried restarting it about 12 times. That... did nothing. Clearly...

I spent about 6 hours trying to recover my data with any software I could find claiming it was "the best". Recovery tools, a live USB, command-line incantations I half understood, the usual stages of grief.

Denial: it's probably just a loose connection, I opened the laptop to clean it and remove + reinsert the SSD. Outcome = Pointless

Anger: WELL IT WORKED FINE BARELY 2 HOURS AGO SOMEONE MUST HAVE PUT A VIRUS ON MY MACHINE AS A JOKE. Yeah sure thing Mr. High Value Target.

Bargaining: Please Mr Gates I have a conference to attend and maybe 15h of uncommited code I will never complain again!

and eventually acceptance: I gave up and did a clean reinstall. It was almost midnight and I was at a loss for words (and for my data).

Here's the part where the story has a moral. Everything I had pushed survived: the repo was on GitHub, the blog was deployed, the architecture spec was committed. Everything that lived only on that disk: local notes, school projects, a folder containing about 21GB of mods named something like mods-backup-final-final(2) is gone, forever, may it rest. In the first post I wrote "committed early and often" as a productivity principle. Windows reinterpreted it as a survival strategy.

Jot this one down dear and gentle reader

Your boot partition is not promised to you. Your work exists exactly as many times as you have pushed it. A local commit, while essential, is quite fickle as it turns out. Just an unpublished fan fiction. I am now an unhappy but wiser newly minted customer of OneDrive, everything syncs as the Source folder is in the OneDrive, and I push like the disk is already on fire. Because for all you know, that hard-shutdown might have been harder than intended.

The reckoning

In post #1 there's a nice table. Week 12, October 5–11, is called "buffer: public demo, write-up, breathe." As I write this it is October 12 and the Datadog Summit in Paris is in three days. By the original plan I should be breathing. Oh sweet reader, I am not breathing. I probably look like one of the victims of the "Waterloo Effect".

What happened between July and now is a longer post for a marginally wiser man, but the short version is: life, school, and one (1) freshly formatted SSD. The honest scoreboard is that the code is at week 1 of a 12-week plan. This blog is called Building in Public, not Succeeding on Schedule in Public, so here it is in public: the timeline slipped, badly, and I reserve the right to retcon the milestone table and pretend it never happened. While ridicule and complaints are welcome, just make sure they're correctly addressed to spam@mrandrej.com.

What I refuse to do is show up with nothing. So the scope changed from "mini-Datadog" which was already impossible to "the walking skeleton, but done properly": OTLP logs in, query out, tests, CI. The thing post #1 said weeks 1–2 would exit with. It exits with it now. I will confess that I have tried improving my understanding of Otel in the meantime, partially for work and partially for this project. The main lesson I've learned is that there is quite a bit to know and quite a bigger bit that I do NOT know.

The OTLP receiver, as promised

Junkyard v2 does not embed the OpenTelemetry Collector. It implements the OTLP service interfaces directly using the generated stubs from go.opentelemetry.io/proto/otlp: a gRPC server on :4317 and an HTTP handler on :4318 that speaks both application/x-protobuf and application/json on POST /v1/logs. The reasoning is unchanged from the spec: embedding the Collector would be less code, and it would hide exactly the parts worth understanding. Which brings us to the two things I promised to explain.

What partial_success actually means. Every OTLP export response carries an optional partial_success field with a rejected_log_records count and an error message. It's how a server says "your request was valid, I accepted the batch, and I still dropped 83 of your 1,000 records, here's why." That is a completely different statement from rejecting the whole request with an error status, and conflating the two is how you lose data silently or retry storms are born. Most client SDKs log it and move on, which is exactly why the count has to be real. Junkyard's walking skeleton accepts or rejects whole batches, so on success it honestly reports zero rejected records - but the field is there, populated, and not decorative.

Backpressure is a feature you design, not a bug you fix. The queue is a fixed-capacity Go channel, and that is the entire queue. When it's full, the receiver doesn't block waiting for the disk to catch up: it tells the client to go away and come back later, RESOURCE_EXHAUSTED on gRPC, 503 with a Retry-After header on HTTP, exactly like the spec's retryable-error rules describe, and increments a dropped-batch metric so the loss is visible instead of silent. A slow disk can degrade Junkyard, but it can never stall every connected client behind one full buffer. In post #1 I called this "a lesson I paid for in advance." I have now paid for it twice, the second installment being one SSD.

Every request still travels the same path:

receiver → normalize → [bounded channel] → batcher → single storage writer

The batcher accumulates until 500 records or one second, whichever comes first, and hands one transaction to a single writer goroutine. SQLite's single-writer limitation stops being a problem you fight and becomes a property you design around. The store itself is the evolved v1 design: WAL mode, indexed on timestamp and service, with trace_id/span_id columns ready for the day traces exist. One fun gotcha: FTS5 isn't compiled into the SQLite driver on my fresh Windows install without a build tag, so full-text search quietly falls back to LIKE. Windows has now cost me one SSD and one full-text index. The score is not close.

And because v1's original sin was "no tests, no CI," v2 has table-driven unit tests for the pipeline, the receivers, and the store - including the shutdown-drain path, because a batcher that loses its final partial batch on exit is just a slower way to drop data - plus GitHub Actions running gofmt, go vet, go test, and go build on every push. Here's the whole thing working, unedited, one curl in and one curl out:

$ curl -X POST localhost:4318/v1/logs \
    -H "Content-Type: application/json" -d @smoke.json
{"partialSuccess":{}}

$ curl "localhost:8080/api/v1/logs?service=smoke"
[{"timestamp":"2026-10-12T20:26:40Z","severity_number":9,
  "severity_text":"INFO","body":"hello from smoke test",
  "service":"smoke"}]

Paris, in three days

So what do I bring to the Summit? Not the mini-Datadog, perhaps a hotdog if I'm feeling peckish. A walking skeleton that does one signal end to end and does it honestly, an architecture document I can defend, tests that were there from day one, and a very good story about why the commit graph has a three-month canyon in it. If you're a Datadog SWE and you're reading this, hello again, stranger. The plan changed; the point didn't.

Next up: metrics and the custom TSDB, on a new and significantly more honest timeline. And backups. So many backups.

← All posts github.com/mr-andrej/junkyard →