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.

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

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 Model — https://collector.example.com/models.
The Data Model Builder lists every model with its field count, mapping
count, version, and completeness status.

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.

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.

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.

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 /deviceslists both Devices as connected. - [ ]
GET /models/$MODEL_IDshows 8 fields and 2 mappings. - [ ] The test-Model call returns parsed rows for both Devices.
- [ ] Both Subscriptions are in
runningstatus. - [ ] 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.