Merge pull request #919 from unpoller/issue-415

Enrich alarms with device names for Loki logs
This commit is contained in:
Cody Lee
2026-01-25 12:22:39 -06:00
committed by GitHub
3 changed files with 94 additions and 3 deletions

4
go.mod
View File

@@ -12,7 +12,7 @@ require (
github.com/prometheus/common v0.67.5
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/unpoller/unifi/v5 v5.6.0
github.com/unpoller/unifi/v5 v5.7.0
golang.org/x/crypto v0.47.0
golang.org/x/term v0.39.0
golift.io/cnfg v0.2.3
@@ -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

4
go.sum
View File

@@ -77,8 +77,8 @@ 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/unpoller/unifi/v5 v5.7.0 h1:mGdHLOvmeQqnyB8mgI/ZzMMd/7kaGm+zd9p6iIF4W6g=
github.com/unpoller/unifi/v5 v5.7.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=

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 ""
}