Skip to content

Import and check

This page answers how to get the five dashboards into a Grafana, what the datasource needs before they can return anything, and what mikroscope dashboards check tells you once they are there. The short version of the last part: check proves that every panel’s query returns rows through Grafana’s own query API. It does not prove that a reader can read the result.

The database comes first: influxdb3 create database mikroscope on the InfluxDB 3 node, with a token that can read and write it — the same database and token the sink writes to, on InfluxDB 3.

The datasource is type influxdb, version: SQL, dbName: mikroscope, with both secure fields set:

  • httpHeaderValue1 = Bearer <token> (the HTTP path)
  • token = <token> (the FlightSQL path)

Without the second, panels fail with flightsql: Unauthenticated (Grafana 12.3.2, 2026-09-12).

The Prometheus dashboard expects two scrape jobs. The first scrapes the collector (mikroscope forward --prom :9124), which carries every family the agent has, recomputed from the samples it received, plus the collector’s own derived and detection families. The second scrapes the agent directly and keeps only the families only the sampler can produce: its tick timing histograms, the trigger and capture counters, and slipped ticks.

- job_name: "mikroscope"
scrape_interval: 5s
static_configs: [{ targets: ["<collector host>:9124"] }]
- job_name: "mikroscope-agent"
scrape_interval: 5s
static_configs: [{ targets: ["172.30.10.2:9123"] }]
metric_relabel_configs:
- source_labels: [__name__]
regex: "mikroscope_(tick_.*|trigger_.*|capture.*|captures_held|slipped_total)"
action: keep

Scraping the agent without the keep list would double every counter the collector also exposes. 172.30.10.2:9123 is the agent’s address on the default install; how a Prometheus host reaches it is on Reaching the agent.

The SQL sink writes a script, not rows: forward --sql out.sql and then psql -f out.sql, or --sql - | psql. So the database has the schema and the data only after that script has been applied — a datasource pointed at an empty database answers every panel with “relation does not exist”.

The datasource is Grafana’s grafana-postgresql-datasource, with the database and the user the script was loaded as. sslmode is yours to choose; postgresVersion only decides which syntax the plugin may emit, and every query in this dashboard is plain SQL.

The panels are the InfluxDB ones, rewritten: the bucket macro, the percentiles, the casts and the column names the SQL sink had to change because user, from and to are reserved words. Ten queries are not rewritten and say so — mikroscope_buddy and the RouterOS interface counters are wide in InfluxDB and long in the SQL schema, and a pivot is a different question.

Graphite has no labels: every dimension is a path node, so a query IS a path — and the first two nodes are yours. --graphite-prefix (default mikroscope) and --host-tag are therefore dashboard variables, read from Graphite’s own metric tree, and the dashboard asks for them at the top rather than being hard-coded to whoever generated it.

The datasource is type graphite; nothing else is needed. From the CLI, check cannot read a browser’s variable picker, so it takes them:

Terminal window
mikroscope dashboards check --store graphite --datasource-uid <uid> \
--var prefix=mikroscope --var host=rb5009

Type elasticsearch, with the index the --elastic-index you forwarded with produces and @timestamp as the time field.

This dashboard is the smallest of the five, and the reason is in the documents rather than in the queries: the sink writes a sample’s per-core and per-device readings as arrayscpu is an array of four objects — and a dynamically mapped array is a multi-valued field with no correspondence between its members. avg(cpu.busy_ratio) is the mean over the cores, which is a real number; “core 2’s busy ratio” is not expressible at all without a nested mapping the sink does not declare. So the Elasticsearch panels are the scalar aggregates, and the per-core ones are absent rather than wrong.

Grafana → Dashboards → New → Import, upload dashboards/mikroscope-influxdb.json or mikroscope-prometheus.json, and pick the datasource when Grafana asks for DS_MIKROSCOPE.

A file uploaded this way carries the compiled defaults: the five panels the reference device cannot produce sit in the not-available row, and every other panel ships with its query, whether your store holds its measurement or not. On InfluxDB a panel whose table or column is missing then shows InfluxDB 3’s planning error, as a red badge, when its section is opened. import from the CLI avoids that.

Terminal window
export GRAFANA_URL=http://grafana:3000 GRAFANA_TOKEN=
mikroscope dashboards import --store influxdb --datasource-uid <uid>
mikroscope dashboards check --store influxdb --datasource-uid <uid> --window 15m
Flag Default Used by What it does
--store influxdb import, check influxdb or prometheus; also the datasource plugin id sent to Grafana
--grafana $GRAFANA_URL import, check Grafana’s base URL
--datasource-uid none, required import, check the datasource DS_MIKROSCOPE is bound to
--no-probe off import, check skip asking the datasource what it holds; use the compiled defaults
--window 15m check length of the query window
--end now check the window’s right edge, RFC 3339
--out dashboards gen the directory gen writes the four files into

The token is read only from GRAFANA_TOKEN; there is no flag for it. It is a Grafana service account token with permission to write dashboards. The datasource UID is the last path segment of the datasource’s settings URL in Grafana, /connections/datasources/edit/<uid>. Without a Grafana URL, a token and a datasource UID, both commands stop with import/check need --grafana, GRAFANA_TOKEN and --datasource-uid.

import posts the dashboard to Grafana’s /api/dashboards/import with the datasource input resolved to your UID, overwrite on, into the General folder (folderId 0). The dashboard’s uid is fixed, so importing again replaces the same dashboard at the same URL. It prints that URL.

Before generating, import and check ask the datasource which of mikroscope’s measurements it holds, through Grafana’s /api/ds/query:

SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'iox'

Columns and not only tables: InfluxDB 3 refuses a query naming a missing column at planning time exactly as it refuses a missing table, and a store written before a field existed has the table and not the field. A panel that reads a field added later declares it, and the probe checks it.

It prints datasource holds N measurements; on InfluxDB, N counts tables plus table.column pairs, so it is larger than the number of measurements. It then generates against the answer:

  • A panel whose measurements and required fields are all present ships in its own section with its query — including a panel the reference device could not produce.
  • A panel with anything missing moves into the collapsed “Not available on this device” row with its query removed. It runs nothing, so it cannot paint a red table … not found badge; its no-value text names what this store does not hold.
  • A probe that fails — an error from Grafana, or an answer with no mikroscope_ names in it — is a warning, not an error. import and check print warning: could not ask <store> which measurements it holds, with the reason, and carry on with the compiled defaults, so you are told which dashboard you got.

--no-probe skips the question and uses the compiled defaults, which is also what plain gen does, since it has no datasource to ask.

check generates the dashboard exactly as import would — probe included — and then, for every panel, including every panel nested inside a collapsed row, sends each of its queries through Grafana’s /api/ds/query against your datasource over the window, and counts the rows that come back. The request carries the step Grafana would compute for that panel: the window divided by 900 data points, raised to the panel’s own minimum interval where it has one. Without that step, an increase(x[$__interval]) target returns an empty frame, because a step below the scrape interval leaves fewer than two points in the range.

It prints one line per panel and a verdict:

ok <panel title> rows=<n> frames=<n>
none <panel title> rows=<n> frames=<n> <error, if any>
FAIL <panel title> rows=<n> frames=<n> <error, if any>
every panel returns data (<k> known-empty tolerated)
  • ok: the panel returned at least one row and no query reported an error.
  • none: the panel is marked known-empty and did not qualify as ok. Two kinds of panel carry the mark: those whose emptiness is the healthy state (for example the detection and trigger panels, the two port-event panels, the gaps table, the opt-in conntrack poll, the sub-sample burst panel and the worst kernel-log severity timeline), and those in the not-available row. A known-empty panel is tolerated whether it returned no rows or an error.
  • FAIL: anything else — no rows, or an error on any of the panel’s queries even if another returned rows.

With one or more failures check exits non-zero with N panel(s) return no data (K known-empty tolerated). A dashboard is not done until every panel that is not known-empty returns rows.

What else is outside its reach, from the code:

  • The dashboard stored in Grafana. check regenerates the dashboard locally and runs those queries. It does not read back what import stored, so a dashboard edited in Grafana’s UI is not what it checks.
  • Whether a number is right. One row is a pass. Wrong arithmetic that returns rows passes.
  • Annotations. Only panels are walked; the detections and triggers annotation queries are not run.
  • Alert rules. The provisioning files gen writes are not loaded or evaluated.
  • What the browser does to a query. Some variables are substituted in the browser, not by the server check talks to. On 2026-09-14 the InfluxDB datasource escaped $__interval_ms in five panels, in the browser, into SQL InfluxDB 3 could not parse; the rendered dashboard showed it.
  • The real panel width. check pretends every panel is 900 data points wide, the value the browser sent for this dashboard’s graphs on 2026-09-12. A narrower panel gets a wider bin.

--window is the length of the query window and --end moves its right edge, so the panels can be checked against a capture that has already finished rather than against an idle now:

Terminal window
mikroscope dashboards check --store influxdb --datasource-uid <uid> \
--window 1h --end 2026-09-13T08:30:00Z

A panel answers differently over a window with data than over one without, and a check is only as good as the window it is pointed at.

2026-09-16, on the owner’s Grafana 13.2.1, against the reference deployment: the agent on the reference RB5009 (RouterOS 7.24.2, privileged, the default triggers); forward --prom :9124 --influx--interfaces bridge,ether1,PPPoE_DIGI --counters-every 10s for 30 minutes into an isolated InfluxDB 3 Core; a Prometheus 3.14 scraping the collector every 5 s, plus the agent directly for the families the collector cannot recompute.

Store Window Panels Failing Known-empty tolerated
InfluxDB 3 30 minutes 171 0 10 (the two port-event panels, the opt-in conntrack poll, the two trigger panels, PSI, the four idle block devices)
Prometheus 30 minutes 133 0 9 (the two port-event panels, the two conntrack API panels, PSI, the four block devices)

The two port-event panels’ SQL was validated the same day against a synthetic table in the same InfluxDB 3, because the live store holds no kind column until the first port record classified by kind is written to it.

The headless row-by-row walk of both dashboards in Chromium at 1600x1000 — 0 error badges, 0 “No data” — is from 2026-09-15 and covers 168 InfluxDB panels and 130 Prometheus ones. It has not been repeated, so no render pass is claimed for the three panels it did not cover: the two port-event panels and “What each interface is: type, role, bridge and label”.

2026-09-12, on the owner’s Grafana 12.3.2. The InfluxDB dashboard against an isolated InfluxDB 3 Core fed by mikroscope forward from an RB5009: every panel returned rows, 158–316 per panel over 10 minutes. The Prometheus dashboard against a Prometheus 3.14 scraping the collector every 5 s: every panel returned rows, 228–2 052 over 5 minutes.