Skip to content
Documentation Prelude Collector 1.0.0

Interface counters across vendors

Build one normalized Model that collects interface counters from two different vendors and lands them under the same field names in the Snapshot.

Multivendor networks normally mean multivendor data shapes: ifInOctets here, counters/in-octets there, capitalized status enums on one vendor and lowercase on the next. Prelude Collector lets you define one normalized Model with vendor-neutral field names and attach a Mapping per vendor. The collector picks the right Mapping for each Device automatically and writes everything into the same Snapshot under the same field names.

This tutorial walks you end-to-end: define the Model, add the eight fields, attach a Mapping for each of two vendors, start streaming Subscriptions, and confirm normalized data flowing in real time.

Plan on 25-35 minutes. The flow uses gNMI and the OpenConfig /interfaces/interface/state path, which both vendors honor with the same path even though they vary in details around the leaves.

What you'll learn

  • Define a vendor-neutral Model with eight normalized fields.
  • Write per-vendor Mappings against the same OpenConfig path.
  • Use value-transform tables to normalize enum casing across vendors.
  • Start streaming Subscriptions on multiple Devices and verify normalized rows in real time.
  • Recognize where vendor differences show up (path leaves, enum casing, tunnel/loopback noise) and where to handle them in the Mapping.

Prerequisites

  • Prelude Collector v0.x running on https://collector.example.com. See Installation.

  • A valid license and an API token. See Licensing.

  • Two reachable devices from different vendors that both speak gNMI and expose /interfaces/interface/state. The walkthrough uses one Nokia SR Linux (srlinux) and one Arista EOS (eos) device, but the same flow works for any two combinations - swap in IOS-XR (ios-xr), Junos (junos), or any other gNMI-capable platform. If you have not onboarded a Cisco device yet, see Onboard a Cisco device.

  • Bearer token exported as TOKEN:

    export BASE="https://collector.example.com"
    export TOKEN="<your-api-token>"
    

Step 1 - Confirm the two devices are connected

List the Devices and capture the two ids you will use:

curl -s "$BASE/api/v1/devices" \
  -H "Authorization: Bearer $TOKEN"

Bruno: 02 Devices / List devices

You should see both Devices with active: true and a Protocol of type gnmi already attached. If a Device is missing, run through Onboard a Cisco device for the Cisco side and the equivalent flow for the second vendor.

Or in the UI: Devices — https://collector.example.com/devices. Each row shows hostname, management address, network OS, version (auto-detected from gNMI capabilities), and the protocols attached.

Devices list — two gNMI devices, an SR Linux router and an Arista cEOS
router, both Active with gNMI protocol attached

Click a device to confirm gNMI is connected and to capture the device id from the URL:

Device detail for the SR Linux device — gNMI Connected, no subscription
yet, other protocols available but unconfigured

For the rest of the tutorial:

export DEVICE_A=12   # the SR Linux device
export DEVICE_B=13   # the Arista EOS device

Step 2 - Test the path on each device

Before defining the Model, confirm both devices actually return something on /interfaces/interface/state. Use the test-path endpoint (or the equivalent helper your install exposes) and check that you get one update per interface, with leaves like oper-status, admin-status, mtu, and counters/in-octets.

curl -s -X POST "$BASE/api/v1/protocols/test-path" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": '"$DEVICE_A"',
    "protocol": "gnmi",
    "paths": ["/interfaces/interface/state"]
  }'

Bruno: 03 Protocols / Test path (gNMI)

Repeat for DEVICE_B. The exact endpoint shape is in the API reference; the goal is the same in either form - confirm the device replies before you build a Model on top of it.

If one vendor returns no data on first try, that is sometimes a streaming-cadence quirk - the device only sends on its own schedule. Subscribing in Step 6 will capture it. If a vendor returns rows but the field names look different (for example in_octets instead of in-octets), note the difference; you will normalize it in the Mapping in Step 5.

Step 3 - Create the Model

A Model defines what you want to collect, independent of how each vendor exposes it. Create a new Model called interface-counters:

curl -s -X POST "$BASE/api/v1/models" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "interface-counters",
    "description": "Interface state and traffic counters - multivendor."
  }'

Bruno: 04 Models / Create model

Capture the id:

export MODEL_ID=10

Or in the UI: Models → Create Modelhttps://collector.example.com/models. The Data Model Builder lists every model with its field count, mapping count, version, and completeness status.

Data Model Builder list —  row showing 8 fields,
2 mappings, version 1.0, Active and Ready

Step 4 - Add the fields

Eight fields cover the common interface-state shape across vendors: the interface name (used as the row key), admin/oper status, MTU, and ingress/egress octets and errors. Vendor-neutral names - interface-name not ifName. The Mapping in Step 5 translates.

for FIELD in \
  '{"name":"interface-name","field_type":"string","required":true,"key":true,"position":0,"description":"Interface name (key)"}' \
  '{"name":"admin-status","field_type":"string","position":1,"description":"Admin status (up/down)"}' \
  '{"name":"oper-status","field_type":"string","position":2,"description":"Operational status"}' \
  '{"name":"mtu","field_type":"uint32","position":3,"description":"MTU in bytes"}' \
  '{"name":"in-octets","field_type":"uint64","position":4,"description":"Ingress byte counter"}' \
  '{"name":"out-octets","field_type":"uint64","position":5,"description":"Egress byte counter"}' \
  '{"name":"in-errors","field_type":"uint64","position":6,"description":"Ingress error counter"}' \
  '{"name":"out-errors","field_type":"uint64","position":7,"description":"Egress error counter"}'
do
  curl -s -X POST "$BASE/api/v1/models/$MODEL_ID/fields" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "$FIELD"
done

Bruno: 04 Models / Add field

Read back the Model and confirm all eight fields landed:

curl -s "$BASE/api/v1/models/$MODEL_ID" \
  -H "Authorization: Bearer $TOKEN"

Bruno: 04 Models / Get model

You should see fields: 8 and a key_field: "interface-name".

Or in the UI: the model detail page lists each field with its type, format, required flag, and description; the key field is marked.

Model detail page for , Fields tab — 8 normalized
fields with types,  flagged as the key

Step 5 - Add a Mapping per vendor

A Mapping says: for this protocol on this network OS, subscribe to these paths, extract these leaves, rename them into the Model's field names, and apply these value-transform tables.

Vendor A (Nokia SR Linux):

curl -s -X POST "$BASE/api/v1/models/$MODEL_ID/mappings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "gnmi",
    "net_os": "srlinux",
    "key_field": "name",
    "gnmi_paths": ["/interfaces/interface/state"],
    "field_mappings": {
      "name": "interface-name",
      "admin-status": "admin-status",
      "oper-status": "oper-status",
      "mtu": "mtu",
      "counters.in-octets": "in-octets",
      "counters.out-octets": "out-octets",
      "counters.in-errors": "in-errors",
      "counters.out-errors": "out-errors"
    },
    "value_transforms": {
      "admin-status": {"UP": "up", "DOWN": "down"},
      "oper-status":  {"UP": "up", "DOWN": "down", "DORMANT": "dormant"}
    },
    "ignore_values": {"interface-name": ["Null0"]}
  }'

Bruno: 04 Models / Add mapping

Vendor B (Arista EOS):

curl -s -X POST "$BASE/api/v1/models/$MODEL_ID/mappings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "gnmi",
    "net_os": "eos",
    "key_field": "name",
    "gnmi_paths": ["/interfaces/interface/state"],
    "field_mappings": {
      "name": "interface-name",
      "admin-status": "admin-status",
      "oper-status": "oper-status",
      "mtu": "mtu",
      "counters.in-octets": "in-octets",
      "counters.out-octets": "out-octets",
      "counters.in-errors": "in-errors",
      "counters.out-errors": "out-errors"
    },
    "value_transforms": {
      "admin-status": {"UP": "up", "DOWN": "down"},
      "oper-status":  {"UP": "up", "DOWN": "down"}
    }
  }'

Bruno: 04 Models / Add mapping

The two Mappings are nearly identical - same OpenConfig path, same field shape - which is the point of OpenConfig. The differences live in the value_transforms table (EOS does not emit DORMANT) and the ignore_values filter (suppressing the noise interfaces each platform exposes).

If your second vendor is Cisco or Juniper, set net_os accordingly (ios-xr, junos) and adjust value_transforms for their enum casing. The Concepts: Data flow page explains how the collector picks a Mapping based on the Device's net_os.

Or in the UI: open the model and switch to the Protocol Mappings tab. Each mapping shows its protocol, network OS, and key field; an edit panel lets you adjust path-to-field bindings and value transforms without touching curl.

Model detail page, Protocol Mappings tab — two mappings: GNMI/eos and
GNMI/srlinux, both keyed on

Step 6 - Test the Model on each Device

Before starting a long-lived Subscription, run a one-shot test to confirm parsing produces the rows you expect.

curl -s -X POST "$BASE/api/v1/models/$MODEL_ID/test" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"device_id": '"$DEVICE_A"'}'

Bruno: 04 Models / Test mapping

Expected: a list of rows, one per interface, with all eight fields populated and the status enums lowercased.

curl -s -X POST "$BASE/api/v1/models/$MODEL_ID/test" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"device_id": '"$DEVICE_B"'}'

Bruno: 04 Models / Test mapping

You should see the same row shape - same field names, same enum values - even though the underlying device is a different vendor. That is the whole point of the normalization layer.

If a row is missing fields, the leaf name in field_mappings probably does not match what the device actually emits. Re-test the path (Step 2), copy the leaf name from the response, and update the Mapping.

Step 7 - Create one Subscription per Device

Now the streaming part. Bind each Device to the new Model:

curl -s -X POST "$BASE/api/v1/subscriptions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": '"$DEVICE_A"',
    "model": "interface-counters",
    "interval": 30,
    "enabled": true
  }'

curl -s -X POST "$BASE/api/v1/subscriptions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": '"$DEVICE_B"',
    "model": "interface-counters",
    "interval": 30,
    "enabled": true
  }'

Bruno: 05 Subscriptions / Create subscription

Both Subscriptions move to running within seconds for streaming gNMI. List them to confirm:

curl -s "$BASE/api/v1/subscriptions?model=interface-counters" \
  -H "Authorization: Bearer $TOKEN"

Bruno: 05 Subscriptions / List subscriptions

Or in the UI: open each device's detail page; the Subscriptions card on the right shows the new interface-counters row in green Running state, with the received-rate column ticking as gNMI updates arrive.

Device detail page for the Arista EOS device — gNMI Connected, the
 subscription Running with non-zero received rate

Step 8 - Read the Snapshots

Pull the latest parsed state for each Device:

curl -s "$BASE/api/v1/snapshots/$DEVICE_A?model=interface-counters" \
  -H "Authorization: Bearer $TOKEN"

curl -s "$BASE/api/v1/snapshots/$DEVICE_B?model=interface-counters" \
  -H "Authorization: Bearer $TOKEN"

Bruno: 06 Snapshots / Get device snapshot

You should see one entry per interface in each, with the eight fields you defined in Step 4. Crucially, the field names match across vendors - both Snapshots have in-octets, oper-status, and so on. Downstream code only has to know the Model.

Verify it works

You are done when:

  • [ ] GET /devices lists both Devices as connected.
  • [ ] GET /models/$MODEL_ID shows 8 fields and 2 mappings.
  • [ ] The test-Model call returns parsed rows for both Devices.
  • [ ] Both Subscriptions are in running status.
  • [ ] Both Snapshots return one row per interface, with the same eight field names.
  • [ ] Stopping one Subscription does not affect the other (the vendors run independently).

Where the dashboards plug in

With both vendors writing to the same Snapshot shape, downstream dashboards do not need to know about vendors at all. A single Grafana panel filtering on model="interface-counters" shows traffic from both, with the device name as a label or tag.

For the Prometheus output (which is the default), see Send data to Grafana. The same approach works for InfluxDB, Kafka, or webhook outputs - the data shape on the wire is the Model's field set, regardless of which vendor produced it.

Adding a third vendor later

Onboard the new device, then add a third Mapping:

curl -s -X POST "$BASE/api/v1/models/$MODEL_ID/mappings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "gnmi",
    "net_os": "junos",
    "key_field": "name",
    "gnmi_paths": ["/interfaces/interface/state"],
    "field_mappings": { ... },
    "value_transforms": { ... }
  }'

Bruno: 04 Models / Add mapping

Subscribe the new Device to the same interface-counters Model. No dashboard or output change is needed. The collector picks the new Mapping based on the Device's net_os and writes into the same Snapshot shape.

Where to next

  • Send data to Grafana - graph what you just normalized, and watch the device label cleanly distinguish vendors in one panel.
  • Custom Starlark transform - take the normalized rows and add derived fields (utilization percent, flapping flag, human-friendly speeds).
  • Concepts: Data flow - the order in which Protocol, Mapping, Transform, and Snapshot stages run.
  • API reference - full request and response shapes for Models, Fields, Mappings, and Subscriptions.
Filtering by: