SNMP
The OpenTelemetry Collector's snmpreceiver polls SNMP-speaking
devices over UDP, converts enterprise and standard MIB OIDs to named
metrics, and exports OTLP to base14 Scout. Use it for network gear
(routers, switches, firewalls), power infrastructure (UPSes, PDUs),
printers, and any host running net-snmp. This guide configures
three representative device profiles — a Linux host, a Cisco-style
router, and an APC-style UPS — and ships the metrics upstream.
The same pattern generalises to any SNMPv1/v2c/v3-capable device.
Because snmpreceiver is poll-only, it is particularly well suited
to monitoring infrastructure that cannot run agents itself — which
makes SNMP a natural bridge into broader IoT and OT telemetry work.
Prerequisites
| Requirement | Minimum | Recommended |
|---|---|---|
| SNMP version | v1 | v2c or v3 |
| OTel Collector Contrib | 0.90.0 | 0.149.0+ |
| Target devices | SNMP-enabled | SNMPv2c/v3 with community or user configured |
| base14 Scout | Any | - |
Before starting:
- Devices must be reachable over UDP/161 from the Collector host
- A read-only community string (v1/v2c) or SNMPv3 user with appropriate auth/priv keys
- OTel Collector installed — see Docker Compose Setup
What You'll Monitor
- Linux / net-snmp hosts: memory (total, available, cached, buffered), per-CPU utilization, 1/5/15-minute load averages, process and user session counts
- Network devices (IF-MIB): per-interface bytes in/out (64-bit ifHCOctets), error counters, nominal speed, administrative and operational status, interface count
- UPSes (PowerNet-MIB and standard UPS-MIB): battery status, capacity, temperature, runtime remaining, input voltage and frequency, output voltage, load percentage, current, online/on-battery state
- Any scalar or table OID the device exposes —
snmpreceivertreats OIDs as first-class; you map each to a metric with a unit and description
Full receiver reference: OTel SNMP Receiver.
The runnable example with a built-in SNMP simulator lives at
base14/examples — components/snmp-telemetry.
It ships three simulated devices (communities linux-host,
cisco-router, apc-ups) on udp://snmpsim:1161, so the config
below works unchanged against the simulator if you swap the real
endpoints for that address.
Access Setup
No code runs on the device. All you need is SNMP read access.
SNMPv2c — enable snmpd and set a read-only community:
rocommunity readonly_community_here default
sysLocation "rack-A1, row-3"
sysContact "ops@example.com"
Then restart snmpd and test from the host that will run the
Collector:
snmpwalk -v2c -c readonly_community_here -t 2 target-host:161 \
1.3.6.1.2.1.1.1
SNMPv3 — prefer authPriv with SHA/AES:
createUser monitor SHA "auth-key-here" AES "priv-key-here"
rouser monitor priv
For network gear, the vendor CLI equivalents apply (Cisco
snmp-server community, Junos snmp { community }, etc.). Always
scope communities and SNMPv3 users to the Collector host's source
address when the device supports it.
Configuration
The snmpreceiver takes one endpoint per receiver instance, so use
one instance per device (or per device class sharing credentials).
Metrics are defined by OID with an explicit unit and either
scalar_oids (values ending in .0) or column_oids (indexed
tables). Table rows fan out to separate resources via
resource_attributes.
receivers:
snmp/linux:
collection_interval: 30s
endpoint: udp://linux-host.internal:161
version: v2c
community: ${env:SNMP_COMMUNITY_LINUX}
resource_attributes:
device.id:
scalar_oid: "1.3.6.1.2.1.1.5.0" # sysName
cpu.index:
indexed_value_prefix: "cpu_"
metrics:
system.processes.count:
description: Number of processes running (hrSystemProcesses)
unit: "{processes}"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.2.1.25.1.6.0"
system.users.count:
description: Active user sessions (hrSystemNumUsers)
unit: "{users}"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.2.1.25.1.5.0"
system.memory.total:
description: Total real memory (UCD memTotalReal)
unit: KiBy
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.4.5.0"
system.memory.available:
description: Available real memory (UCD memAvailReal)
unit: KiBy
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.4.6.0"
system.memory.cached:
description: Cached memory (UCD memCached)
unit: KiBy
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.4.11.0"
system.memory.buffered:
description: Buffered memory (UCD memBuffer)
unit: KiBy
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.4.14.0"
system.cpu.load_average.1m:
description: "1-minute load average x 100 (UCD laLoadInt.1)"
unit: "1"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.10.1.5.1"
system.cpu.load_average.5m:
description: "5-minute load average x 100 (UCD laLoadInt.2)"
unit: "1"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.10.1.5.2"
system.cpu.load_average.15m:
description: "15-minute load average x 100 (UCD laLoadInt.3)"
unit: "1"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.2021.10.1.5.3"
system.cpu.utilization:
description: Per-CPU utilization (hrProcessorLoad)
unit: "%"
gauge: { value_type: int }
column_oids:
- oid: "1.3.6.1.2.1.25.3.3.1.2"
resource_attributes: [cpu.index]
snmp/router:
collection_interval: 30s
endpoint: udp://edge-router.internal:161
version: v2c
community: ${env:SNMP_COMMUNITY_ROUTER}
resource_attributes:
device.id:
scalar_oid: "1.3.6.1.2.1.1.5.0"
network.interface.name:
oid: "1.3.6.1.2.1.31.1.1.1.1" # ifName
attributes:
direction:
enum: [receive, transmit]
metrics:
system.network.interfaces.count:
description: Number of network interfaces (IF-MIB ifNumber)
unit: "{interfaces}"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.2.1.2.1.0"
network.io:
description: Interface I/O bytes (ifHCInOctets / ifHCOutOctets)
unit: By
sum:
value_type: int
monotonic: true
aggregation: cumulative
column_oids:
- oid: "1.3.6.1.2.1.31.1.1.1.6"
resource_attributes: [network.interface.name]
attributes:
- { name: direction, value: receive }
- oid: "1.3.6.1.2.1.31.1.1.1.10"
resource_attributes: [network.interface.name]
attributes:
- { name: direction, value: transmit }
network.errors:
description: Interface errors (ifInErrors / ifOutErrors)
unit: "{errors}"
sum:
value_type: int
monotonic: true
aggregation: cumulative
column_oids:
- oid: "1.3.6.1.2.1.2.2.1.14"
resource_attributes: [network.interface.name]
attributes:
- { name: direction, value: receive }
- oid: "1.3.6.1.2.1.2.2.1.20"
resource_attributes: [network.interface.name]
attributes:
- { name: direction, value: transmit }
network.interface.speed:
description: Interface nominal speed (ifHighSpeed)
unit: "Mbit/s"
gauge: { value_type: int }
column_oids:
- oid: "1.3.6.1.2.1.31.1.1.1.15"
resource_attributes: [network.interface.name]
network.interface.admin_status:
description: "ifAdminStatus - 1=up, 2=down, 3=testing"
unit: "1"
gauge: { value_type: int }
column_oids:
- oid: "1.3.6.1.2.1.2.2.1.7"
resource_attributes: [network.interface.name]
network.interface.oper_status:
description: "ifOperStatus - 1=up, 2=down, 3=testing, 4=unknown, 5=dormant"
unit: "1"
gauge: { value_type: int }
column_oids:
- oid: "1.3.6.1.2.1.2.2.1.8"
resource_attributes: [network.interface.name]
snmp/ups:
collection_interval: 30s
endpoint: udp://ups-dc01.internal:161
version: v2c
community: ${env:SNMP_COMMUNITY_UPS}
resource_attributes:
device.id:
scalar_oid: "1.3.6.1.2.1.1.5.0"
metrics:
ups.battery.status:
description: "Battery status - 1=unknown, 2=normal, 3=low, 4=replace"
unit: "1"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.2.1.1.0"
ups.battery.capacity:
description: Remaining battery capacity (upsAdvBatteryCapacity)
unit: "%"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.2.2.1.0"
ups.battery.temperature:
description: Battery temperature (upsAdvBatteryTemperature)
unit: "Cel"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.2.2.2.0"
ups.battery.runtime_remaining:
description: Estimated runtime remaining, centiseconds (TimeTicks)
unit: "cs"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.2.2.3.0"
ups.battery.replace_indicator:
description: "1=no replacement needed, 2=battery needs replacement"
unit: "1"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.2.2.4.0"
ups.input.voltage:
description: Input line voltage (upsAdvInputLineVoltage)
unit: "V"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.3.2.1.0"
ups.input.frequency:
description: Input line frequency (upsAdvInputFrequency)
unit: "Hz"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.3.2.4.0"
ups.output.status:
description: "Output status - 2=onLine, 3=onBattery, 4=onSmartBoost, ..."
unit: "1"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.4.1.1.0"
ups.output.voltage:
description: Output voltage (upsAdvOutputVoltage)
unit: "V"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.4.2.1.0"
ups.output.load:
description: Output load as percent of rated capacity
unit: "%"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.4.2.3.0"
ups.output.current:
description: Output current (upsAdvOutputCurrent)
unit: "A"
gauge: { value_type: int }
scalar_oids:
- oid: "1.3.6.1.4.1.318.1.1.1.4.2.4.0"
processors:
memory_limiter:
check_interval: 5s
limit_percentage: 80
spike_limit_percentage: 25
batch:
timeout: 10s
send_batch_size: 1024
# Static metadata per pipeline. snmpreceiver only attaches values
# it can retrieve via SNMP; constants like service.name and
# device.kind are added here. Each device gets a distinct
# service.name so it appears as its own service in Scout.
resource/linux:
attributes:
- { key: service.name, value: "linux-host-01", action: insert }
- { key: service.namespace, value: ${env:SERVICE_NAMESPACE}, action: insert }
- { key: environment, value: ${env:ENVIRONMENT}, action: insert }
- { key: device.kind, value: compute, action: insert }
- { key: device.manufacturer, value: "generic-linux", action: insert }
- { key: device.model.identifier, value: "net-snmp", action: insert }
- { key: site.id, value: ${env:SITE_ID}, action: insert }
resource/router:
attributes:
- { key: service.name, value: "cisco-router-01", action: insert }
- { key: service.namespace, value: ${env:SERVICE_NAMESPACE}, action: insert }
- { key: environment, value: ${env:ENVIRONMENT}, action: insert }
- { key: device.kind, value: network, action: insert }
- { key: device.manufacturer, value: "cisco", action: insert }
- { key: device.model.identifier, value: "ISR-C2900", action: insert }
- { key: site.id, value: ${env:SITE_ID}, action: insert }
resource/ups:
attributes:
- { key: service.name, value: "apc-ups-01", action: insert }
- { key: service.namespace, value: ${env:SERVICE_NAMESPACE}, action: insert }
- { key: environment, value: ${env:ENVIRONMENT}, action: insert }
- { key: device.kind, value: power, action: insert }
- { key: device.manufacturer, value: "apc", action: insert }
- { key: device.model.identifier, value: "Smart-UPS-SRT", action: insert }
- { key: site.id, value: ${env:SITE_ID}, action: insert }
exporters:
otlphttp/b14:
endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT}
tls:
insecure_skip_verify: true
service:
pipelines:
metrics/linux:
receivers: [snmp/linux]
processors: [memory_limiter, resource/linux, batch]
exporters: [otlphttp/b14]
metrics/router:
receivers: [snmp/router]
processors: [memory_limiter, resource/router, batch]
exporters: [otlphttp/b14]
metrics/ups:
receivers: [snmp/ups]
processors: [memory_limiter, resource/ups, batch]
exporters: [otlphttp/b14]
Environment Variables
SNMP_COMMUNITY_LINUX=readonly_community_here
SNMP_COMMUNITY_ROUTER=readonly_community_here
SNMP_COMMUNITY_UPS=readonly_community_here
# Service identity — each device pipeline stamps its own service.name
# (linux-host-01 / cisco-router-01 / apc-ups-01). These env vars
# apply to all three.
SERVICE_NAMESPACE=network-infra
ENVIRONMENT=demo
SITE_ID=your-site-id
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.<your-tenant>.base14.io
Every metric carries a service.name/service.namespace/environment
set and the IoT device schema: device.id, device.kind
(compute / network / power), device.manufacturer,
device.model.identifier, site.id. Network gear is a compute
device with device.kind=network — there is no separate
network.device.* namespace. Each simulated (or real) device
appears as its own service in Scout because each receiver pipeline
stamps a distinct service.name.
Shipping via a Local Scout Collector
The otlphttp/b14 exporter above ships directly to Scout. If you
already run a tenant-local Scout Collector — recommended when you
have multiple apps on the same host, need local buffering, or want
a single point of auth — forward metrics to it instead:
exporters:
otlphttp/upstream:
endpoint: http://otel-collector-base14:4318
Then join the Scout Collector's Docker network so DNS resolves the container name:
services:
otel-collector:
# ...
networks:
- default
- otel-collector-network
networks:
otel-collector-network:
external: true
For setting up that upstream Scout Collector, see Docker Compose Setup.
Verify the Setup
Start the Collector and check for successful scrapes within 60 seconds. From the host running the Collector:
# Probe each device reachability and credentials
snmpwalk -v2c -c "$SNMP_COMMUNITY_LINUX" -t 2 linux-host.internal:161 1.3.6.1.2.1.1.1
snmpwalk -v2c -c "$SNMP_COMMUNITY_ROUTER" -t 2 edge-router.internal:161 1.3.6.1.2.1.31.1.1.1.6
snmpwalk -v2c -c "$SNMP_COMMUNITY_UPS" -t 2 ups-dc01.internal:161 1.3.6.1.4.1.318.1.1.1.2.2
# Confirm the Collector emitted metrics
docker logs otel-collector 2>&1 | grep -E 'Name: (network\.io|ups\.|system\.)'
Troubleshooting
no such host or DNS lookup failures
Cause: The Collector cannot resolve the device hostname, or a Docker network isolates it from the device.
Fix:
- Use an IP literal in
endpointif DNS is not available inside the Collector container - On Docker Desktop, use
host.docker.internalto reach services on the host - Run the Collector with
network_mode: hostif the target is on the host's network segment
Request timeout on scrape
Cause: Device is slow to respond, or UDP packets are being dropped.
Fix:
- Raise
timeout: 10son the receiver block - Lower
collection_intervalpressure if the device is resource- constrained (default 30s is usually fine) - Verify UDP/161 is not blocked by an ACL or firewall between Collector and device
No Such Instance for Counter64 OIDs
Cause: The device does not support the ifXTable (IF-MIB
high-capacity counters), common on older hardware.
Fix:
Fall back to 32-bit counters:
column_oids:
- oid: "1.3.6.1.2.1.2.2.1.10" # ifInOctets (Counter32)
resource_attributes: [network.interface.name]
attributes:
- { name: direction, value: receive }
Expect wraparound on high-throughput interfaces; ifInOctets wraps at ~34 GiB.
Metric values look wrong by a factor of 10 or 100
Cause: Several MIBs store fixed-point values as scaled integers
(for example, UCD laLoadInt uses ×100; PowerNet upsAdvOutputFrequency
uses units of 0.1 Hz). upsAdvBatteryRunTimeRemaining is a
TimeTicks value — centiseconds, not seconds — so a 30-minute
runtime reads as 180000.
Fix:
- Check the MIB definition for the OID's
UNITSclause - Document the scale in the metric
description, or - Apply a
transformprocessor to rescale before export
Scrape errors flood the log during a device outage
Cause: The receiver logs each failed scrape at error level.
Fix:
The Collector pipeline keeps running — the error entries are
expected and recoverable. To quiet them, increase
collection_interval for the flaky receiver, or route the
Collector's own logs through a filter/logs processor that drops
snmp scrape errors.
FAQ
Does this work with SNMPv3?
Yes. Set version: v3 and provide user, security_level,
auth_type, auth_password, privacy_type, and privacy_password
as appropriate. Prefer authPriv with SHA-256 and AES-256 on
production gear.
How do I monitor many similar devices without repeating YAML?
snmpreceiver does not have a native templating feature, but the
Collector config can be rendered from a template (Jinja, envsubst,
Helm). Generate one snmp/<device> block per target and share the
metric definitions via YAML anchors, or manage the config as code
and let the generator emit identical metric blocks.
Can I receive SNMP traps with this receiver?
No. snmpreceiver is poll-only. Trap ingestion is handled by a
separate (still-in-development) receiver. If you need traps now,
forward them into the Collector as logs via snmptrapd + a file or
syslog receiver.
What's the difference between using scalar_oid vs oid under
resource_attributes?
scalar_oid fetches a single value (must end in .0) and stamps
every metric from this receiver with it — use it for device-wide
attributes like device.id from sysName. oid points to a
column and produces per-index resources — use it for things that
vary per row like interface names.
Why is device.kind set by a processor and not the receiver?
snmpreceiver can only attach values it fetches over SNMP. Static
labels like device.kind=network don't exist on the device, so we
add them with a resource processor scoped to each device
pipeline. This keeps each pipeline self-contained.
Can I map string values (DisplayString) to numeric metrics?
No. snmpreceiver expects numeric types (Integer, Counter*,
Gauge32, TimeTicks). If a MIB returns a DisplayString like UCD
laLoad ("0.21"), switch to the integer-scaled sibling OID
(laLoadInt, ×100) and document the scaling in the metric
description.
Does polling add significant load on devices?
A 30-second collection_interval against a dozen scalar OIDs and
a single interface table is negligible for modern network gear.
For very large ifTable walks on older devices, raise the
interval or reduce the column list.
What's Next?
- Create Dashboards: Start with interface throughput, CPU load, and UPS battery capacity panels. See Create Your First Dashboard
- Add more devices: Drop in a switch MIB, a PDU MIB, or a
printer MIB — the
snmpreceiverpattern is the same. The runnable example at components/snmp-telemetry shows how to add a fourth device. - Broaden IoT coverage: SNMP is the first hop into IoT monitoring. See the IoT OTel landscape notes for MQTT, Sparkplug B, and OPC-UA paths when you need protocols SNMP cannot reach.
Related Guides
- OTel Collector Configuration — Advanced Collector configuration
- Docker Compose Setup — Run the Collector locally
- Docker Monitoring — Host-level container metrics alongside network gear
- HAProxy Monitoring — Monitor the load balancer in front of those network devices