# Where the project stands

What works end to end, what it costs on the one device it has run on, what was found and not fixed, and what the release publishes.

Source: https://jmrplens.github.io/mikroscope/about/status/

This page answers what a reader deciding whether to try mikroscope needs first:
which parts work end to end, on what hardware that was shown, what the
observer costs today, which defects are known and still open, and whether
there is anything to download. It is checked against the code as of 2026-09-16.

## What works end to end

Everything below except the test suites and the seven sinks marked as such has
run against the reference RB5009 (RouterOS 7.24.2), not only against fakes:

- **The agent** reads the shared kernel's `/proc`, `/sys`, `/dev/kmsg` and
  `perf_event_open` counters on a fixed ticker at 1 to 100 Hz (10 Hz by
  default; 10, 50 and 100 Hz measured), keeps the samples in a ring, and serves
  them: `/healthz`, `/capabilities`, `/snapshot`, `/stream`, `/metrics`, and
  the triggered-capture endpoints `/captures` and `/capture`.
- **The deployment CLI** installs, upgrades and removes it — `doctor`, `plan`,
  `install`, `status`, `upgrade`, `uninstall` — with every write listed before
  it happens and every removal verified by ownership counts. On 2026-09-12,
  `doctor` → `install` → `status` → `upgrade` → `uninstall` left the router's
  `/export` byte-identical, compared by hash in memory and never written to
  disk. The agent answered 3 s after install, at a 5–7 ms round trip. On
  2026-09-17 all four install routes were run against the same device, one
  after another and each removed before the next: the checkout build, the
  published agent tar, the router's own pull from Docker Hub, and the
  `plan --rsc` script imported on the router with no CLI in the install itself.
  The agent answered in every one, and the `/export` after the four was
  byte-identical to the one before them.
- **`record`, `mark` and `plot`** work end to end. A 60 s recording at 10 Hz on
  2026-09-12 yielded exactly 600 samples, 0 gaps and −7 ms of clock skew.
- **`forward`**, the collector, merges the kernel tier with the RouterOS API
  tier and writes to ten sinks. File, Prometheus and InfluxDB 3 have run from
  the RB5009; Loki, OTLP, Graphite, Elasticsearch, SQL, Telegraf and stdout
  have not yet had router samples pushed through them, but every one of them
  now writes into the real product — InfluxDB 3, PostgreSQL, Elasticsearch,
  Graphite, Loki, an OpenTelemetry Collector, Telegraf and Prometheus in
  containers — and the suite reads each one back through its own API
  (2026-09-16). Its derive stage adds derived values and detection events
  beside the raw rows, and it emits a device-info stream once per capability
  hash.
- **Five Grafana dashboards**, one per store — InfluxDB 3, Prometheus,
  PostgreSQL, Graphite and Elasticsearch — generated from one panel list, with
  alert rules for the three whose query language the rules are written in. The
  two SQL stores are asked the same question in two dialects: the PostgreSQL
  panels are the InfluxDB ones rewritten, and every one of its 209 queries is
  planned by a real PostgreSQL in the container suite. Graphite and
  Elasticsearch carry fewer panels on purpose (41 and 30 against 171): Graphite
  has no labels and Elasticsearch no nested documents, so what they cannot
  express is absent rather than wrong. All five are imported into a real
  Grafana over the stores the suite filled, and every panel is asked: on
  2026-09-17, 57, 98, 136, 35 and 28 panels returned data and none failed.
  On 2026-09-16, in Grafana 13.2.1, against the RB5009 with
  the default triggers and a 30-minute `forward` into both stores,
  `dashboards check` passed 171 InfluxDB panels (0 failing, 10 known-empty
  tolerated) and 133 Prometheus panels (0 failing, 9 known-empty tolerated).
  The headless row-by-row render walk of both dashboards — 0 error badges, 0
  "No data" — is from 2026-09-15 and covers 168 and 130 panels; it has not been
  repeated for the two port-event panels and the interface-inventory table it
  does not cover. A walk on 2026-09-14, against a 10.5 h capture from the same
  device, rendered 140 InfluxDB panels with the same result.
- **An end-to-end suite** builds both binaries and drives them against a
  captured `/proc` tree of the reference device and against a fake agent, with
  one receiver per sink protocol asserting the bytes. It needs no router, no
  Grafana and no network, and runs in CI.
- **A second suite against the stores themselves** (`make test-e2e-docker`)
  starts nine of them with docker compose, runs the same collector against the
  same fake agent with every sink pointed at them, and then asks each store its
  own question with its own API: SQL against InfluxDB 3, `psql` against the
  script the SQL sink wrote, a search against Elasticsearch, `query_range`
  against Loki, `render` against Graphite, the decoded OTLP the collector
  received, the line protocol Telegraf parsed, and a scrape of the exporter
  stored in Prometheus. It then imports both dashboards into Grafana and runs
  every panel's query through Grafana's own API. It needs Docker and no router:
  the samples are the same canned ones, so the run is reproducible anywhere.
  The first full run, on 2026-09-16, found a panel that named two columns the
  store only has when the API tier ran.

## What the agent costs today

At the install default — 10 Hz, default per-source floors, a 300 s ring — the
agent costs **2.85 % of one core and 31.3 MiB RSS**,
read from its own cgroup at steady state with the ring full:

Measured on RB5009UG+S+ · 4 × 1.4 GHz Cortex-A72 · RouterOS 7.24.2 · 2026-09-15 · 60 s windows at steady state (ring full), full source set, collector forwarding to a file, a Prometheus exposition and InfluxDB 3 at once

The measured runs:

| rate | floors | CPU of one core | µs/sample | RSS | slipped ticks | gaps / drops |
| --- | --- | --- | --- | --- | --- | --- |
| 10 Hz (default) | default | **2.85 %** | 2 856 | 31.3 MiB | **0** | 0 / 0 |

That is above the budget of 2 % of one core and 16 MiB RSS. The
budget is guidance rather than a contract: cost scales with the device, the
source set and the ring size. The image budget is 8 MiB; the one image size on
record is 6.1 MiB, and it carries no date.

Two rules keep the cost figure honest: wait out the ring (`BUFFER_S`) before
quoting a steady-state figure — on the RB5009, at a 14 MiB soft memory limit, a
reading taken in the first minute after install came back at 1.47 % of one core
against a 9.38 % steady state — and read cost from `/metrics` rather than from a
large `/snapshot`, whose ~1.5 MB response the agent must serialise.

### Cost at 10 Hz, by configuration

Every row was measured on the same RB5009 at 10 Hz, and each is one window with
no spread recorded. They are the settings a reader can choose, so the table says
what each one costs — not what the number was at some earlier point.

| Configuration                                         | CPU of one core                                                       | RSS                            | Measured   | Note                                                                                                     |
| ----------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------- |
| Every source read every tick                          | 2.43 %                                                                | not recorded                   | 2026-09-12 | above the budget                                                                                         |
| Ring full, `MEM_LIMIT_MB` 14                          | 9.38 % (9 374 µs/sample) | not recorded                   | 2026-09-12 | a 300 s ring of lines of about 2.4 kB holds ~7.3 MB; the Go GC runs without pause |
| Ring full, `--mem-limit-mb 40`, `--memory-max 64M`    | 1.39 % (1 388 µs/sample) | 25.13 MiB                      | 2026-09-12 | 0 slipped ticks                                                                                          |
| PMU counters on, a live `forward` writing to InfluxDB | 1.72 %                                                                | not recorded                   | 2026-09-12 | 0 slipped ticks; 2 400 samples forwarded, 0 gaps, 0 drops                                                |
| The install default                                   | 2.85 %                                        | 31.3 MiB | 2026-09-15 | the figure above                                                                                         |

## One device, one RouterOS version

Every on-device figure in this documentation comes from one RB5009UG+S+ on
RouterOS 7.24.2, the owner's production router. There is no lab device.

> **Untested**
>
> A second board of any kind. The hEX S (2025) — 32-bit RouterOS on an ARM64 chip, which is what the
> agent's `linux/arm` build is for — has not arrived, so the 32-bit counter-wrap path and the
> `linux/arm` image have not run on hardware. No x86_64 RouterOS host. No RouterOS version other
> than 7.24.2. Nothing that needs a reboot, which waits for a maintenance window. The end-to-end
> suite has run on Linux only; it has not been run on macOS or Windows.

## Found and not fixed

Checked against the code as of this page:

- **`forward`'s end-of-run summary goes to stdout**, the same stream the
  `--stdout` sink writes records to, so `forward --stdout=lp | telegraf` ends
  every run with lines the consumer cannot parse.
- **A wrong `--token` does not fail the run.** Each pull is logged as
  `401 Unauthorized`, and the run then ends at its `--for` deadline with exit
  status 0 and `forwarded 0 kernel samples`. The health check `forward` makes before
  it starts pulling reads `/healthz`, which needs no token, so it passes. The message is right;
  the exit status tells a unit file nothing.
- **Duplicate sequence numbers in the 2026-09-13 overnight run.** The 50 Hz run
  wrote repeated `seq` values into InfluxDB; the dashboards name those rows
  "duplicate sample (seq repeated)". The leading explanation, which is
  unverified, is Go's HTTP client replaying a POST on a dead pooled connection
  after the server had committed it. The InfluxDB, Loki, OTLP, Elasticsearch and
  Telegraf sinks refuse that replay, so a dead connection is an error the sink
  retries and counts. No run since that night shows whether the duplicates are
  gone.
- **Graphite names thermal zones by index.** The index is deliberate — a
  zone's `type` string is not unique across zones — but the path listing at the
  top of `internal/sinks/graphite.go` reads `thermal.<zone>`.

### Found on the device and not collected

Two readable files on the reference device, read there on 2026-09-15, are not
collected: `/proc/cmdline`, which
carries `board=5009 ver=7.24.1` — a second device identity without the API —
and the hardware watchdog at `/sys/class/watchdog/watchdog0`.

## What the release publishes

The current release is **v1.0.0** — the version in `VERSION`, compiled into both
binaries and reported by `mikroscope version`. A `v*` tag runs the GoReleaser
configuration, which publishes:

- **CLI archives** for linux, darwin, windows and freebsd on amd64, arm64 and
  arm: `.tar.gz`, and `.zip` on Windows, each carrying `LICENSE` and `README.md`.
- **Agent archives** for linux on the same three architectures, for a host that
  wants the bare binary rather than an image.
- **Side-loadable agent image tars**, one per architecture —
  `mikroscope-agent-arm64.tar`, `mikroscope-agent-arm.tar`,
  `mikroscope-agent-amd64.tar` — which `install --agent-tar` uploads to the
  router.
- **The agent image in two registries**, `jmrplens/mikroscope-agent:1.0.0` on
  Docker Hub and `ghcr.io/jmrplens/mikroscope-agent:1.0.0` on GHCR, each one
  manifest over `linux/amd64`, `linux/arm64` and `linux/arm/v7`, which
  `install --remote-image` makes the router pull. RouterOS takes the registry
  host from the global `/container/config registry-url`, which ships as
  `https://registry-1.docker.io`, so the Docker Hub reference needs nothing set
  on the device and the GHCR one needs that setting changed first.
- **`checksums.txt`**, covering every archive and every image tar, a keyless
  cosign signature over it, and an SPDX SBOM per archive, signed in its own
  right.

Installing from a release needs no Go toolchain and no checkout. A checkout and
Go 1.27 install an agent built from your own tree instead: `make build` for the
CLI, `make build-agent` for the agent, which `install` and `image` also compile
themselves. The four ways to get the agent onto a router, and what each one
needs, are on [Installing the agent](/mikroscope/install/).

What the release publishes for `linux/arm` and `linux/amd64` is cross-built and
checked in CI. Of the three architectures, only `arm64` has run on hardware — on
the one RB5009 above.

## See also

- [The cost of the observer](/mikroscope/cost/): the budget, the current figure and how to measure
  it on your own device.
- [The rate ceiling](/mikroscope/cost/rate-ceiling/): the five measured runs at 10, 50 and 100 Hz.
- [The file and the other sinks](/mikroscope/sinks/other/): the ten sinks `forward` writes to.
- [Lineage and licence](/mikroscope/about/lineage/): where the deployment code and the API client
  came from.
