Percentiles don't average

There is a gap between "kubectl logs is enough" and "let's run Prometheus, Grafana and a time-series database". metrik lives in it: one native binary, one SQLite file, no scrape configuration and no service discovery.

It answers four questions — how many requests, how many errors, how slow, is memory running out — and writes to Telegram when the answers get worse.

The agent has to be free

An agent that costs the monitored service real latency will be turned off during the incident it was installed for. So the budget was: measurable, and small enough that nobody argues.

It comes to 106 nanoseconds per request and one UDP packet per minute. The aggregation happens in-process; nothing is scraped, so there is no endpoint to discover and no scheduler to misconfigure.

No JMX either. The JVM build reads Runtime and ProcessHandle; the native build reads /proc.

Percentiles do not average

This is the one I would put on a poster.

Three instances report p99 latencies of 100 ms, 100 ms and 400 ms. What is the p99 of the service?

It is not 200 ms. The average of percentiles is not a percentile of anything — it is a number with no meaning that happens to be in the right range, which is worse than no number, because it looks fine.

metrik ships exponential histograms and merges them. Merging histograms and then reading the quantile gives the actual p99 across instances, and it stays correct when one instance is having a bad time and the others are not — which is precisely when you are looking.

Route templates are the other half of keeping this honest. Series are labelled /users/{id}, not /users/8134, so cardinality stays bounded instead of growing one series per user.

"Worse" needs "worse after what"

A latency chart that goes up tells you nothing you can act on. The useful question is always what changed, and the answer is almost always a deploy.

So every chart carries deploy markers. It is a small feature and it is the one that turns looking at a graph into finding a cause.

Alerts fire on error rate, latency, memory and silence — with hysteresis and a cooldown, because an alert that flaps is an alert that gets muted. Silence matters as much as the rest: a service that stopped reporting looks identical to a healthy one on every chart.

Three ways to read it

The dashboard is Compose Multiplatform, light and dark. There is also a terminal client — the same numbers, one native binary, no browser.

And there is an MCP endpoint, so an agent can query the metrics directly instead of being handed a link to a page it cannot see. That turns out to be the one I use most: "why did p99 move at 14:20" is a question worth asking in the same place you are already working.

Deployment

Zero runtime dependencies — Kotlin/Native, storage in SQLite through sqlx4k, and authentication delegated to whatever reverse proxy is already in front (oauth2-proxy, Traefik, NGINX). One binary, one file.


Source: github.com/youndie/metrik · live at metrik.kotlin.website