The collector
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.
mikroscope forward --prom :9124 --influx "$MIKROSCOPE_INFLUX_URL" --interfaces bridge,ether1forward 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.
What one run does
Section titled “What one run does”-
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.
-
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. -
Pull the ring every
--poll. The first pull starts after the agent’s newest sample, soforwarddoes 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. -
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.
-
Sample the API tier every
--api-every(1 s by default) when API credentials are configured. See the RouterOS API tier. -
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.
-
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.
Pulling fast enough
Section titled “Pulling fast enough”--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 transportThe 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.
Whose clock each record carries
Section titled “Whose clock each record carries”| 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 |
Scroll sideways to see every column
Which one should I use?
Section titled “Which one should I use?”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 |
Scroll sideways to see every column
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.
The ten sinks
Section titled “The ten sinks”| 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_ |
queued | Telegraf |
Scroll sideways to see every column
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.
A slow sink never stops the loop
Section titled “A slow sink never stops the loop”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.
What the counters count
Section titled “What the counters count”forward prints written, dropped and errors per sink, and the unit differs by
shape:
- Queued sinks count batches.
writtenis one batch the destination accepted,droppedone batch evicted by the byte budget,errorsone failed attempt — a batch that fails three times and then lands is 3 errors and 1 written. Elasticsearch adds onedroppedper 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.
What forward prints
Section titled “What forward prints”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> errorsOn 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> errorsTwo 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.
One dimension, one name
Section titled “One dimension, one name”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.
What has been measured
Section titled “What has been measured”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:
mikroscope forward --for 8m --prom :9124 --influx … --interfaces bridge,ether1,PPPoE_DIGI --conntrack-every 10sIn 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