Agent for SNMP Devices#1795
Conversation
|
Im also opening my beszel instance at https://beszel.kusuri.my.id/ |
|
Thanks so much for your work on this. I'm swamped right now but I'll get back to you as soon as I can. |
There was a problem hiding this comment.
Hi @marmar76,
As I was searching for this exact functionality I thought I'd read up on your PR and to speed things along here are some comments you can already have a look at pending the review of @henrygd:
Looking at it, I would argue this is a generic SNMP provider, not a MikroTik specific integration.
All OIDs used here are standard IF-MIB (RFC 2863). They work on any SNMP-capable device: Cisco, UniFi, Synology, etc. The file should be renamed from mikrotik_helpers.go to something like snmp_network.go, and all Mikrotik prefixed functions and env vars should follow. MIKROTIK_IP becomes SNMP_TARGET, etc.
Like I mention in my comment as well, the existing codebase uses manager structs (dockerManager, gpuManager etc) for optional services. SNMP arguably fits that same pattern.
A NetworkIOProvider interface with a local /proc implementation as default and an SNMP implementation selected at startup when SNMP_TARGET is set would keep network.go clean. The SNMP client gets initialized once, stored on the provider struct, and reused for every poll. This also avoids the current issue of reinitializing gosnmp.Default globals in multiple places.
Finally the SNMP version is hardcoded to v2c and the community string to "public". At minimum these should be env vars (SNMP_TARGET, SNMP_COMMUNITY, SNMP_VERSION). v3 support can come later but locking to v2c rules out anyone using v3.
Apologies for the wall of comments on someone else's repo. I just want to see this feature land and hopefully this feedback helps get it there faster.
Great start overall, especially for a first Go project. The collection logic works and your demo instance looks solid!
PS: an example of the interface approach I mentioned:
// NetworkIOProvider abstracts network interface stats collection.
type NetworkIOProvider interface {
IOCounters() ([]psutilNet.IOCountersStat, error)
}
// localNetworkIO reads from /proc via gopsutil (default).
type localNetworkIO struct{}
func (l *localNetworkIO) IOCounters() ([]psutilNet.IOCountersStat, error) {
return psutilNet.IOCounters(true)
}
// snmpNetworkIO reads interface stats from a remote SNMP target.
type snmpNetworkIO struct {
client *gosnmp.GoSNMP
}
func newSNMPNetworkIO(target, community string) (*snmpNetworkIO, error) {
client := &gosnmp.GoSNMP{
Target: target,
Community: community,
Version: gosnmp.Version2c,
Timeout: 2 * time.Second,
}
if err := client.Connect(); err != nil {
return nil, fmt.Errorf("snmp connect: %w", err)
}
return &snmpNetworkIO{client: client}, nil
}
func (s *snmpNetworkIO) IOCounters() ([]psutilNet.IOCountersStat, error) {
// walk IF-MIB, return []psutilNet.IOCountersStat
}Then in NewAgent():
if target, exists := utils.GetEnv("SNMP_TARGET"); exists {
community, _ := utils.GetEnv("SNMP_COMMUNITY")
if community == "" {
community = "public"
}
agent.networkIO, err = newSNMPNetworkIO(target, community)
} else {
agent.networkIO = &localNetworkIO{}
}|
|
||
| func getMikrotikInterfaces() []int { | ||
| Interfaces := []int{} | ||
| gosnmp.Default.Target = "192.168.10.2" // RouterOS IP |
There was a problem hiding this comment.
This hardcoded IP should be removed and replaced with the IP read from the environment variable.
| func getMikrotikInterfaces() []int { | ||
| Interfaces := []int{} | ||
| gosnmp.Default.Target = "192.168.10.2" // RouterOS IP | ||
| gosnmp.Default.Community = "public" |
There was a problem hiding this comment.
This community string should be the default but read from an environment variable since a lot of people change it on their device.
| func GetMikrotikInterfacesStats() []psutilNet.IOCountersStat { | ||
| OIDHelper := MikrotikOID() | ||
|
|
||
| // // Get System Name |
There was a problem hiding this comment.
These comments can probably be removed.
| tracker.Cycle() | ||
|
|
||
| for _, v := range netIO { | ||
| // fmt.Println("_", v) |
There was a problem hiding this comment.
This comment should be removed as it is a disabled debugging line.
| ) | ||
|
|
||
| // toBig converts any numeric SNMP value to *big.Int safely. | ||
| func toBig(v interface{}) *big.Int { |
There was a problem hiding this comment.
This reimplements what gosnmp.ToBigInt already provides. For the SNMP counter types used here (Counter32, Counter64, Gauge32), both produce identical results.
| } | ||
| InterfacesResult = append(InterfacesResult, thisInterface) | ||
| } | ||
| // for _, v := range InterfacesResult { |
There was a problem hiding this comment.
This commented out code block can probably be removed (see my other comment).
| gosnmp.Default.Target = "192.168.10.2" // RouterOS IP | ||
| gosnmp.Default.Community = "public" | ||
| gosnmp.Default.Version = gosnmp.Version2c | ||
| gosnmp.Default.Timeout = time.Duration(2) * time.Second |
There was a problem hiding this comment.
In general you are re-initializing the gosnmp package in multiple places, this should be consolidated and ideally the connection should be reused.
| return cfg.isBlacklist | ||
| } | ||
|
|
||
| func getNetIO() ([]psutilNet.IOCountersStat, error) { |
There was a problem hiding this comment.
This check puts device-specific logic in network.go. The existing codebase uses manager structs (dockerManager, gpuManager etc) for optional services. SNMP arguably fits that same pattern..
My approach would be a NetworkIOProvider interface:
type NetworkIOProvider interface {
IOCounters() ([]psutilNet.IOCountersStat, error)
}With the current implementation (/proc) as the default, and an SNMP implementation selected at startup when SNMP_TARGET is set.
| // [upload bytes, download bytes, total upload, total download] | ||
| // if something here | ||
| netIO, err := getNetIO() | ||
| if err != nil { |
There was a problem hiding this comment.
If im not mistaken this error check is inverted.
|
|
||
| err := gosnmp.Default.Connect() | ||
| if err != nil { | ||
| log.Fatalf("Connect() err: %v", err) |
There was a problem hiding this comment.
You are using log.Fatalf in multiple places, this will kill the agent which seems excessive. Beszel uses slog.Error everywhere else.
|
Hi @bassiebal, thank you for your feedback — I really appreciate it. I completely missed that my IP was hard‑coded. You're right: an SNMP implementation should behave more like a database connection — initialized once and kept alive. My current approach doesn’t reflect that well, so I’ll work on improving it based on your suggestions. I’ve only been using SNMP on a MikroTik router, so I focused specifically on that. I didn’t realize other devices also commonly expose SNMP, so thanks for pointing that out. I’d also love your feedback on this discussion: #1537. |
|
No problem! And I gave some feedback as well on the discussion. In my opinion a remote agent polling SNMP seems like the best approach but that entirely depends on the scope of this project. I can also imagine it a good solution to skip networking gear entirely to keep the project lean and simple. |
- Add NetworkIOProvider interface to support multiple network IO implementations - Implement localNetworkIO for standard network stats and snmpNetworkIO for SNMP-based devices - Replace direct Mikrotik checks with provider pattern initialization - Change environment variable from MIKROTIK_IP to SNMP_TARGET for broader SNMP device support - Rename mikrotik_helpers.go to snmp.go and update function names (PrettyValue -> SNMP_PrettyValue) - Remove toBig() helper in favor of gosnmp.ToBigInt() - Centralize network IO logic through agent.networkIO provider instead of conditional checks in getNetIO() This refactoring improves code maintainability by abstracting network IO operations behind an interface, making it easier to add support for additional SNMP devices beyond Mikrotik.
|
Hi @bassiebal, I also just learned that Go has this kind of abstraction—where you can call the same method but get different results depending on how the struct is initialized. It really taught me OOP on a deeper level. |
|
Hello, is the VulnCheck conflict a barrier to this pull request ? |

📃 Description
This PR adds support for monitoring MikroTik devices via SNMP from the internal agent.
If the environment variable
BESZEL_AGENT_MIKROTIK_IPis set, the agent will query SNMP statistics from the specified MikroTik device instead of relying solely on local network metrics.I tested it on hAP AX 2
The goal is to allow Beszel agents running inside a MikroTik container to collect accurate network interface statistics from the host router.
📖 Documentation
Documentation will be added after the implementation is reviewed and the approach is confirmed.
🪵 Changelog
➕ Added
BESZEL_AGENT_MIKROTIK_IPenvironment variable📷 Screenshots
cummulative download is accurate !!

💬 Context
This PR follows the discussion in issue #1537.
🙋 Notes
This is my first time working with Go, so feedback is very welcome.

The current implementation works for my setup, but I'm sure there are better or more idiomatic approaches.
Also, the agent size is getting bigger around 500kb
Any suggestions or improvements are appreciated!