Send data to Grafana
Pipe interface counters from Prelude Collector into InfluxDB and chart them in Grafana, with a one-shot Docker Compose stack you can drop next to the collector.
By the end of this tutorial, your Grafana instance will show live
per-interface traffic rates for every device subscribed to your
interface-counters Model, sourced from Prelude Collector. Because
the Model normalizes interface state across vendors, a single set of
panels covers all your devices regardless of platform. The pipeline
is:
Device --(gNMI)--> Prelude Collector --(InfluxDB output)--> InfluxDB --(query)--> Grafana
You will use the collector's built-in InfluxDB v2 output backend to push every snapshot as a measurement, then point Grafana at the same InfluxDB instance. Plan on 25-35 minutes.
What you'll learn
- Stand up InfluxDB + Grafana with a single
docker compose up. - Configure the collector's InfluxDB output backend over the REST API (and the equivalent UI route).
- Verify data is landing in InfluxDB before you wire Grafana to it.
- Build panels that show live, vendor-normalized interface counters from every subscribed device using Flux queries.
- Recognize the common failure modes (no data, wrong bucket, token rejected) and where to look for each.
Prerequisites
- Prelude Collector v0.x running, reachable on
https://collector.example.com. See Installation. - An API token exported as
TOKEN(the install flow creates one for you). - One or more devices already onboarded with a running Subscription
against the
interface-countersModel. If you have not built that Model yet, run through Interface counters across vendors first - the steps below assume the eight normalized fields it defines (interface-name,admin-status,oper-status,mtu,in-octets,out-octets,in-errors,out-errors). - Docker (or Podman) on the host that will run InfluxDB and Grafana. The compose file in Step 1 wires both up alongside the collector.
Set the basics for the rest of the tutorial:
export BASE="https://collector.example.com"
export TOKEN="<your-api-token>"
Step 1 - Stand up InfluxDB and Grafana
Save this as docker-compose.grafana.yml next to the collector's
own docker-compose.yml:
services:
influxdb:
image: influxdb:2.7
ports:
- "8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: change-me-please
DOCKER_INFLUXDB_INIT_ORG: prelude
DOCKER_INFLUXDB_INIT_BUCKET: collector
DOCKER_INFLUXDB_INIT_RETENTION: 30d
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: prelude-dev-token-change-me
volumes:
- influxdb-data:/var/lib/influxdb2
- influxdb-config:/etc/influxdb2
restart: unless-stopped
grafana:
image: grafana/grafana-oss:latest
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: change-me
GF_INSTALL_PLUGINS: ""
volumes:
- grafana-data:/var/lib/grafana
depends_on:
- influxdb
restart: unless-stopped
volumes:
influxdb-data:
influxdb-config:
grafana-data:
Bring it up:
docker compose -f docker-compose.grafana.yml up -d
The first start of InfluxDB seeds the org prelude, the bucket
collector, and the admin token prelude-dev-token-change-me from
the environment variables. Replace the token before anything goes
to production — anyone with it has full read/write access to the
instance.
Confirm InfluxDB is healthy:
curl -s http://localhost:8086/health
You should see {"status":"pass", ...}. Confirm Grafana is up by
opening https://localhost:3000 in a
browser; sign in with admin / change-me.
Step 2 - Confirm Subscriptions are producing data
Grafana can only render what the collector is actually collecting.
List your Subscriptions and confirm each one bound to
interface-counters is running:
curl -sk "$BASE/api/v1/subscriptions?model=interface-counters" \
-H "Authorization: Bearer $TOKEN"
Bruno: 05 Subscriptions / List subscriptions
Or in the UI: Subscriptions — https://collector.example.com/subscriptions.
Each device's detail page also lists its subscriptions and ticks the
received-rate column as gNMI updates arrive.
If nothing is running, return to Interface counters across vendors and start a Subscription per device. Then check that the Snapshot is non-empty for one of them:
export DEVICE_ID=12
curl -sk "$BASE/api/v1/snapshots/$DEVICE_ID?model=interface-counters" \
-H "Authorization: Bearer $TOKEN"
Bruno: 06 Snapshots / Get device snapshot
You should see one entry per interface. If the response is {},
wait one full Subscription interval and try again — streaming
protocols populate the Snapshot as samples arrive.
Step 3 - Configure the InfluxDB output on the collector
Tell the collector where to push every snapshot. The collector
exposes an InfluxDB v2 output backend, configurable per-instance
through PUT /api/v1/outputs/influxdb:
curl -sk -X PUT "$BASE/api/v1/outputs/influxdb" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"config": {
"url": "http://host.docker.internal:8086",
"token": "prelude-dev-token-change-me",
"org": "prelude",
"bucket": "collector",
"batch-size": 500,
"flush-interval": 1000
}
}'
Bruno: 08 Outputs / Update output config
Or in the UI: Settings → Outputs — https://collector.example.com/outputs.
The Outputs page shows every backend as a card. Click Configure on
the InfluxDB card to enter URL, token, org, and bucket, then Save
Settings. The card flips to Enabled on save.


Notes:
host.docker.internalresolves to the Docker host on Docker Desktop (macOS, Windows). On Linux, attach the collector and InfluxDB to the same Docker network and usehttp://influxdb:8086, or use the host IP.batch-sizeis the number of points buffered before a write; 500 is a sensible default for one or two devices.flush-interval(milliseconds) caps how long a partial batch sits before being flushed. 1000 ms gives near-real-time charts without hammering the database.
Verify the output came up cleanly:
curl -sk "$BASE/api/v1/outputs/influxdb" \
-H "Authorization: Bearer $TOKEN"
Bruno: 08 Outputs / Get output config
enabled should be true and config.url should match what you
set. The collector now writes one InfluxDB measurement per snapshot
— the measurement name is the Model name (interface-counters), tags
include the device and the interface name, and the fields are the
model's typed leaves.
Step 4 - Confirm data is landing in InfluxDB
Before touching Grafana, make sure points are actually arriving.
InfluxDB ships with a built-in Flux query CLI inside the container:
docker compose -f docker-compose.grafana.yml exec influxdb \
influx query 'from(bucket:"collector") |> range(start:-5m) |> limit(n:5)' \
--token prelude-dev-token-change-me \
--org prelude
You should see five recent rows tagged with your device name and the
interface-counters measurement. If the query returns no rows:
- Wait one full Subscription
interval(the collector flushes after each cycle). - Re-check
enabledon the output (Step 3). - Tail the collector logs for
InfluxDB write errorlines — authentication and URL mistakes show up here first.
Step 5 - Add the data source in Grafana
Open https://localhost:3000 and sign in.
- Open Connections → Data sources → Add data source.
- Pick InfluxDB.
- Set Query language to Flux.
- URL:
http://influxdb:8086. (influxdbis the compose service name; Grafana resolves it on the same Docker network.) - Toggle Auth → Basic auth off; enable Custom HTTP headers
instead and add:
- Header:
Authorization - Value:
Token prelude-dev-token-change-me
- Header:
- Organization:
prelude. Default bucket:collector. Min time interval:5s. - Click Save & test. You should see "datasource is working — 1 buckets found".
If the test fails the most common cause is networking: confirm the URL you entered is the URL Grafana can reach, not the URL you reach from your laptop.
Step 6 - Build the panel
In Grafana:
-
Dashboards → New → New dashboard → Add visualization.
-
Pick the InfluxDB data source from Step 5.
-
Switch the editor to Flux if it is not there already.
-
Paste:
from(bucket: "collector") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r._measurement == "interface-counters") |> filter(fn: (r) => r._field == "in-octets") |> toFloat() |> derivative(unit: 1s, nonNegative: true) |> map(fn: (r) => ({ r with _value: r._value * 8.0 }))derivative(... nonNegative: true)converts the cumulativein-octetscounter into a per-second delta, and the* 8converts bytes/sec to bits/sec.toFloat()is required because the model declares the counter asuint64andderivativeonly accepts floats. -
In Standard options, set Unit to bits/sec (SI).
-
In Legend, set the format to
{{device}} {{interface-name}}. Thedevicetag distinguishes vendors automatically; one panel covers them all. -
Set the panel title to "In rate by interface".
-
Save dashboard, pick a name.
Repeat with r._field == "out-octets" for the egress panel. You
should see one line per interface and per device, updating as
InfluxDB receives fresh points.
Step 7 - Add a current-values table
A current-state table is a useful complement to the rate chart.
-
Add visualization → Table.
-
Query:
from(bucket: "collector") |> range(start: -5m) |> filter(fn: (r) => r._measurement == "interface-counters") |> filter(fn: (r) => r._field == "in-octets" or r._field == "admin-status" or r._field == "oper-status") |> last() |> pivot(rowKey: ["_time", "device", "interface-name"], columnKey: ["_field"], valueColumn: "_value") -
Transformations → Organize fields: hide
_start,_stop,_measurement, keepdevice,interface-name,admin-status,oper-status, andin-octets. -
Title: "Current interface state".
You now have a small dashboard: rates over time, plus a snapshot of current counter values for every device. Save it.

Verify it works
You have a working pipeline if all of the following are true:
- [ ]
curl http://localhost:8086/healthreturnsstatus: pass. - [ ] The Flux CLI in Step 4 returns recent rows from your device.
- [ ] In Grafana Explore with the InfluxDB data source, the
bucket
collectorand measurementinterface-countersare visible. - [ ] Your panel updates as the device's traffic changes (or, on idle interfaces, stays flat at the right magnitude rather than empty).
- [ ] Stopping the Subscription on the collector causes the panel to stop receiving fresh samples within one or two intervals.
Sample dashboard JSON
Save this as interface-counters.json and import it in Grafana
(Dashboards → New → Import). Replace <datasource-uid> with the
UID of your InfluxDB data source (visible in the data source URL
after you save it):
{
"title": "Prelude Collector — Interface Counters",
"schemaVersion": 39,
"panels": [
{
"type": "timeseries",
"title": "In rate by interface",
"datasource": { "type": "influxdb", "uid": "<datasource-uid>" },
"targets": [
{
"query": "from(bucket: \"collector\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r._measurement == \"interface-counters\") |> filter(fn: (r) => r._field == \"in-octets\") |> toFloat() |> derivative(unit: 1s, nonNegative: true) |> map(fn: (r) => ({ r with _value: r._value * 8.0 }))",
"legendFormat": "{{device}} {{interface-name}}"
}
],
"fieldConfig": { "defaults": { "unit": "bps" } },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }
},
{
"type": "timeseries",
"title": "Out rate by interface",
"datasource": { "type": "influxdb", "uid": "<datasource-uid>" },
"targets": [
{
"query": "from(bucket: \"collector\") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r._measurement == \"interface-counters\") |> filter(fn: (r) => r._field == \"out-octets\") |> toFloat() |> derivative(unit: 1s, nonNegative: true) |> map(fn: (r) => ({ r with _value: r._value * 8.0 }))",
"legendFormat": "{{device}} {{interface-name}}"
}
],
"fieldConfig": { "defaults": { "unit": "bps" } },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }
}
]
}
If panels render empty after import, the most common cause is a bucket or measurement-name mismatch. Re-run Step 4 against your install and update the Flux queries.
Troubleshooting
InfluxDB returns 401 on writes. The token is wrong or the org does not exist. Confirm the values in Step 3 match the env vars in the compose file. Tokens are easy to mis-paste — regenerate from the InfluxDB UI (Data → API Tokens) if in doubt.
url=http://localhost:8086 from the collector container doesn't
work. The collector sees localhost as itself, not the host.
Use host.docker.internal (Docker Desktop) or attach both to a
shared Docker network and use http://influxdb:8086.
Field values stuck at zero. That is real — your device is showing zero on those counters. Pick a busier interface or generate traffic to confirm.
Field types changed after a Model edit. InfluxDB is strict about field types per measurement. If you change a Model field from int to string, InfluxDB rejects subsequent writes for that field. Either rename the field, or drop the bucket and let the collector recreate it.
Panel updates feel slow. The collector flushes once per
flush-interval (1 s in Step 3) or when batch-size fills. Lower
either to push points sooner; remember that a busy device with many
sub-second subscriptions is expensive on both ends.
Where to next
- Onboard a Cisco device — add another device so the dashboard fills out.
- Interface counters across vendors — use one normalized Model so a single panel covers two vendors.
- Custom Starlark transform — reshape fields (units, labels, derived values) before they reach InfluxDB.
- API reference — full list of fields each Snapshot exposes; the InfluxDB output writes them as measurement fields one to one.