A.ILIEVSKI/BLOG

JUNKYARD V2 · #12026-07-20~8 min read

Rebuilding Junkyard: a mini-Datadog from scratch

I have a "working" log aggregator, a self-imposed conference deadline, and a job interview to prepare for. This is why I'm scrapping the first one and rebuilding the architecture into something better. Junkyard v1 was meant to be DIY solution duct-taped together in a panic to satisfy a need for a project for Epitech where we were given pitiful VMs with 2GB of RAM that could barely keep themselves turned on.

The deadline

On October 15, 2026, there's a Datadog Summit in Paris. I registered, and I asked to chat with a Datadog engineer, mentioning, perhaps optimistically, my Go observability project. When my M.Sc. at Epitech finishes in 2027, I want to land a junior Software Engineer role there.

The funny thing being that this isn't my first attempt at this. When I finished my first Master's (BAC +5) in software architecture through OpenClassrooms I also applied perhaps 5 times to Datadog for a CDI. I got 0 interviews out of that, cue the tiny violin music and dim the lights.

Datadog's interviews are not framework trivia, whatever that means exactly. Similarly to some FAANG companies, they most likely probe systems thinking: concurrency, backpressure, distributed-systems trade-offs, production-ready code. So the project can't just exist by October. It has to be the kind of thing you can defend line by line in a design discussion. Twelve weeks out, my current project is not that thing.

If you're a Datadog SWE and you're reading this, hello there stranger. If you aren't and you're reading this, please forward it to anyone you know at Datadog. I will be bound to you by honor for at least 3 years, I pinkie promise!

What Junkyard v1 actually is

Junkyard v1 is a lightweight log aggregator I built in Go: HTTP and syslog ingestion, a SQLite store (WAL mode, FTS5 full-text search), a query API, a web UI, and a CLI. It genuinely worked. It collected logs from multiple VMs into one searchable place. Though the networking changes I had to do to get to there were an absolute nightmare.

Safe to say it was rushed. Here is a fun little list:

Google search on how to exit Vim, with a simple answer of No

The foundation is good, the SQLite log store design carries over, but v1 demonstrates "I can build a thing for logs," not "I can reason about an ingestion pipeline under load." Though I would like to say that due to some bad setup on our part, we were indeed getting about 15,000 logs per day at some point. My thing of an app only crapped out and died when the logs ate up all the remaining storage space on the VM. So the plan for now: rebuild, in "public", with a decision log. The v1 code stays in git history under a v1.0.0 tag. The rebuild from scratch is part of the story, not something to hide.

The target: a mini-Datadog

I understand how it reads, like I haven't been sleeping enough for a month and now I'm rambling. But Junkyard v2 is/will be/should be a personal observability platform, scoped to what one person can build well in twelve weeks:

The architecture

demo app Go · OTel SDK OTLP JUNKYARD-SERVER OTLP receivers Logs · Metrics · Traces · gRPC :4317 / HTTP :4318 ingest pipeline validate → bounded queue → batch → single writer backpressure: 503 / RESOURCE_EXHAUSTED metrics custom TSDB head + WAL segments + rollups logs SQLite FTS5 search traces SQLite lookup by trace_id query API + web UI /api/v1/{logs,traces,metrics} · /metrics self-instrumentation /metrics Datadog free tier dogfooding you browser · curl
fig. 1: one monolith binary, three signals, three storage engines. The diagram is hand-written inline SVG. It follows the site's light/dark theme.

Two things to notice. First, it's a monolith: one binary runs receivers, pipeline, storage, and query API. Splitting this into microservices would add deployment complexity with zero learning benefit at this scale, is what I should say. The truth is I don't feel like frying my brain any more than required. The interesting concurrency lives inside the process, not between processes. Second, there are three different storage answers for the three signals. That's deliberate, for now. As I have very little clue what I'm doing I reserve the right to retcon anything I say and pretend it never happened.

The ingestion pipeline

Every request travels the same path:

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

The queue is a fixed-capacity Go channel per signal type, and that's the entire queue. When it's full, the receiver doesn't block: it tells the client to back off (RESOURCE_EXHAUSTED on gRPC, 503 + Retry-After on HTTP, per the OTLP spec) and increments a dropped-batch metric. A slow disk can never stall every connected client.

The batcher accumulates points until a size or time threshold, then hands one batch to a single writer goroutine per signal type. That serializes all writes, which turns SQLite's single-writer limitation from a problem you fight into a property you design around.

Trade-off, stated out loud

The queue is in-memory, so a crash loses whatever hasn't been flushed: seconds of data. A durable, Kafka-style ingest queue would close that window and is explicitly out of scope. For a demo platform, bounded memory and simple reasoning beat durability guarantees I don't need.

Storage: three signals, three answers

Metrics get a custom TSDB: the centerpiece, and the part most likely to teach me humility. An in-memory head block (series ID → append-only samples) behind a write-ahead log, flushed periodically into immutable segment files, with a background job downsampling raw data into 1-minute and 1-hour rollups:

data/
├── wal/            # replayed on startup
├── head/           # in-memory, recent window
└── segments/       # immutable flushed blocks
    └── rollup/     # 1m → 1h downsampling

Retention becomes trivial: raw data lives 7 days, 1-minute rollups 30 days, 1-hour rollups a year, and "expiring data" is just deleting old segment directories. That property alone justifies immutable segments.

Logs and traces stay on SQLite. The v1 log store already proved itself (WAL mode, FTS5). It gains trace_id/span_id columns so logs and traces can reference each other. Traces get a span-oriented schema where fetching a whole trace is one indexed lookup.

Why not ClickHouse / VictoriaMetrics / Elasticsearch?

Because they hide exactly what I'm trying to learn. A general-purpose TSDB for all three signals is a multi-month project, but metrics alone are the right target: uniform data model, append-heavy workload, and a rich downsampling story. Every simplification gets a paragraph in the repo's decision log. The naivety is deliberate and documented, which is what makes it defensible in an interview rather than merely naive. In theory.

The plan

Twelve weeks, five milestones, committed early and often: a visible three-month commit history is worth more than a polished weekend dump:

Weeks (2026) Milestone Exit criteria
1–2 · Jul 20–Aug 2 Walking skeleton OTLP logs in, query out, CI running tests, on GitHub from day one
3–6 · Aug 3–30 Metrics + traces All three signals ingested, TSDB head/WAL/segments, rollup job
7–9 · Aug 31–Sep 20 Kubernetes helm install works on kind from a clean clone
10–11 · Sep 21–Oct 4 Polish README + architecture diagram, Datadog dogfooding, screenshots
12 · Oct 5–11 Buffer Public demo, write-up, breathe

Hard deadline: Datadog Summit Paris, October 15. The project needs to exist and be presentable by then, which is exactly why week 12 is called "buffer" and not "victory lap".

The full architecture spec and decision log (both shorter than this post, I promise) live in the repo at docs/V2-ARCHITECTURE.md, and the milestones are tracked as GitHub issues. This blog runs in parallel: each post will take one slice (the OTLP receiver, the WAL, crash recovery, the Helm chart) and go deeper than a commit message can.

Next up: Writing a spec-compliant OTLP receiver without embedding the OpenTelemetry Collector: what partial_success actually means, and why backpressure is a feature you design, not a bug you fix. A lesson I paid for in advance.

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