# Five minutes with a router

Get the CLI, install the agent, record a window while you change something, add the router’s own log as markers, and draw the chart — with a real recording from an RB5009.

Source: https://jmrplens.github.io/mikroscope/start/walkthrough/

The question this tool exists for: _I am about to change something on the router — what does it
actually do, at a resolution where I can see it?_ This page is the shortest path from a router
that has never seen mikroscope to a chart that answers it: six steps with the commands as they
are, and one real recording from the RB5009 read line by line.

## Before you start

The router needs what the tool cannot give it: RouterOS 7.24 or later with the `container` package
and `device-mode container=yes`, which MikroTik gates behind a reset-button press or a power cycle.
7.24 is the floor because the container step writes `privileged=`, an attribute MikroTik added in
that release; `--privileged=false` changes its value, not whether it is written, so an older 7.x
rejects the command either way. Everything on this page was measured on 7.24.2. `doctor` reports
the router's version in its first line but does not gate on it.
[What the router needs](/mikroscope/install/prerequisites/) has the whole list, and
`mikroscope doctor` checks the rest read-only and names the fix for anything missing.

The CLI does not read `.env` itself, and only some flags take their default from a
`MIKROSCOPE_*` variable: the connection and naming ones (`ROUTER`, `SSH_PORT`, `SSH_KEY`, `NAME`,
`VETH`, `SUBNET`, `IFACE_LIST`, `ADDR_LIST`, `DISK`, `ARCH`, `TOKEN`, `LAN_ADDRESS`), the API ones
(`API_ADDR`, `API_USER`, `API_PASSWORD`), the sink URLs such as `INFLUX_URL`, `INTERFACES` and
`HOST_TAG`. The rest, among them `--ephemeral`, `--rate`, `--for`, `--topics` and `--prom`, have
plain defaults, whatever the CLI's usage text says. Export the variables you need, or copy
`.env.example` to `.env` and source it with `set -a; . ./.env; set +a`;
[environment variables](/mikroscope/reference/environment/) lists every one.

## The path

1. **Get mikroscope.**

   ```sh wrap
   tar xzf mikroscope_1.0.0_linux_x86_64.tar.gz   # a .zip on Windows
   ./mikroscope version
   ```

   The release carries the CLI as an archive per platform — linux, macOS, Windows and FreeBSD on
   amd64, arm64 and arm — with `checksums.txt`, cosign signatures and SBOMs beside it. The agent
   travels separately, as one image tar per architecture (`mikroscope-agent-arm64.tar`,
   `mikroscope-agent-arm.tar`, `mikroscope-agent-amd64.tar`) and as a registry image, published
   both as `jmrplens/mikroscope-agent:1.0.0` on Docker Hub and as
   `ghcr.io/jmrplens/mikroscope-agent:1.0.0` on GHCR; step 2 takes one of the two.

   From a checkout instead:

   ```sh wrap
   git clone https://github.com/jmrplens/mikroscope && cd mikroscope && make build
   ```

   That needs Go 1.27 and leaves the CLI in `bin/mikroscope`, which is how the commands below are
   written. It is also the only path that installs an agent built from your own tree, because
   `install` cross-compiles the agent from the module root.

2. **Install, once.**

   ```sh wrap
   export MIKROSCOPE_ROUTER=admin@192.168.88.1
   bin/mikroscope plan --ephemeral
   bin/mikroscope doctor --ephemeral && bin/mikroscope install --ephemeral
   ```

   `plan` prints every RouterOS command and writes nothing. `install` puts the agent image on the
   router, lists the objects, runs the same preflight again (`--no-doctor` skips it), asks
   `write the objects above to the router? [y/N]` (`--yes` skips the question), writes, and then
   probes the agent from your host.

   Where the image comes from is your choice, and it is the only difference between the three
   installs:

   - **your Go toolchain**, as above: the CLI runs `go build ./cmd/mikroscope-agent` with
     `CGO_ENABLED=0`, `GOOS=linux` and `GOARCH` from `--arch`, which is why it has to run from the
     module root;
   - **the published tar**, `--agent-tar mikroscope-agent-arm64.tar`: no Go toolchain and no
     checkout. The CLI reads the tar before it uploads it — it has to be a mikroscope agent image
     and its architecture has to match `--arch`, or the verb stops and names the asset to download
     instead;
   - **the registry**, `--remote-image jmrplens/mikroscope-agent:1.0.0`: the router pulls the image
     itself, nothing is uploaded, and `uninstall` has no file to account for. RouterOS takes the
     registry host from the global `/container/config registry-url`, which mikroscope never writes
     because every container on the device shares it, and which ships as
     `https://registry-1.docker.io` — so the Docker Hub reference above runs on a stock router as
     it stands. The GHCR reference, `ghcr.io/jmrplens/mikroscope-agent:1.0.0`, names a host of its
     own: `doctor` checks the setting against it and prints
     `/container/config/set registry-url=https://ghcr.io` when it does not match, or points at
     `--agent-tar`. The pull needs the router to reach the registry and the free RAM for the layers.

   `--arch` defaults to `arm64`, the RB5009's; it is not detected from the router, but `doctor`
   compares it with the architecture the router reports and names the `--arch` value to re-run with
   if they differ.

   On a router you only reach through WinBox or WebFig, there is a fourth way with no CLI on your
   side at all: `mikroscope plan --rsc --remote-image jmrplens/mikroscope-agent:1.0.0 --out install.rsc`
   writes the same commands, in the same order and with the same tags, as a RouterOS script to
   paste into the terminal or `/import`. [Installing the agent](/mikroscope/install/) has that path
   and its two caveats in full.

   `--ephemeral` puts the image tar and the container's root on the router's tmpfs RAM disk and
   creates the container with `start-on-boot=no`, so the agent does not come back after a reboot.
   `install` deletes the tar once the container has extracted it. Nothing was written to flash: `write-sect-since-reboot` stayed at 58 279 across install, run and removal (verified on RB5009UG+S+, RouterOS 7.24.2, 2026-09-11). Drop it for a persistent install. The tmpfs disk has to exist;
   the RB5009 has one, and `doctor --ephemeral` checks for it and prints the `/disk/add` that
   creates it if it does not. Pass `--ephemeral` to `doctor` too: without it, `doctor` checks free
   flash instead.

   `install` ends by probing the agent from your host, and prints one line:

   ```text wrap
   direct transport ok: agent 1.0.0 (9ddd760) built 2026-09-16T08:38:27Z, 10 Hz, seq 29, 0 slipped, 7ms round trip
   ```

   The first field is the agent's build identity: the version, which a release stamps from the
   `VERSION` file, then the commit and the build date. An unstamped `go build` inside a checkout
   reports that same version with the commit and time the Go toolchain records, so the field is
   never `dev` and never a bare hash. An agent from the tar or the registry reports what the
   release build stamped into it; an agent `install` built from your tree carries the CLI's own
   stamp, so the two report the same string. The rest of the line is the probe itself: the sampler
   rate, the sequence the agent had reached, the ticks it slipped and the round trip. On the RB5009
   the probe answered after about three seconds with a 7 ms round trip, and 5 ms after an `upgrade`
   the same day (2026-09-12).

   The probe waits up to 30 s. If your host cannot reach the agent's /30, it first asks the router
   whether the container is running and only then suggests alternatives; see
   [reaching the agent](/mikroscope/install/reaching-the-agent/).

3. **Record while you do the thing.**

   ```sh wrap
   bin/mikroscope record --for 70s --out burst
   ```

   Type a line and press Enter whenever you do something worth remembering; it becomes a marker
   with the agent's timestamp. That works when standard input is a terminal, and the CLI says so:
   `type a line and press Enter to add a marker; Ctrl-C stops`. From another shell,

   ```sh wrap
   bin/mikroscope mark --out burst "queue tree applied"
   ```

   does the same: `mark` appends to `<prefix>.markers.csv` while `record` holds the file open, so a
   note from a second terminal and a note typed into the recorder's own end up in the same file.
   `--for 0`, the default, records until Ctrl-C. At the end `record` prints the sample count, the
   sequence range, the gaps, the markers and the transport it used, and names any stretch of
   samples that was no longer in the agent's ring.

4. **Add the router's own log.**

   This step talks to the RouterOS API, not to the agent, so it needs an account on the router to
   talk to. A read-only one is enough, and [the API user](/mikroscope/security/api-user/) has the
   two commands that create the group and the user; the CLI never needs `admin` for this.

   ```sh wrap
   export MIKROSCOPE_API_ADDR=192.168.88.1:8728 MIKROSCOPE_API_USER=mikroscope MIKROSCOPE_API_PASSWORD=…
   bin/mikroscope mark --out burst --log-markers --router-tz Europe/Madrid
   ```

   (Or put those three in `.env` — `cp .env.example .env` — and source it, as above.)

   Every log line of the recording's window — from its start to its last sample, in the agent's
   clock — whose topic is `system`, `interface` or `container` becomes a marker; add `firewall` or
   `script` with `--topics` when their lines are the story. The router reports errors and scheduler
   runs there, and the log often explains a transient you did not cause.

   `MIKROSCOPE_API_ADDR` has the flag `--api` and `MIKROSCOPE_API_USER` the flag `--api-user`; the
   password has no flag. `--router-tz` is the IANA zone the router's clock shows, because RouterOS
   log times carry no zone; it defaults to your machine's. `record --log-markers` does the same at
   the end of a recording, and `mark --log-markers` does it afterwards, as here
   ([the router's own log as markers](/mikroscope/record/#the-routers-own-log-as-markers)).

5. **Look.**

   ```sh wrap
   bin/mikroscope plot --in burst
   ```

   This writes `burst.svg`: three panels on one time axis — busy ratio per core, softnet drops
   and time squeezes per second, memory available — with every marker as a dashed vertical drawn
   per panel, so no panel title is struck through. Log
   markers that fall in the same second fold into one line whose label reads `N×` and the first
   message; a gap in the samples is drawn as a red line; a label longer than 40 characters is cut to
   37 and an ellipsis, and a chip with no room left shortens further until it fits rather than
   being laid over its neighbour, keeping only its rule when nothing readable is left; a marker
   outside the recording's span is not drawn. The same recording
   always yields the same bytes. `--title` sets the heading and `--svg` another
   output path.

6. **Keep it running (optional).**

   ```sh wrap
   bin/mikroscope forward --prom :9124 --influx "$MIKROSCOPE_INFLUX_URL" --interfaces bridge,ether1
   ```

   This runs the collector: the kernel tier from the agent and, with the `MIKROSCOPE_API_*`
   variables from step 4 in the environment, the RouterOS API tier beside it. With the default
   `--api-mode full` that tier reads `/system/resource`, `/system/resource/cpu`, `/system/health`,
   `monitor-traffic` every second for the interfaces named in `--interfaces`, every port's
   cumulative counters every 10 s, and what each interface is — its comment, type, interface lists,
   bridge and MTU — at start and every 5 min;
   [the RouterOS API tier](/mikroscope/sinks/api-tier/) says which of those the agent can read
   itself. Without those variables the API tier is disabled with a warning and the kernel tier still runs. Both go to a Prometheus
   exposition on `:9124` and to InfluxDB 3. From there,
   [import and check](/mikroscope/dashboards/import-and-check/) sets up the two Grafana dashboards,
   the datasource field an InfluxDB 3 import needs and the two Prometheus scrape jobs.

## What the chart shows

[![RB5009UG+S+, 70 s at 10 Hz: 700 samples over 69.9 s on four cores, with three dashed markers — “baseline, router idle” at 12 s, “dashboards check started” at 30 s and “check finished” at 50 s. Per-core busy stays low with single-sample excursions to 100 %; the softnet panel shows time squeezes and a flat zero for dropped; memory available stays between 662 and 671 MiB.](https://raw.githubusercontent.com/jmrplens/mikroscope/main/site/src/assets/walkthrough/rb5009-walkthrough.svg)](https://raw.githubusercontent.com/jmrplens/mikroscope/main/site/src/assets/walkthrough/rb5009-walkthrough.svg)

[Open the chart at full size](https://raw.githubusercontent.com/jmrplens/mikroscope/main/site/src/assets/walkthrough/rb5009-walkthrough.svg) (SVG, 1200 × 754) to read its labels on
a phone.

Measured on RB5009UG+S+ · 4 × 1.4 GHz Cortex-A72 · RouterOS 7.24.2 · 2026-09-16 · a 70 s `record` at 10 Hz, 700 samples over 69.9 s, the router otherwise at rest, three notes typed into `record`'s terminal

This is a real recording of a router at rest, the shape everything else is read against. The three
dashed lines are the notes typed into `record`'s terminal during it; the chips carry them in full
because they fit, and a longer one is shortened rather than laid over the next.

- **t = 12 s, `baseline, router idle`.** Nothing is happening, and the panels say so: every core
  averages under 10 % over the whole recording, and over the quiet stretch that follows this note
  the four together average 3.9 %.
- **t = 30 to 50 s, between `dashboards check started` and `check finished`.** A browser loading
  the two Grafana dashboards against this router's own collector. The four cores together average
  5.0 % over that stretch, and the work arrives in two short bursts right after the note: core 0
  at or above 50 % for 0.6 s from 32.3 s, and again for 0.3 s at 33.2 s.
- **The work is in short excursions.** 76 of the 700 samples have a core at or above 50 %, in 46
  separate stretches; 38 of those are one sample long, and the longest is 1.4 s, at the start of
  the recording and before the first note. The kernel's scheduler puts each excursion on whichever
  core is free. One isolated sample — one core at 100 % for 100 ms — moves a four-core, one-second
  average by 2.5 %.
- **softnet: `dropped` flat at zero, `time_squeeze` between 0 and about 20 per second.** Nothing
  was lost in 70 s. The squeezes are this device's background rather than an event; what the
  collector calls a microburst is a cluster of them — three flagged samples on one core inside
  60 s — and never a single one.
- **memory available, 662 to 671 MiB.** About 9 MiB of ordinary churn across the recording, with
  no step at either end of the dashboards check.

RouterOS's own `cpu-load`, at 1 s, reports this minute as a flat few per cent. The recording shows
what the few per cent are made of: which core took each excursion, how long it lasted, and where
the notes fall against it.

> **True of this device, not of yours**
>
> One recording, on one router, at 10 Hz, with the router otherwise at rest. The per-panel figures
> above are read off this chart, not re-measured. The three seconds before the probe answered and
> the 7 ms round trip are what that install on that network showed, not a figure for yours.

## What step 2 wrote, and taking it off

**What `install` writes to your router**

- a veth
- one address
- one interface-list membership
- one address-list entry
- an envlist
- the image tar, unless `--remote-image` has the router pull the image
- the container

Every object carries the comment `mikroscope:<name> (managed by mikroscope)`

`mikroscope plan` prints every command before anything is written.

`uninstall` removes by exact tag plus identity, never by pattern, and fails naming the step if anything remains.

```sh wrap
bin/mikroscope uninstall --ephemeral
```

Pass the same `--ephemeral`, `--disk` and `--name` you installed with: `uninstall` and `status`
rebuild the plan from their own flags, and without `--ephemeral` they look for the image tar on
flash instead of on tmpfs. `uninstall` removes every step of the plan in reverse order, by exact tag, then counts what is
left owned by mikroscope on the router and fails, naming the objects, if anything remains.
`bin/mikroscope status --ephemeral` prints the same ownership counts at any time, plus the agent's health when
it is reachable.

> **ssh is not free on the router**
>
> Every ssh connect costs the RB5009 20–27 % CPU for its duration. The CLI
> therefore batches each read into one connect and never uses ssh as a data path; the samples
> travel over HTTP to the veth, or over the RouterOS API relay.

## See also

- [Installing the agent](/mikroscope/install/): every install option, and where each object goes.
- [Record, mark, plot](/mikroscope/record/): the recording files, the transports and the markers in
  full.
- [The API user](/mikroscope/security/api-user/): the read-only account step 4 and the API tier
  need, and the two commands that create it.
- [The collector](/mikroscope/sinks/): what `forward` merges and where it sends it.
- [Import and check](/mikroscope/dashboards/import-and-check/): the Grafana dashboards, their
  datasources and the scrape jobs.
- [How to read what it shows](/mikroscope/playbooks/): the signatures a real fault and a provoked
  one leave on these panels.
