Skip to content
Documentation Prelude Collector 1.0.0

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-counters Model. 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.

Outputs page — six backend cards (NATS, InfluxDB, Kafka, Prometheus,
Webhook, File) with InfluxDB and NATS shown as Enabled

InfluxDB Configuration form — Enabled toggle, URL, API Token,
Organization, Bucket fields, with Save Settings and Test Connection
buttons

Notes:

  • host.docker.internal resolves to the Docker host on Docker Desktop (macOS, Windows). On Linux, attach the collector and InfluxDB to the same Docker network and use http://influxdb:8086, or use the host IP.
  • batch-size is 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 enabled on the output (Step 3).
  • Tail the collector logs for InfluxDB write error lines — authentication and URL mistakes show up here first.

Step 5 - Add the data source in Grafana

Open https://localhost:3000 and sign in.

  1. Open Connections → Data sources → Add data source.
  2. Pick InfluxDB.
  3. Set Query language to Flux.
  4. URL: http://influxdb:8086. (influxdb is the compose service name; Grafana resolves it on the same Docker network.)
  5. Toggle Auth → Basic auth off; enable Custom HTTP headers instead and add:
    • Header: Authorization
    • Value: Token prelude-dev-token-change-me
  6. Organization: prelude. Default bucket: collector. Min time interval: 5s.
  7. 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:

  1. Dashboards → New → New dashboard → Add visualization.

  2. Pick the InfluxDB data source from Step 5.

  3. Switch the editor to Flux if it is not there already.

  4. 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 cumulative in-octets counter into a per-second delta, and the * 8 converts bytes/sec to bits/sec. toFloat() is required because the model declares the counter as uint64 and derivative only accepts floats.

  5. In Standard options, set Unit to bits/sec (SI).

  6. In Legend, set the format to {{device}} {{interface-name}}. The device tag distinguishes vendors automatically; one panel covers them all.

  7. Set the panel title to "In rate by interface".

  8. 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.

  1. Add visualization → Table.

  2. 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")
    
  3. Transformations → Organize fields: hide _start, _stop, _measurement, keep device, interface-name, admin-status, oper-status, and in-octets.

  4. 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.

Grafana dashboard with the interface-counters measurement: counts
of UP/DOWN per device, per-device interface tables with admin/oper
status and in-octets, Input Traffic Rate and Output Traffic Rate
time-series panels, and an Error Rate
panel

Verify it works

You have a working pipeline if all of the following are true:

  • [ ] curl http://localhost:8086/health returns status: pass.
  • [ ] The Flux CLI in Step 4 returns recent rows from your device.
  • [ ] In Grafana Explore with the InfluxDB data source, the bucket collector and measurement interface-counters are 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

Filtering by: