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 <noreply@anthropic.com>
This commit is contained in:
Cody Lee
2026-01-25 12:17:12 -06:00
co-authored by Claude Sonnet 4.5
parent a35e52c140
commit 97d3f995b1
3 changed files with 91 additions and 2 deletions
+2
View File
@@ -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
-2
View File
@@ -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=
+89
View File
@@ -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 ""
}