Skip to content

The collector

Where the data comes from and where it goes The router runs the agent in a container that reads the shared kernel and serves it over a veth. The collector on your machine pulls that, merges the RouterOS API tier into it, derives, and writes to every sink you named. THE ROUTER Shared kernel /proc · /sys · /dev/kmsg · PMU mikroscope-agent 1–100 Hz · 300 s ring · HTTP RouterOS API 1 Hz, what the kernel cannot see YOUR MACHINE mikroscope forward pull · merge · derive · fan out veth /30 SINKS InfluxDB 3 Prometheus file · SQL Loki · OTLP Graphite · Elastic Telegraf · stdout pull every 500 ms
Where the data comes from and where it goes The router runs the agent in a container that reads the shared kernel and serves it over a veth. The collector on your machine pulls that, merges the RouterOS API tier into it, derives, and writes to every sink you named. THE ROUTER Shared kernel /proc · /sys · /dev/kmsg · PMU mikroscope-agent 1–100 Hz · 300 s ring · HTTP RouterOS API veth /30 mikroscope forward pull · merge · derive · fan out SINKS InfluxDB 3 Prometheus file · SQL Loki · OTLP Graphite · Elastic Telegraf · stdout

mikroscope forward is the collector. It pulls the kernel tier from the agent, samples the RouterOS API tier, stamps both in the agent’s clock, runs the derive stage over them and writes the merged timeline to every sink you name. This page answers what one run does, how it keeps up with the agent, which clock each record carries, and what “dropped” means when a destination stops answering.

Terminal window
mikroscope forward --prom :9124 --influx "$MIKROSCOPE_INFLUX_URL" --interfaces bridge,ether1

forward with no sink is an error, not a silent no-op: it would read the router and throw the data away. At least one of --file, --prom, --influx, --loki, --otlp, --graphite, --elastic, --sql, --telegraf or --stdout is required, and more than one at a time is the normal arrangement.

  1. Ask the agent for its health. The reply carries the agent’s wall clock, its rate, its newest sequence number and its capability hash. The difference between the agent’s clock and the collector’s is the skew; the rate sizes the pull batch and the derive stage’s trailing baselines.

  2. Hand every sink the device-info stream. The agent’s /capabilities — board, kernel, ceilings, cadences — goes out once as its own record. See the device-info stream.

  3. Pull the ring every --poll. The first pull starts after the agent’s newest sample, so forward does not replay what the ring held before it started. Each pull asks for samples after the last sequence number seen. A trigger marker rides among the samples in sequence order and is forwarded as an annotation, never decoded as a sample.

  4. Derive, then fan out. Every kernel sample goes through the derive stage, and the sample, its derived values and any detection it raised go to every sink in the same order.

  5. Sample the API tier every --api-every (1 s by default) when API credentials are configured. See the RouterOS API tier.

  6. Re-measure the skew every minute. A jump of more than 50 ms is logged — a router clock step, an NTP correction — and counted as a skew jump. The same health read re-checks the capability hash.

  7. On Ctrl-C or at the end of --for, pull once more, close every sink with a final flush, and print what each one did.

No interpolation happens anywhere in that loop: every consumer sees the cadence each source really has.

--poll (default 500 ms) is how often the ring is pulled and --batch how many samples one pull asks for. The default batch is twice what one poll interval produces at the agent’s rate, and never under 20. A fixed 20 samples per 500 ms poll would cap a pull at 40 Hz and lose 1 − 40/50 of a 50 Hz agent’s samples, which is why the batch is sized from the agent’s rate. A pull is repeated while it comes back full — up to 100 times — so the cursor catches up within one poll instead of advancing one batch per poll. A short reply is the ring’s edge.

The relay transport caps a pull at 18 lines, and the cap is computed rather than chosen: /tool fetch returns 64 512 B at most, the mean ring line is taken as 2 560 B (the measured 2 439 B, rounded up), and the cap allows 134 % of that mean so a batch of above-average lines still fits — 18 lines, about 46 kB. A reply that reaches the fetch limit anyway is refused with relay reply hit the 64512-byte fetch limit; lower the batch rather than parsed truncated. At the default 500 ms poll that is 36 samples/s, and above that the collector falls behind. forward computes what the effective batch and the poll allow per second and warns at start when that is below the agent’s rate — for the relay against an agent at 100 Hz, the arithmetic gives:

warning: at most 18 samples per pull every 500ms is 36/s, below the agent's 100 Hz; the collector will fall behind and report gaps. Raise --batch, lower --poll, or use the direct transport

The cap and the warning are read from the code on 2026-09-15, not re-measured against a device.

A collector that falls further behind than the agent’s ring (300 s by default) receives a gap line instead of the samples, and every sink records the gap.

Record Timestamp
Kernel sample, derived values the agent’s own wall clock, as the sample carries it
Detection the wall clock of the sample that raised it
Trigger marker the agent’s wall clock of the fire
API-tier sample the collector’s clock plus the measured skew
Gap the collector’s clock when the pull that found it returned
Device-info record the collector’s clock: board facts have no timestamp of their own

ten destinations, capitalised by the sentence it opens: the honest answer is that most readers want one of the first two. The rest exist so that mikroscope fits what you already run rather than asking you to run something new.

If you… Use It carries Dashboard
want the whole thing, with the dashboards, and have nothing yet --influx every measurement, as line protocol yes, generated
already run Prometheus --prom every family, recomputed from the samples yes, generated
want to capture a window and look at it later --file the merged timeline as JSONL, nothing to install no
keep long-term data in PostgreSQL or TimescaleDB --sql DDL and INSERTs for psql, no driver yes, generated
want the kernel log and the detections where your logs are --loki events only — kmsg, detections, gaps no
already run an OpenTelemetry pipeline --otlp metrics as OTLP/HTTP no
already run Graphite or Elasticsearch --graphite, --elastic every measurement, in that product’s shape yes, a smaller one
already run Telegraf --telegraf every measurement, as line protocol no
want to pipe it into something of your own --stdout line protocol or NDJSON on standard output no

Nothing stops you naming several at once, and that is the normal arrangement: --file beside a store gives you a capture to go back to, and --loki beside --influx puts the kernel log where a log query can reach it while the numbers go to the store the dashboards read.

Two of these do not carry the same thing as the rest. Loki takes events, not metrics — the kernel-log records, the detections and the gaps — so a Loki-only run has no CPU or memory numbers in it at all. --prom is scraped, not pushed: forward serves /metrics and Prometheus comes to it, which means the collector has to be reachable from the Prometheus host.

Flag Destination URL or credential from the environment Shape Page
--file path.jsonl JSONL file synchronous the file
--prom :9124 Prometheus /metrics on the collector host in memory Prometheus
--influx URL InfluxDB 3 line protocol MIKROSCOPE_INFLUX_URL, MIKROSCOPE_INFLUX_TOKEN queued InfluxDB 3
--sql path or --sql - PostgreSQL / TimescaleDB statements, for psql synchronous SQL
--stdout lp or --stdout json standard output queued stdout
--loki URL Loki push API: events, not metrics MIKROSCOPE_LOKI_URL, MIKROSCOPE_LOKI_TOKEN, MIKROSCOPE_LOKI_TENANT queued Loki
--otlp URL OTLP/HTTP metrics, JSON encoding MIKROSCOPE_OTLP_URL, MIKROSCOPE_OTLP_TOKEN queued OTLP
--graphite host:port carbon plaintext over TCP MIKROSCOPE_GRAPHITE_ADDR queued Graphite
--elastic URL Elasticsearch or OpenSearch _bulk MIKROSCOPE_ELASTIC_URL, MIKROSCOPE_ELASTIC_AUTH queued Elasticsearch
--telegraf URL a Telegraf listener over HTTP, TCP or UDP MIKROSCOPE_TELEGRAF_URL, MIKROSCOPE_TELEGRAF_TOKEN queued Telegraf

Sink credentials never come from a flag: a flag is visible in ps and in a shell history. Each sink token is read from its MIKROSCOPE_* variable only. The agent’s own bearer token is the exception: forward takes it as --token, default MIKROSCOPE_TOKEN. --host-tag (MIKROSCOPE_HOST_TAG, default router) puts the same host tag or label on every point in every sink.

A sink that was asked for and cannot be constructed — a port already bound, a file that cannot be opened — fails the run. A sink silently absent is worse than no data, because the absence is invisible.

The collector’s pull loop must never wait on a destination. Every sink that talks to a remote renders into memory and hands the bytes to a bounded queue that a goroutine of its own drains once a second:

  • The queue holds --queue-seconds (default 60) seconds’ worth of a byte budget: 64 KiB per second for InfluxDB, Loki, OTLP, Elasticsearch, Telegraf and stdout, 256 KiB per second for Graphite, whose one-line-per-value format is bulkier.
  • Past the budget the oldest batch is evicted and counted; the newest is always kept, because fresh telemetry beats stale.
  • A failed delivery backs off 2 s, doubling to 60 s, and logs at most one line per minute. Everything else is in the counters.
  • Each HTTP post carries a 10 s timeout, and a dead pooled connection is an error that is retried and counted, not a silent resend.

Three sinks are not queued. The file and SQL sinks write synchronously through a 64 KiB buffer, because a local file does not stall the way a remote does; a write error counts one error and one drop. The Prometheus sink updates in-memory state under a lock and serves it on scrape.

forward prints written, dropped and errors per sink, and the unit differs by shape:

  • Queued sinks count batches. written is one batch the destination accepted, dropped one batch evicted by the byte budget, errors one failed attempt — a batch that fails three times and then lands is 3 errors and 1 written. Elasticsearch adds one dropped per document the cluster refused inside a 200 reply.
  • File, SQL and Prometheus count events: one per sample, trigger, API read, gap, detection or device record accepted.

At start, on standard error: one sink: <name> line per sink, the API tier’s settings, and the agent’s version, rate, sequence number, skew, transport and effective batch. Every minute, on standard error, a running report:

forwarded <n> kernel, <n> api, <n> gap(s), <n> trigger(s), <n> detection(s), last seq <n>; <sink>: <n> written, <n> dropped, <n> errors

On exit, on standard output, the totals and one line per sink:

forwarded <n> kernel samples, <n> api samples, <n> gap(s), <n> skew jump(s)
<sink>: <n> written, <n> dropped, <n> errors

Two properties of that output can surprise a consumer: the exit summary goes to the same stream the --stdout sink writes, so forward --stdout=lp | telegraf ends every run with lines the consumer cannot parse; and a wrong --token is logged per pull as 401 Unauthorized while the run still ends at its --for deadline with exit status 0 and forwarded 0 kernel samples.

A processor is cpu everywhere — InfluxDB tag, SQL column, Prometheus label, OTLP attribute — never core. The wire carries the kernel’s own unit and names it in the field (_khz, _kb, _ticks, _pages, _sectors); each sink converts once, to that store’s convention, and converts a value and its ceiling identically. So temperature is celsius beside critical_celsius, and block-device busy time is io_s.

On the reference RB5009 on 2026-09-12, this eight-minute run forwarded 4 800 kernel and 479 API samples with 0 gaps and 0 drops:

Terminal window
mikroscope forward --for 8m --prom :9124 --influx --interfaces bridge,ether1,PPPoE_DIGI --conntrack-every 10s

In the five rate runs of 2026-09-15 the collector wrote to three sinks at once, and every one reported 0 gaps and 0 drops at 10, 50 and 100 Hz:

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