From 97d3f995b13a296e1d711c1b4da2469353f460dd Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sun, 25 Jan 2026 12:17:12 -0600 Subject: [PATCH] Enrich alarms with device names for Loki logs Added device name enrichment to alarms so that Loki logs show human-readable device names instead of just MAC addresses. Changes: - Modified collectAlarms to fetch devices and build MAC-to-name lookup - Added extractDeviceNameFromAlarm helper to extract MAC addresses from alarm messages and lookup corresponding device names - Device names are extracted from messages like "AP[fc:ec:da:89:a6:91]" or from SrcMAC/DstMAC fields - Added go.mod replace directive to use local unifi library with new DeviceName field The device_name field will now be included in the JSON output sent to Loki, making it easier to identify which device triggered an alarm. Fixes #415 Co-Authored-By: Claude Sonnet 4.5 --- go.mod | 2 + go.sum | 2 - pkg/inputunifi/collectevents.go | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index a620f168..77151542 100644 --- a/go.mod +++ b/go.mod @@ -46,3 +46,5 @@ require ( golang.org/x/sys v0.40.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) + +replace github.com/unpoller/unifi/v5 => ../unifi diff --git a/go.sum b/go.sum index 060b42e6..046c16a2 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/unpoller/unifi/v5 v5.6.0 h1:qryEAJNYXG/WbjIqrIEZ/npRNBwHTsv/ZFbseYry8ug= -github.com/unpoller/unifi/v5 v5.6.0/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= diff --git a/pkg/inputunifi/collectevents.go b/pkg/inputunifi/collectevents.go index 835efff5..ed451f87 100644 --- a/pkg/inputunifi/collectevents.go +++ b/pkg/inputunifi/collectevents.go @@ -51,6 +51,56 @@ func (u *InputUnifi) collectAlarms(logs []any, sites []*unifi.Site, c *Controlle if *c.SaveAlarms { u.LogDebugf("Collecting controller alarms: %s (%s)", c.URL, c.ID) + // Get devices for all sites to build MAC-to-name lookup + devices, err := c.Unifi.GetDevices(sites) + if err != nil { + u.LogDebugf("Failed to get devices for alarm enrichment: %v (continuing without device names)", err) + devices = &unifi.Devices{} // Empty devices struct, alarms will not have device names + } + + // Build MAC address to device name lookup map + macToName := make(map[string]string) + for _, d := range devices.UAPs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.USGs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.USWs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.UDMs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.UXGs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.PDUs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.UBBs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, d := range devices.UCIs { + if d.Mac != "" && d.Name != "" { + macToName[strings.ToLower(d.Mac)] = d.Name + } + } + for _, s := range sites { events, err := c.Unifi.GetAlarmsSite(s) if err != nil { @@ -58,6 +108,9 @@ func (u *InputUnifi) collectAlarms(logs []any, sites []*unifi.Site, c *Controlle } for _, e := range events { + // Try to extract MAC address from alarm message and enrich with device name + e.DeviceName = u.extractDeviceNameFromAlarm(e, macToName) + logs = append(logs, e) webserver.NewInputEvent(PluginName, s.ID+"_alarms", &webserver.Event{ @@ -343,3 +396,39 @@ func hasProtectThumbnail(eventType string) bool { return false } } + +// extractDeviceNameFromAlarm attempts to extract a device name for an alarm by looking up +// MAC addresses found in the alarm message or fields. Returns empty string if no match found. +func (u *InputUnifi) extractDeviceNameFromAlarm(alarm *unifi.Alarm, macToName map[string]string) string { + // Try to extract MAC from message like "AP[fc:ec:da:89:a6:91] was disconnected" + // Look for pattern: [XX:XX:XX:XX:XX:XX] where X is hex digit + msg := alarm.Msg + + // Simple regex-like search for MAC address in brackets + start := strings.Index(msg, "[") + end := strings.Index(msg, "]") + if start >= 0 && end > start { + potentialMAC := msg[start+1 : end] + // Basic validation: should be 17 characters and contain colons + if len(potentialMAC) == 17 && strings.Count(potentialMAC, ":") == 5 { + if name, ok := macToName[strings.ToLower(potentialMAC)]; ok { + return name + } + } + } + + // Also try SrcMAC and DstMAC fields if present + if alarm.SrcMAC != "" { + if name, ok := macToName[strings.ToLower(alarm.SrcMAC)]; ok { + return name + } + } + + if alarm.DstMAC != "" { + if name, ok := macToName[strings.ToLower(alarm.DstMAC)]; ok { + return name + } + } + + return "" +}