Tested on Zabbix 7.0.0 and Cisco ACI 6.1(5e).
If you run optical links, you probably already know this: the moment you actually need DOM stats (Digital Optical Monitoring — the light levels your SFPs see and send) is the moment it's too late to start collecting them. The value is in the history.
On our previous infrastructure we collected SFP Rx/Tx power over SNMP as a matter of course, and it paid off more than once. Watching the values over time makes troubleshooting flapping links much less of a guessing game — and in one case it did something even better: we had a batch of SFPs from a bad production series, and the slow, steady decline in their optical levels let me predict the failures about a week before they actually died. Replacing optics on your own schedule instead of at 3 AM is a good trade for a few Zabbix items.
Then we moved to Cisco ACI, and the SNMP approach hit a wall.
The problem: ACI doesn't expose DOM stats over SNMP
As far as I can tell, Cisco ACI switches (Nexus 9000 series in ACI mode) simply don't have SNMP OIDs for DOM statistics. I tested this myself, walking the usual suspects, and found nothing. I even gave an Anthropic Opus model access to SNMP on the switches and let it dig through the MIBs on its own — same conclusion: it's just not there.
I'm honestly not sure whether this is a limitation of the switch series or of ACI mode specifically. My money is on ACI mode, because on our Inter-Pod Network — similar but different Cisco Nexus switches running in NX-OS mode — the DOM OIDs exist and work exactly as you'd expect. Same vendor, same optics concept, different operating mode, completely different story.
DOM stats are collected — by the APIC (once you turn it on)
The good news: the APIC controller can collect DOM statistics from the whole fabric. The less good news: for some reason it's not enabled by default. You have to switch it on here:
Fabric → Fabric Policies → Policies → Monitoring → Fabric Node Controls → Enable DOM
Why isn't this the default? Genuine question. It's cheap telemetry about the physical layer of every link in your fabric, the hardware is already measuring it, and the failure mode it catches is one of the most common ones in a data center. If anyone from the BU is reading this — I'd love to hear the reasoning.
Once enabled, the APIC collects the values, and I believe it would raise a fault if a value crossed the configured thresholds. But what the APIC can't do is draw you a graph of those values over time. And as I said at the start, the history is the whole point — a single snapshot won't tell you that an SFP has been quietly losing 0.2 dB a week. So we want these values in an external monitoring system.
Monitoring over the API instead
No SNMP, but there's a perfectly good REST API. The catch: classic NMS tools and REST APIs don't get along very well. With Zabbix, though, this is very doable.
The typical pain point of API-based monitoring is authentication. It's usually a multi-step dance: first a login call to get a token, then the actual query using that temporary token, plus refresh logic when it expires. That's exactly the kind of statefulness you don't want in a monitoring item — ideally, every poll should be a single self-contained API call.
Cisco APIC has a neat answer to this: certificate-based (signature) authentication. You generate an X.509 certificate, attach it to a local APIC user, and then sign each request with the private key. The signature travels in a Cookie header alongside the request itself — no login, no token, no session, one HTTP call.
It's slightly more involved to set up, because you have to generate the certificate and sign every request. But it solves the one-call problem, and there's a nice security bonus: since a signature is only valid for the exact request it was computed for, your monitoring system effectively gets access to a handful of specific API calls with specific attributes — and nothing else. Even if someone extracts the signed cookie from Zabbix, all they can do with it is read DOM stats. Then you just parse the JSON response and you're done.
Here's how to set the whole thing up, end to end.
Step 1: Generate a certificate
A plain self-signed cert is fine. Ten years of validity so you don't have to think about it again any time soon (after 10 years, generate and upload a new one):
openssl req -newkey rsa:2048 -nodes \
-keyout zabbix-monitoring.key \
-x509 -days 3650 \
-out zabbix-monitoring.crt \
-subj "/CN=zabbix-monitoring"
chmod 600 zabbix-monitoring.key
Keep the .key file private — it never leaves the machine that signs requests. The .crt is the public part that goes to APIC.
If you'd rather not do this by hand, there's a ready-made script in my GitHub repo (more on that at the end) that generates the pair and prints everything you'll need for the next steps.
Step 2: Create the APIC user and attach the certificate
In the APIC GUI:
-
Admin → AAA → Users → Create Local User, e.g.
zabbix-monitoring. - Assign the correct permissions. I used security domain all, role read-all, with Read privilege.
- Open the user, go to X.509 Certificates, click +, name the certificate (I keep it identical to the username, e.g.
zabbix-monitoring), and paste in the contents of the.crtfile. - Save.
Two values from this step matter later:
-
The certificate DN, which follows the pattern
uni/userext/user-<username>/usercert-<certname>. With the names above:uni/userext/user-zabbix-monitoring/usercert-zabbix-monitoring - The certificate's SHA256 fingerprint, which you can get locally:
openssl x509 -in zabbix-monitoring.crt -noout -fingerprint -sha256
Step 3: The URL, and how to sign a request
DOM stats live in two APIC object classes — one for the receive side, one for the transmit side:
- Rx:
/api/class/ethpmDOMRxPwrStats.json - Tx:
/api/class/ethpmDOMTxPwrStats.json
Important detail: Rx and Tx are two separate API calls, and therefore two separate signed requests. The signature covers method + URI + body of one specific request, so you can't reuse the Rx cookie for the Tx endpoint. You'll go through the signing below twice, once per URI.
The signing itself is: take the string GET + the URI (path and query string, no host), sign it with your private key (SHA256 + PKCS#1 v1.5), and base64 the result. In plain shell:
URI='/api/class/ethpmDOMRxPwrStats.json'
SIGNATURE=$(printf 'GET%s' "$URI" \
| openssl dgst -sha256 -sign zabbix-monitoring.key \
| base64 -w 0)
FINGERPRINT=$(openssl x509 -in zabbix-monitoring.crt -noout -fingerprint -sha256 \
| sed 's/^.*Fingerprint=//')
DN='uni/userext/user-zabbix-monitoring/usercert-zabbix-monitoring'
COOKIE="APIC-Certificate-Algorithm=v1.0; APIC-Certificate-Fingerprint=$FINGERPRINT; APIC-Certificate-DN=$DN; APIC-Request-Signature=$SIGNATURE"
echo $COOKIE
And the test call:
curl -sk "https://<your-apic>$URI" -H "Cookie: $COOKIE"
If everything is wired up correctly, you get back JSON along these lines (trimmed):
{
"totalCount": "214",
"imdata": [
{
"ethpmDOMRxPwrStats": {
"attributes": {
"dn": "topology/pod-1/node-101/sys/phys-[eth1/1]/phys/domstats/rxpower",
"value": "-2.334269",
"hiAlarm": "1.999912",
"hiWarn": "0.000000",
"loAlarm": "-13.695722",
"loWarn": "-10.300390",
"alert": "none"
}
}
}
]
}
One object per optical interface in the whole fabric, with the current Rx power in dBm plus the module's own alarm/warning thresholds. The dn tells you exactly which pod/node/port it is.
For Tx, repeat the exact same procedure with URI='/api/class/ethpmDOMTxPwrStats.json' — nothing else changes.
There's also a Python script in the repo that does the signing for you and prints the finished Cookie header value:
./generate-apic-cookie.py \
--key zabbix-monitoring.key \
--cert zabbix-monitoring.crt \
--dn "uni/userext/user-zabbix-monitoring/usercert-zabbix-monitoring" \
--uri "/api/class/ethpmDOMRxPwrStats.json"
Step 4: Wiring it into Zabbix
Now the Zabbix side. The pattern is: one HTTP agent item fetches the raw JSON, a low-level discovery rule finds all the interfaces in it, and dependent item prototypes slice out the per-interface dBm values — so the APIC gets polled exactly once per cycle no matter how many optics you have.
4.1 Create the host and macros
Create a host for your APIC (Data collection → Hosts → Create host) and give it an interface with the APIC's address. The interface type doesn't matter here — it's just a holder for the APIC's FQDN/IP (used via {HOST.CONN} below), since the actual polling happens through HTTP agent items, not the interface itself. Then add two host macros holding the full Cookie header values you generated in step 3:
-
{$APIC.COOKIE.RX}=APIC-Certificate-Algorithm=v1.0; APIC-Certificate-Fingerprint=...; APIC-Certificate-DN=...; APIC-Request-Signature=... -
{$APIC.COOKIE.TX}= same idea, generated for the Tx URI
4.2 The raw HTTP agent item
Create an item on the host:
-
Name:
APIC Rx power raw -
Type:
HTTP agent -
Key:
apic.optics.rx.raw -
URL:
https://{HOST.CONN}/api/class/ethpmDOMRxPwrStats.json -
Headers:
Cookie→{$APIC.COOKIE.RX} -
Type of information:
Text -
History / Trends:
Do not store(this item only exists to feed the items below — no point storing the huge JSON blob)
Test it (the Test button in the item form) — you should see the same JSON as from curl.
4.3 The discovery rule
Create a discovery rule on the host:
-
Name:
APIC Rx optics discovery -
Type:
Dependent item, Master item:apic.optics.rx.raw -
Key:
apic.optics.rx.discovery -
Preprocessing: one step,
JSONPath→$.imdata -
LLD macros:
{#DN}→$.ethpmDOMRxPwrStats.attributes.dn
This turns every interface in the JSON into a discovered entity identified by its DN.
4.4 The item prototype
Inside the discovery rule, create an item prototype:
-
Name:
DOM stats - Rx dbm [{#DN}] -
Type:
Dependent item, Master item:apic.optics.rx.raw -
Key:
apic.optics.rx.dbm[{#DN}] -
Type of information:
Numeric (float), Units:dbm -
Preprocessing: two
JSONPathsteps:-
$.imdata[?(@.ethpmDOMRxPwrStats.attributes.dn=="{#DN}")]— pick this interface's object out of the array -
$[0].ethpmDOMRxPwrStats.attributes.value— extract the dBm value
-
And that's it. After the next discovery run, Latest data fills up with one Rx power item per optical interface in the fabric, and Zabbix starts building the history that started this whole exercise.
For Tx, clone the whole chain (raw item → discovery rule → prototype), swap the URL to ethpmDOMTxPwrStats.json, the cookie macro to {$APIC.COOKIE.TX}, and RxPwrStats to TxPwrStats in the JSONPaths.
The response also carries each module's own hiAlarm/hiWarn/loAlarm/loWarn thresholds, so you can build alerting on top of this — evaluating the value against those limits and firing triggers. I've kept that out of this article to keep it focused, but a finished version of exactly that (a status item computed in JavaScript preprocessing plus warning/critical trigger prototypes) is in the GitHub template.
The repo
While building this, I went looking for an existing Zabbix template for Cisco APIC — and came up empty. So I made one and put it on GitHub:
github.com/jpokorny-cz/zabbix-cisco-aci-templates
If you'd rather skip the manual setup above entirely, you can just import the template — everything from this article is in there, plus a good deal more: fabric health score, fault counts by severity, controller status, CPU/memory/disk usage, threshold evaluation and triggers for the optics, and a companion template for Nexus switches in the Inter-Pod Network role. The cert and cookie scripts I linked are in there too.
If you run ACI and Zabbix, give it a try — and if you extend it or find something broken, issues and pull requests are very welcome. And if you've solved ACI DOM monitoring some other way (streaming telemetry? something I missed?), I'd genuinely like to hear about it in the comments.










Top comments (0)