SNMP with Grafana
What you will need
Section titled “What you will need”This project assumes that you have already followed our Raspberry Pi installation project, and have the resulting configured Raspberry Pi. If you are installing on different hardware or a different OS, these steps may not all be the same for you.
System Preparation
Section titled “System Preparation”Before continuing with the project, make sure your system is up to date.
sudo apt updatesudo apt upgrade -yInstall a text editor of your choice. Here we use vim as an example.
sudo apt install vimConcepts
Section titled “Concepts”What is Monitoring / Network Monitoring?
Section titled “What is Monitoring / Network Monitoring?”Network monitoring is the process of continuously observing network devices, services, and traffic to understand their availability, health, and performance.
A network administrator might want to know:
- Is a device online?
- Are its network interfaces up?
- How much network traffic is it handling?
- Is it experiencing errors or dropped packets?
- How much CPU, memory, or storage is it using?
- Has any of this changed over time?
Why monitor?
Section titled “Why monitor?”Without monitoring, administrators may not know something is wrong until a user reports it.
Monitoring allows administrators to:
- Detect problems earlier
- Identify unusual behavior
- Compare current performance against normal performance
- Track changes over time
- Make decisions about capacity and upgrades
Raspberry Pi Networking Basics
Section titled “Raspberry Pi Networking Basics”ip command
Section titled “ip command”The ip command is a powerful tool for managing network interfaces, IP addresses, and routing on Linux systems. You can use it for things such as:
- Viewing your IP address
- Viewing network interface statuses
- Viewing network statistics / traffic
- etc.
ip [ OPTIONS ] OBJECT { COMMAND | help }Some objects you might commonly use with the ip command include:
addr- for managing IP addresseslink- for managing network interfacesroute- for managing routing tables
Some options you might commonly use with the ip command include:
-s- show statistics-4- use IPv4 only-6- use IPv6 only
Examples:
ip addr showitclub@monk:~ $ ip -4 addr show1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 inet 192.168.1.198/24 brd 192.168.1.255 scope global dynamic noprefixroute eth0 valid_lft 81398sec preferred_lft 81398secitclub@monk:~ $ip -s linkitclub@monk:~ $ ip -s link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 RX: bytes packets errors dropped missed mcast 62544 772 0 0 0 0 TX: bytes packets errors dropped carrier collsns 62544 772 0 0 0 02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link/ether d8:3a:dd:f8:f2:cf brd ff:ff:ff:ff:ff:ff RX: bytes packets errors dropped missed mcast 411962531 307160 0 7 0 4325 TX: bytes packets errors dropped carrier collsns 6213265 66544 0 0 0 03: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether d8:3a:dd:f8:f2:d1 brd ff:ff:ff:ff:ff:ff RX: bytes packets errors dropped missed mcast 0 0 0 0 0 0 TX: bytes packets errors dropped carrier collsns 0 0 0 0 0 0itclub@monk:~ $Looking at our network statistics
Section titled “Looking at our network statistics”The Raspberry Pi already knows a lot about its network interfaces and traffic by default. From the output of ip -s link:
itclub@monk:~ $ ip -s link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 RX: bytes packets errors dropped missed mcast 62544 772 0 0 0 0 TX: bytes packets errors dropped carrier collsns 62544 772 0 0 0 02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 link/ether d8:3a:dd:f8:f2:cf brd ff:ff:ff:ff:ff:ff RX: bytes packets errors dropped missed mcast 411962531 307160 0 7 0 4325 TX: bytes packets errors dropped carrier collsns 6213265 66544 0 0 0 03: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether d8:3a:dd:f8:f2:d1 brd ff:ff:ff:ff:ff:ff RX: bytes packets errors dropped missed mcast 0 0 0 0 0 0 TX: bytes packets errors dropped carrier collsns 0 0 0 0 0 0itclub@monk:~ $We can see the statistics for each network interface, including:
- Interface state
- Bytes received (RX)
- Bytes transmitted (TX)
- Packets received/transmitted
- Errors
- Dropped packets
And that’s all well and good when we are manually inspecting network statistics on the Raspberry Pi. What if we had multiple Pis, or a whole fleet of devices to monitor? Manually logging into every device and checking each one would be impractical. And how could we collect this information over time in order to analyze trends and detect anomalies? This is where SNMP comes in handy, allowing us to programmatically query and monitor network statistics across devices.
What is SNMP?
Section titled “What is SNMP?”SNMP stands for Simple Network Management Protocol.
It is an application-layer protocol used to monitor and manage network-connected devices, and can be supported by many different types of equipment.
SNMP Agent
Section titled “SNMP Agent”Software running on the device being monitored.
The agent:
- Collects information about the device
- Makes that information available through SNMP
- Responds to requests from an SNMP manager
On our Raspberry Pi, we’ll use snmpd.
SNMP Manager
Section titled “SNMP Manager”The SNMP manager is the system that queries and collects information from SNMP agents.
It is responsible for:
- Sending requests to SNMP agents
- Receiving responses from SNMP agents
- Processing the returned management information
SNMP managers are commonly part of network management and monitoring systems. In this project, we will eventually use Telegraf to automatically poll our SNMP agent and collect its metrics.
For this guide, we will start by using SNMP tools such as snmpwalk and snmpget to manually query our Raspberry Pi, and then move to using Telegraf as our SNMP manager in combination with InfluxDB and Grafana to collect and visualize data from our Raspberry Pi devices.
Package Installation
Section titled “Package Installation”We will need the following packages to get started with SNMP:
snmp: Command-line tools (like snmpwalk, snmpget) used to query SNMP agentssnmpd: The SNMP agent daemon that runs on a background service to report stats from your machinesnmp-mibs-downloader: Installs standard, text-readable MIB files so you can query system metrics by name instead of numeric OID. This is particularly useful when using tools likesnmpwalkandsnmpgetto make the output more human-readable.
sudo apt install snmp snmpd snmp-mibs-downloaderVerifying SNMP Agent Installation
Section titled “Verifying SNMP Agent Installation”-
Check that the agent is running:
snmpd Service Status sudo systemctl status snmpdsnmpd Service Status Example itclub@monk:~ $ sudo systemctl status snmpd● snmpd.service - Simple Network Management Protocol (SNMP) Daemon.Loaded: loaded (/usr/lib/systemd/system/snmpd.service; enabled; preset: enabled)Active: active (running) since Wed 2026-10-14 02:07:25 EDT; 52min agoInvocation: 855bc710bfc5492c9301114088a97160Main PID: 15524 (snmpd)Tasks: 1 (limit: 8796)CPU: 3.004sCGroup: /system.slice/snmpd.service└─15524 /usr/sbin/snmpd -LOw -u Debian-snmp -g Debian-snmp -I -smux mteTrigger mteTriggerConf -fOct 14 02:07:25 monk systemd[1]: Starting snmpd.service - Simple Network Management Protocol (SNMP) Daemon....Oct 14 02:07:25 monk systemd[1]: Started snmpd.service - Simple Network Management Protocol (SNMP) Daemon..itclub@monk:~ $ -
Check where it’s listening:
snmpd Listening Ports sudo ss -ulpn | grep snmpdsnmpd Listening Ports Example itclub@monk:~ $ sudo ss -ulpn | grep snmpdUNCONN 0 0 127.0.0.1:161 0.0.0.0:* users:(("snmpd",pid=15524,fd=6))UNCONN 0 0 [::1]:161 *:* users:(("snmpd",pid=15524,fd=7))itclub@monk:~ $You should see
127.0.0.1:161and[::1]:161indicating that the SNMP daemon is listening on both IPv4 and IPv6 loopback addresses on port 161. This means that SNMP will only be accessible locally on the machine, which is fine for the purposes of this guide.
Object Identifiers (OIDs)
Section titled “Object Identifiers (OIDs)”SNMP needs a standardized way to identify every piece of information that can be requested.
It uses an Object Identifier, or OID.
An OID is a globally unique identifier that represents a specific piece of information in the SNMP-managed device.
OIDs are organized in a hierarchical tree structure, and each node in the tree has a numeric identifier. By querying an OID, you can retrieve the corresponding value from the device. For example, .1.3.6.1.2.1.1.5.0 is the OID for the system name of the device.
1 - iso└── .3 - org └── .6 - dod └── .1 - internet └── .2 - mgmt └── .1 - mib-2 ├── .1 - system │ ├── .1 - sysDescr │ ├── .3 - sysUpTime │ └── .5 - sysName └── .2 - interfacesThats a lot of numbers, so here is where MIBs come in.
Management Information Bases (MIBs)
Section titled “Management Information Bases (MIBs)”A Management Information Base (MIB) defines and describes objects that can be managed through SNMP. MIBs give meaningful names to OIDs.
Instead of remembering:
.1.3.6.1.2.1.1.5.0we can use:
SNMPv2-MIB::sysName.0Default Configuration
Section titled “Default Configuration”The following is an example of the default configuration for snmpd (minus all the comments).
itclub@monk:~ $ sudo grep -vE '^\s*(#|$)' /etc/snmp/snmpd.confsysLocation Sitting on the Dock of the BaysysServices 72master agentxagentaddress 127.0.0.1,[::1]view systemonly included .1.3.6.1.2.1.1view systemonly included .1.3.6.1.2.1.25.1rocommunity public default -V systemonlyrocommunity6 public default -V systemonlyrouser authPrivUser authpriv -V systemonlyincludeDir /etc/snmp/snmpd.conf.ditclub@monk:~ $Some configuration options in the default snmpd.conf include:
agentaddress 127.0.0.1,[::1]- specifies the IP addresses and ports on which the SNMP agent listens for incoming requests. As mentioned earlier, for the purposes of this guide, local is sufficient.view systemonly included <OID>- defines a view namedsystemonlythat includes the specified OID subtree, allowing access to only the objects within that subtree.rocommunity public default -V systemonly- defines a read-only community stringpublicthat has access to thesystemonlyview. This allows SNMP clients to query system-related information. A community string acts somewhat like a shared access string in SNMPv1/v2c, however it is sent without encryption. For SNMPv3, instead of a community string, you need to provide (with therouseroption) a username, password, and optionally an encryption key for secure communication, which is why we aren’t using SNMPv3 in this guide.
Editing the Configuration
Section titled “Editing the Configuration”-
Open the snmpd.conf file for editing using your preferred text editor (e.g.,
vim).Editing /etc/snmp/snmpd.conf sudo vim /etc/snmp/snmpd.conf -
Locate the
view systemonly ...configuration and modify it to include the additional OID subtrees for system and host-related information by adding/editing the following lines:/etc/snmp/snmpd.conf view systemonly included .1.3.6.1.2.1.1view systemonly included .1.3.6.1.2.1.2view systemonly included .1.3.6.1.2.1.25view systemonly included .1.3.6.1.2.1.31 -
Restart the
snmpdservice to apply the changes.Restarting snmpd sudo systemctl restart snmpd -
Check the status of the
snmpdservice to ensure it is running correctly.Checking snmpd status sudo systemctl status snmpd
Querying with SNMP tools
Section titled “Querying with SNMP tools”snmpget: retrieve individual management values using SNMP GET requestssnmpwalk: retrieve a subtree of management values using SNMP GETNEXT requests
snmpget
Section titled “snmpget”snmpget is used to retrieve individual management values using SNMP GET requests. It allows you to query specific OIDs or MIB names to get their current values.
snmpget -v2c -c public localhost SNMPv2-MIB::sysName.0Options used in the example:
-v2cspecifies the SNMP version to use (in this case, SNMPv2c)-c publicspecifies the community string (in this case,public)localhostspecifies the target host (in this case, the local machine)SNMPv2-MIB::sysName.0specifies the OID (or MIB name) to query (in this case, the system name)
Your output should show the system name of the local machine, which in this case is monk:
itclub@monk:~ $ snmpget -v2c -c public localhost SNMPv2-MIB::sysName.0SNMPv2-MIB::sysName.0 = STRING: monksnmpwalk
Section titled “snmpwalk”snmpwalk is used to retrieve a subtree of management values using SNMP GETNEXT requests. It allows you to query a range of OIDs or MIB names to get all the current values within that subtree.
snmpwalk -v2c -c public localhost IF-MIB::ifDescrSee snmpget for a description of the options used.
Your output should show the descriptions of all network interfaces on the local machine:
itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifDescrIF-MIB::ifDescr.1 = STRING: loIF-MIB::ifDescr.2 = STRING: eth0IF-MIB::ifDescr.3 = STRING: wlan0itclub@monk:~ $Tables and Indexes
Section titled “Tables and Indexes”SNMP commonly represents information about multiple similar objects using tables. Each row in a table is uniquely identified by an index, which allows you to correlate and retrieve specific information about that row across different columns in the SNMP table.
So for example, when looking at the IF-MIB::ifDescr table, each network interface is associated with a unique index.
itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifDescrIF-MIB::ifDescr.1 = STRING: loIF-MIB::ifDescr.2 = STRING: eth0IF-MIB::ifDescr.3 = STRING: wlan0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifOperStatusIF-MIB::ifOperStatus.1 = INTEGER: up(1)IF-MIB::ifOperStatus.2 = INTEGER: up(1)IF-MIB::ifOperStatus.3 = INTEGER: down(2)From these two commands, we can see that the network interface eth0 is associated with the index 2, and thus has a status of up(1).
Queries of Interest
Section titled “Queries of Interest”Here are some SNMP queries of interest to monitor system and network information:
System Information
Section titled “System Information”snmpget -v2c -c public localhost SNMPv2-MIB::sysName.0itclub@monk:~ $ snmpget -v2c -c public localhost SNMPv2-MIB::sysName.0SNMPv2-MIB::sysName.0 = STRING: monkNetwork Information
Section titled “Network Information”snmpwalk -v2c -c public localhost IF-MIB::ifDescrsnmpwalk -v2c -c public localhost IF-MIB::ifOperStatussnmpwalk -v2c -c public localhost IF-MIB::ifHighSpeeditclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifDescrIF-MIB::ifDescr.1 = STRING: loIF-MIB::ifDescr.2 = STRING: eth0IF-MIB::ifDescr.3 = STRING: wlan0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifOperStatusIF-MIB::ifOperStatus.1 = INTEGER: up(1)IF-MIB::ifOperStatus.2 = INTEGER: up(1)IF-MIB::ifOperStatus.3 = INTEGER: down(2)itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifHighSpeedIF-MIB::ifHighSpeed.1 = Gauge32: 10IF-MIB::ifHighSpeed.2 = Gauge32: 1000IF-MIB::ifHighSpeed.3 = Gauge32: 0Network Information (Traffic)
Section titled “Network Information (Traffic)”snmpwalk -v2c -c public localhost IF-MIB::ifHCInOctetssnmpwalk -v2c -c public localhost IF-MIB::ifHCOutOctetssnmpwalk -v2c -c public localhost IF-MIB::ifInErrorssnmpwalk -v2c -c public localhost IF-MIB::ifOutErrorssnmpwalk -v2c -c public localhost IF-MIB::ifInDiscardssnmpwalk -v2c -c public localhost IF-MIB::ifOutDiscardsitclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifHCInOctetsIF-MIB::ifHCInOctets.1 = Counter64: 73370IF-MIB::ifHCInOctets.2 = Counter64: 416356208IF-MIB::ifHCInOctets.3 = Counter64: 0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifHCOutOctetsIF-MIB::ifHCOutOctets.1 = Counter64: 73971IF-MIB::ifHCOutOctets.2 = Counter64: 7262031IF-MIB::ifHCOutOctets.3 = Counter64: 0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifInErrorsIF-MIB::ifInErrors.1 = Counter32: 0IF-MIB::ifInErrors.2 = Counter32: 0IF-MIB::ifInErrors.3 = Counter32: 0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifOutErrorsIF-MIB::ifOutErrors.1 = Counter32: 0IF-MIB::ifOutErrors.2 = Counter32: 0IF-MIB::ifOutErrors.3 = Counter32: 0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifInDiscardsIF-MIB::ifInDiscards.1 = Counter32: 0IF-MIB::ifInDiscards.2 = Counter32: 35IF-MIB::ifInDiscards.3 = Counter32: 0itclub@monk:~ $ snmpwalk -v2c -c public localhost IF-MIB::ifOutDiscardsIF-MIB::ifOutDiscards.1 = Counter32: 0IF-MIB::ifOutDiscards.2 = Counter32: 0IF-MIB::ifOutDiscards.3 = Counter32: 0Device Resources
Section titled “Device Resources”CPU Usage
Section titled “CPU Usage”snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrProcessorLoaditclub@monk:~ $ snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrProcessorLoadHOST-RESOURCES-MIB::hrProcessorLoad.196608 = INTEGER: 1HOST-RESOURCES-MIB::hrProcessorLoad.196609 = INTEGER: 0HOST-RESOURCES-MIB::hrProcessorLoad.196610 = INTEGER: 0HOST-RESOURCES-MIB::hrProcessorLoad.196611 = INTEGER: 1Memory and Storage
Section titled “Memory and Storage”snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageDescrsnmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageSizesnmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageUsedsnmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageAllocationUnitsitclub@monk:~ $ snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageDescrHOST-RESOURCES-MIB::hrStorageDescr.1 = STRING: Physical memoryHOST-RESOURCES-MIB::hrStorageDescr.3 = STRING: Virtual memoryHOST-RESOURCES-MIB::hrStorageDescr.6 = STRING: Memory buffersHOST-RESOURCES-MIB::hrStorageDescr.7 = STRING: Cached memoryHOST-RESOURCES-MIB::hrStorageDescr.8 = STRING: Shared memoryHOST-RESOURCES-MIB::hrStorageDescr.10 = STRING: Swap spaceHOST-RESOURCES-MIB::hrStorageDescr.11 = STRING: Available memoryHOST-RESOURCES-MIB::hrStorageDescr.35 = STRING: /runHOST-RESOURCES-MIB::hrStorageDescr.36 = STRING: /HOST-RESOURCES-MIB::hrStorageDescr.38 = STRING: /dev/shmHOST-RESOURCES-MIB::hrStorageDescr.44 = STRING: /run/lockHOST-RESOURCES-MIB::hrStorageDescr.47 = STRING: /run/credentials/systemd-journald.serviceHOST-RESOURCES-MIB::hrStorageDescr.50 = STRING: /boot/firmwareHOST-RESOURCES-MIB::hrStorageDescr.51 = STRING: /tmpHOST-RESOURCES-MIB::hrStorageDescr.53 = STRING: /run/user/1000itclub@monk:~ $ snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageSizeHOST-RESOURCES-MIB::hrStorageSize.1 = INTEGER: 8007388HOST-RESOURCES-MIB::hrStorageSize.3 = INTEGER: 10104536HOST-RESOURCES-MIB::hrStorageSize.6 = INTEGER: 8007388HOST-RESOURCES-MIB::hrStorageSize.7 = INTEGER: 2119016HOST-RESOURCES-MIB::hrStorageSize.8 = INTEGER: 144308HOST-RESOURCES-MIB::hrStorageSize.10 = INTEGER: 2097148HOST-RESOURCES-MIB::hrStorageSize.11 = INTEGER: 7370944HOST-RESOURCES-MIB::hrStorageSize.35 = INTEGER: 400370HOST-RESOURCES-MIB::hrStorageSize.36 = INTEGER: 30581889HOST-RESOURCES-MIB::hrStorageSize.38 = INTEGER: 1000923HOST-RESOURCES-MIB::hrStorageSize.44 = INTEGER: 1280HOST-RESOURCES-MIB::hrStorageSize.47 = INTEGER: 256HOST-RESOURCES-MIB::hrStorageSize.50 = INTEGER: 1032408HOST-RESOURCES-MIB::hrStorageSize.51 = INTEGER: 1000924HOST-RESOURCES-MIB::hrStorageSize.53 = INTEGER: 200184HOST-RESOURCES-MIB::hrStorageSize.54 = INTEGER: 256itclub@monk:~ $ snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageUsedHOST-RESOURCES-MIB::hrStorageUsed.1 = INTEGER: 2582884HOST-RESOURCES-MIB::hrStorageUsed.3 = INTEGER: 2582884HOST-RESOURCES-MIB::hrStorageUsed.6 = INTEGER: 73484HOST-RESOURCES-MIB::hrStorageUsed.7 = INTEGER: 2119016HOST-RESOURCES-MIB::hrStorageUsed.8 = INTEGER: 144308HOST-RESOURCES-MIB::hrStorageUsed.10 = INTEGER: 0HOST-RESOURCES-MIB::hrStorageUsed.11 = INTEGER: 0HOST-RESOURCES-MIB::hrStorageUsed.35 = INTEGER: 2349HOST-RESOURCES-MIB::hrStorageUsed.36 = INTEGER: 1790266HOST-RESOURCES-MIB::hrStorageUsed.38 = INTEGER: 55HOST-RESOURCES-MIB::hrStorageUsed.44 = INTEGER: 4HOST-RESOURCES-MIB::hrStorageUsed.47 = INTEGER: 0HOST-RESOURCES-MIB::hrStorageUsed.50 = INTEGER: 163107HOST-RESOURCES-MIB::hrStorageUsed.51 = INTEGER: 1HOST-RESOURCES-MIB::hrStorageUsed.53 = INTEGER: 16HOST-RESOURCES-MIB::hrStorageUsed.54 = INTEGER: 0itclub@monk:~ $ snmpwalk -v2c -c public localhost HOST-RESOURCES-MIB::hrStorageAllocationUnitsHOST-RESOURCES-MIB::hrStorageAllocationUnits.1 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.3 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.6 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.7 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.8 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.10 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.11 = INTEGER: 1024 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.35 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.36 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.38 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.44 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.47 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.50 = INTEGER: 512 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.51 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.53 = INTEGER: 4096 BytesHOST-RESOURCES-MIB::hrStorageAllocationUnits.54 = INTEGER: 4096 BytesThe Problem With What We’re Doing
Section titled “The Problem With What We’re Doing”Manually retrieving system information using SNMP commands like snmpget and snmpwalk is not scalable. For a single device, and for looking at the system’s status at a specific point in time, it might be feasible. However, this approach quickly becomes cumbersome as the number of devices increases or when continuous monitoring is required.
We need something to:
- Query SNMP automatically
- Query it repeatedly at regular intervals over time
- Store the collected data
- Calculate changes over time
- Visualize the results
This is where automated monitoring solutions like Telegraf, InfluxDB, and Grafana come into play, allowing for continuous data collection, storage, and visualization across multiple devices.
Install and Configure Docker
Section titled “Install and Configure Docker”To run Telegraf, InfluxDB, and Grafana in a consistent and isolated environment, we can use Docker. Docker allows us to containerize these applications, making it easier to deploy and manage them without worrying about dependencies and system configurations.
See our portainer guide for a more comprehensive explanation of Docker and container management.
-
Verify that Docker is installed correctly:
Terminal window docker --versionTerminal window docker compose version -
Verify that the
dockergroup exists:Terminal window cat /etc/group | grep dockerIf it does not exist, you may need to create it:
Terminal window sudo groupadd docker -
Add your user to the
dockergroup:Terminal window sudo usermod -aG docker $USER -
Log out and log back in for the group changes to take effect, or run
newgrp dockerto apply the changes immediately. You can use the commandgroupsto verify your group membership. -
Verify that you can run Docker commands without
sudo:Terminal window docker run hello-world -
(Optional) Configure Docker to start on boot:
Terminal window sudo systemctl enable docker.servicesudo systemctl enable containerd.service
Telegraf, InfluxDB, and Grafana Configuration
Section titled “Telegraf, InfluxDB, and Grafana Configuration”The stack we are building:
snmpd: snmp agent running on the host, which exposes system information via SNMP on port 161 for Telegraf to collectTelegraf: collects metrics from snmpd and sends them to InfluxDBInfluxDB: stores the collected metrics in a time-series databaseGrafana: visualizes the metrics stored in InfluxDB through dashboards
-
Make a project directory
Directorysnmp-monitoring/
Directorytelegraf/
- …
mkdir -p ~/snmp-monitoring/telegrafcd ~/snmp-monitoring -
Create the InfluxDB environment file
Directorysnmp-monitoring/
- .env
Directorytelegraf/
- …
The
.envfile will contain the credentials/settings to be shared between the services, such as InfluxDB username, password, and bucket name.vim .envsnmp-monitoring/.env INFLUXDB_USERNAME=builditINFLUXDB_PASSWORD=buildit-monitoringINFLUXDB_ORG=builditINFLUXDB_BUCKET=networkINFLUXDB_TOKEN=buildit-monitoring-token -
Create the Telegraf configuration file
Directorysnmp-monitoring/
- .env
Directorytelegraf/
- telegraf.conf
The Telegraf configuration file
telegraf.confdefines what metrics to collect and where to send them. In this case, we are going to input from SNMP and output to InfluxDB.vim telegraf/telegraf.confsnmp-monitoring/telegraf/telegraf.conf [agent]interval = "10s"[[inputs.snmp]]agents = ["udp://127.0.0.1:161"]version = 2community = "public"[[inputs.snmp.field]]name = "hostname"oid = "SNMPv2-MIB::sysName.0"[[inputs.snmp.field]]name = "uptime"oid = "SNMPv2-MIB::sysUpTime.0"[[inputs.snmp.table]]name = "interface"[[inputs.snmp.table.field]]name = "interface"oid = "IF-MIB::ifDescr"is_tag = true[[inputs.snmp.table.field]]name = "status"oid = "IF-MIB::ifOperStatus"[[inputs.snmp.table.field]]name = "speed_mbps"oid = "IF-MIB::ifHighSpeed"[[inputs.snmp.table.field]]name = "in_octets"oid = "IF-MIB::ifHCInOctets"[[inputs.snmp.table.field]]name = "out_octets"oid = "IF-MIB::ifHCOutOctets"[[inputs.snmp.table.field]]name = "in_errors"oid = "IF-MIB::ifInErrors"[[inputs.snmp.table.field]]name = "out_errors"oid = "IF-MIB::ifOutErrors"[[inputs.snmp.table.field]]name = "in_discards"oid = "IF-MIB::ifInDiscards"[[inputs.snmp.table.field]]name = "out_discards"oid = "IF-MIB::ifOutDiscards"[[inputs.snmp.table]]name = "cpu"index_as_tag = true[[inputs.snmp.table.field]]name = "load_percent"oid = "HOST-RESOURCES-MIB::hrProcessorLoad"[[inputs.snmp.table]]name = "storage"[[inputs.snmp.table.field]]name = "storage"oid = "HOST-RESOURCES-MIB::hrStorageDescr"is_tag = true[[inputs.snmp.table.field]]name = "allocation_units"oid = "HOST-RESOURCES-MIB::hrStorageAllocationUnits"[[inputs.snmp.table.field]]name = "size"oid = "HOST-RESOURCES-MIB::hrStorageSize"[[inputs.snmp.table.field]]name = "used"oid = "HOST-RESOURCES-MIB::hrStorageUsed"[[outputs.influxdb_v2]]urls = ["http://127.0.0.1:8086"]token = "${INFLUXDB_TOKEN}"organization = "${INFLUXDB_ORG}"bucket = "${INFLUXDB_BUCKET}" -
Create the Telegraf Dockerfile
Directorysnmp-monitoring/
- .env
Directorytelegraf/
- Dockerfile
- telegraf.conf
The Telegraf
Dockerfilewill define the environment for running Telegraf, including the base image and the downloading of the MIB files to make our lives easier.vim telegraf/Dockerfilesnmp-monitoring/telegraf/Dockerfile # Download the standard MIB definitionsFROM debian:bookworm AS mibsRUN sed -i 's/Components: main/Components: main non-free/' \/etc/apt/sources.list.d/debian.sources \&& apt-get update \&& apt-get install -y --no-install-recommends snmp-mibs-downloader# Telegraf imageFROM telegraf:latestUSER rootCOPY --from=mibs /var/lib/mibs /var/lib/mibsRUN mkdir -p /usr/share/snmp/mibs \&& ln -s /var/lib/mibs/ietf /usr/share/snmp/mibs/ietf \&& ln -s /var/lib/mibs/iana /usr/share/snmp/mibs/ianaUSER telegraf -
Create the Docker Compose file
Directorysnmp-monitoring/
- .env
- compose.yml
Directorytelegraf/
- Dockerfile
- telegraf.conf
The Docker
compose.ymlfile will define and connect the services for the monitoring stack (InfluxDB,Telegraf, andGrafana). It will specify the images, environment variables, volumes, and network settings required for each service.vim compose.ymlsnmp-monitoring/compose.yml services:influxdb:image: influxdb:2container_name: influxdbrestart: unless-stoppedports:- "127.0.0.1:8086:8086"environment:DOCKER_INFLUXDB_INIT_MODE: setupDOCKER_INFLUXDB_INIT_USERNAME: ${INFLUXDB_USERNAME}DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUXDB_PASSWORD}DOCKER_INFLUXDB_INIT_ORG: ${INFLUXDB_ORG}DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUXDB_BUCKET}DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUXDB_TOKEN}volumes:- influxdb-data:/var/lib/influxdb2telegraf:build: ./telegrafcontainer_name: telegrafnetwork_mode: hostenvironment:INFLUXDB_ORG: ${INFLUXDB_ORG}INFLUXDB_BUCKET: ${INFLUXDB_BUCKET}INFLUXDB_TOKEN: ${INFLUXDB_TOKEN}volumes:- ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:rografana:image: grafana/grafana:latestcontainer_name: grafanarestart: unless-stoppedports:- "3000:3000"volumes:- grafana-data:/var/lib/grafanavolumes:influxdb-data:grafana-data: -
Start the monitoring stack
Now that you have the following file structure:
Directorysnmp-monitoring/
- .env
- compose.yml
Directorytelegraf/
- Dockerfile
- telegraf.conf
Start the monitoring stack using Docker Compose:
docker compose up -d
Grafana Web Interface
Section titled “Grafana Web Interface”Once the monitoring stack is up and running, you can access the Grafana web interface to visualize the collected metrics.
-
Open your web browser and navigate to
http://localhost:3000. -
Log in using the default username and password:
- Username:
admin - Password:
admin
- Username:
-
You will be prompted to change the password after the first login.
-
Add
InfluxDBas a data source in Grafana using the credentials and bucket name specified in your.envfile.- Query language:
Flux - URL:
http://influxdb:8086 - Organization:
buildit - Token:
buildit-monitoring-token - Default Bucket:
network
Click
Save & Testto verify the connection to InfluxDB.
- Query language:
-
Create a dashboard to visualize the SNMP metrics collected by Telegraf.
-
Add panels to the dashboard to display different types of data, such as network traffic, CPU usage, and memory utilization.
The full documentation for creating dashboards and panels in Grafana can be found in the Grafana documentation.
The queries below are some examples that retrieve the SNMP metrics collected by Telegraf from InfluxDB for a panel. Some metrics can be displayed directly, while others need to be calculated or converted before they are useful for visualization.
System Uptime
Section titled “System Uptime”System Uptime from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "snmp")|> filter(fn: (r) => r._field == "uptime")|> last()|> map(fn: (r) => ({ r with _value: float(v: r._value) / 100.0 }))CPU Utilization
Section titled “CPU Utilization”hrProcessorLoad(and consequently thecpuandload_percentdata we are importing to InfluxDB) reports utilization separately for each logical processor. This query retrieves the latest reading for each processor and calculates their mean to produce an overall CPU utilization percentage.Current CPU Utilization from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "cpu")|> filter(fn: (r) => r._field == "load_percent")|> last()|> group()|> mean()Memory Utilization
Section titled “Memory Utilization”Linux uses otherwise-unused memory for caching, so the amount of memory marked as “used” does not necessarily represent memory pressure. Instead, we’ll calculate utilization using available memory:
Memory Utilization = (1 − Available Memory / Total Physical Memory) × 100
Memory Utilization total = from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "storage")|> filter(fn: (r) => r.storage == "Physical memory")|> filter(fn: (r) => r._field == "size")|> last()|> findRecord(fn: (key) => true, idx: 0)available = from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "storage")|> filter(fn: (r) => r.storage == "Available memory")|> filter(fn: (r) => r._field == "size")|> last()|> findRecord(fn: (key) => true, idx: 0)from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "storage")|> filter(fn: (r) => r.storage == "Physical memory")|> filter(fn: (r) => r._field == "size")|> last()|> map(fn: (r) => ({r with_value: (1.0 - (float(v: available._value) / float(v: total._value))) * 100.0}))Disk Utilization
Section titled “Disk Utilization”Disk Utilization = Used Space / Total Space × 100
Disk Utilization size = from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "storage")|> filter(fn: (r) => r.storage == "/")|> filter(fn: (r) => r._field == "size")|> last()|> findRecord(fn: (key) => true, idx: 0)from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "storage")|> filter(fn: (r) => r.storage == "/")|> filter(fn: (r) => r._field == "used")|> last()|> map(fn: (r) => ({r with_value: float(v: r._value) / float(v: size._value) * 100.0}))Network Interface Status
Section titled “Network Interface Status”Status of Network Interface eth0 from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "interface")|> filter(fn: (r) => r.interface == "eth0")|> filter(fn: (r) => r._field == "status")|> last()Network Interface Speed
Section titled “Network Interface Speed”eth0 Speed Capacity from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "interface")|> filter(fn: (r) => r.interface == "eth0")|> filter(fn: (r) => r._field == "speed_mbps")|> last()Network Traffic
Section titled “Network Traffic”Earlier, we calculated a traffic rate by finding the difference between two SNMP counter readings over time. The derivative() function performs that calculation automatically. Because ifHCInOctets and ifHCOutOctets count bytes, multiplying the resulting bytes-per-second value by 8 converts it to bits per second.
eth0 Inbound Traffic from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "interface")|> filter(fn: (r) => r.interface == "eth0")|> filter(fn: (r) => r._field == "in_octets")|> derivative(unit: 1s, nonNegative: true)|> map(fn: (r) => ({r with_value: float(v: r._value) * 8.0}))|> set(key: "_field", value: "Inbound")|> keep(columns: ["_time", "_value", "_field"])eth0 Outbound Traffic from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "interface")|> filter(fn: (r) => r.interface == "eth0")|> filter(fn: (r) => r._field == "out_octets")|> derivative(unit: 1s, nonNegative: true)|> map(fn: (r) => ({r with_value: float(v: r._value) * 8.0}))|> set(key: "_field", value: "Outbound")|> keep(columns: ["_time", "_value", "_field"])Network Errors and Discards
Section titled “Network Errors and Discards”eth0 Errors and Discards from(bucket: "network")|> range(start: v.timeRangeStart, stop: v.timeRangeStop)|> filter(fn: (r) => r._measurement == "interface")|> filter(fn: (r) => r.interface == "eth0")|> filter(fn: (r) =>r._field == "in_errors" orr._field == "out_errors" orr._field == "in_discards" orr._field == "out_discards")|> derivative(unit: 1s, nonNegative: true)|> keep(columns: ["_time", "_value", "_field"])
Test Your Dashboard
Section titled “Test Your Dashboard”Generate some traffic
Section titled “Generate some traffic”Use CloudFlare’s speed test to generate network traffic:
curl -o /dev/null https://speed.cloudflare.com/__down?bytes=100000000Generate some memory usage
Section titled “Generate some memory usage”Make Python eat our memory:
python3 -c "import time; x = b'x' * 3_000_000_000; time.sleep(60)"