Merge pull request #900 from svennergr/svennergr/add-syslog

This commit is contained in:
Cody Lee
2025-12-22 11:33:22 -06:00
committed by GitHub
13 changed files with 248 additions and 83 deletions

View File

@@ -174,13 +174,19 @@
# Enable this only if using InfluxDB or Loki. This will leak PII data! # Enable this only if using InfluxDB or Loki. This will leak PII data!
save_ids = false save_ids = false
# Enable collection of UniFi Events (InfluxDB/Loki only). # Enable collection of UniFi Events using the v1 API (InfluxDB/Loki only).
# This may store a lot of information. Only recommended for testing and debugging. # This uses the legacy /api/s/{site}/stat/event endpoint.
# There are no dashboards to display this data. It can be used for annotations. # For UDM devices, use save_syslog instead (v2 API).
# This is a new (June, 2020) feature. Please provide feedback if you try it out! # Enable this only if using InfluxDB or Loki. This may leak PII data!
# Enable this only if using InfluxDB or Loki. This will leak PII data!
save_events = false save_events = false
# Enable collection of UniFi System Log using the v2 API (Loki only).
# This uses the /v2/api/site/{site}/system-log/all endpoint.
# Recommended for UDM/UDM-Pro/UCG devices running modern firmware.
# Provides richer event data including client roaming, admin access, etc.
# Enable this only if using Loki. This may leak PII data!
save_syslog = false
# Enable collection of UniFi Alarms (InfluxDB/Loki only). # Enable collection of UniFi Alarms (InfluxDB/Loki only).
# There are no dashboards to display this data. It can be used for annotations. # There are no dashboards to display this data. It can be used for annotations.
# This is a new (June, 2020) feature. Please provide feedback if you try it out! # This is a new (June, 2020) feature. Please provide feedback if you try it out!
@@ -232,6 +238,7 @@
# hash_pii = false # hash_pii = false
# save_ids = false # save_ids = false
# save_events = false # save_events = false
# save_syslog = false
# save_alarms = false # save_alarms = false
# save_anomalies = false # save_anomalies = false
# save_dpi = false # save_dpi = false

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/prometheus/common v0.67.4 github.com/prometheus/common v0.67.4
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/unpoller/unifi/v5 v5.3.0 github.com/unpoller/unifi/v5 v5.4.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.46.0
golang.org/x/term v0.38.0 golang.org/x/term v0.38.0
golift.io/cnfg v0.2.3 golift.io/cnfg v0.2.3

4
go.sum
View File

@@ -75,8 +75,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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/unpoller/unifi/v5 v5.3.0 h1:6ykCP4wL5nk/icMu8Qc24ApWD0A5JdCSYxQJIg2FQyg= github.com/unpoller/unifi/v5 v5.4.0 h1:bXNjL0lQi9ldrapXI/gLKmepyYEvOlop9zxkG6GPn/s=
github.com/unpoller/unifi/v5 v5.3.0/go.mod h1:pa6zv4Oyb1nFEm4qu/8CUv8Q25hQof04Wh2D0RXcTYc= github.com/unpoller/unifi/v5 v5.4.0/go.mod h1:pa6zv4Oyb1nFEm4qu/8CUv8Q25hQof04Wh2D0RXcTYc=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=

View File

@@ -34,7 +34,7 @@ func (u *InputUnifi) collectControllerEvents(c *Controller) ([]any, error) {
type caller func([]any, []*unifi.Site, *Controller) ([]any, error) type caller func([]any, []*unifi.Site, *Controller) ([]any, error)
for _, call := range []caller{u.collectIDs, u.collectAnomalies, u.collectAlarms, u.collectEvents} { for _, call := range []caller{u.collectIDs, u.collectAnomalies, u.collectAlarms, u.collectEvents, u.collectSyslog} {
if newLogs, err = call(logs, sites, c); err != nil { if newLogs, err = call(logs, sites, c); err != nil {
return logs, err return logs, err
} }
@@ -98,7 +98,7 @@ func (u *InputUnifi) collectAnomalies(logs []any, sites []*unifi.Site, c *Contro
func (u *InputUnifi) collectEvents(logs []any, sites []*unifi.Site, c *Controller) ([]any, error) { func (u *InputUnifi) collectEvents(logs []any, sites []*unifi.Site, c *Controller) ([]any, error) {
if *c.SaveEvents { if *c.SaveEvents {
u.LogDebugf("Collecting controller site events: %s (%s)", c.URL, c.ID) u.LogDebugf("Collecting controller site events (v1): %s (%s)", c.URL, c.ID)
for _, s := range sites { for _, s := range sites {
events, err := c.Unifi.GetSiteEvents(s, time.Hour) events, err := c.Unifi.GetSiteEvents(s, time.Hour)
@@ -123,6 +123,35 @@ func (u *InputUnifi) collectEvents(logs []any, sites []*unifi.Site, c *Controlle
return logs, nil return logs, nil
} }
func (u *InputUnifi) collectSyslog(logs []any, sites []*unifi.Site, c *Controller) ([]any, error) {
if *c.SaveSyslog {
u.LogDebugf("Collecting controller syslog (v2): %s (%s)", c.URL, c.ID)
// Use v2 system-log API
req := unifi.DefaultSystemLogRequest(time.Hour)
entries, err := c.Unifi.GetSystemLog(sites, req)
if err != nil {
return logs, fmt.Errorf("unifi.GetSystemLog(): %w", err)
}
for _, e := range entries {
e := redactSystemLogEntry(e, c.HashPII, c.DropPII)
logs = append(logs, e)
webserver.NewInputEvent(PluginName, e.SiteName+"_syslog", &webserver.Event{
Msg: e.Msg(), Ts: e.Datetime(), Tags: map[string]string{
"type": "syslog", "key": e.Key, "event": e.Event,
"site_name": e.SiteName, "source": e.SourceName,
"category": e.Category, "subcategory": e.Subcategory,
"severity": e.Severity,
},
})
}
}
return logs, nil
}
func (u *InputUnifi) collectIDs(logs []any, sites []*unifi.Site, c *Controller) ([]any, error) { func (u *InputUnifi) collectIDs(logs []any, sites []*unifi.Site, c *Controller) ([]any, error) {
if *c.SaveIDs { if *c.SaveIDs {
u.LogDebugf("Collecting controller IDs data: %s (%s)", c.URL, c.ID) u.LogDebugf("Collecting controller IDs data: %s (%s)", c.URL, c.ID)
@@ -175,3 +204,50 @@ func redactEvent(e *unifi.Event, hash *bool, dropPII *bool) *unifi.Event {
return e return e
} }
// redactSystemLogEntry attempts to mask personally identifying information from v2 system log entries.
func redactSystemLogEntry(e *unifi.SystemLogEntry, hash *bool, dropPII *bool) *unifi.SystemLogEntry {
if !*hash && !*dropPII {
return e
}
// Redact CLIENT parameter if present
if client, ok := e.Parameters["CLIENT"]; ok {
if *dropPII {
client.Hostname = ""
client.Name = ""
client.ID = ""
client.IP = ""
} else {
client.Hostname = RedactNamePII(client.Hostname, hash, dropPII)
client.Name = RedactNamePII(client.Name, hash, dropPII)
client.ID = RedactMacPII(client.ID, hash, dropPII)
client.IP = RedactIPPII(client.IP, hash, dropPII)
}
e.Parameters["CLIENT"] = client
}
// Redact IP parameter if present
if ip, ok := e.Parameters["IP"]; ok {
if *dropPII {
ip.ID = ""
ip.Name = ""
} else {
ip.ID = RedactIPPII(ip.ID, hash, dropPII)
ip.Name = RedactIPPII(ip.Name, hash, dropPII)
}
e.Parameters["IP"] = ip
}
// Redact ADMIN parameter if present
if admin, ok := e.Parameters["ADMIN"]; ok {
if *dropPII {
admin.Name = ""
} else {
admin.Name = RedactNamePII(admin.Name, hash, dropPII)
}
e.Parameters["ADMIN"] = admin
}
return e
}

View File

@@ -279,6 +279,22 @@ func RedactMacPII(pii string, hash *bool, dropPII *bool) (output string) {
return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", s[:2], s[2:4], s[4:6], s[6:8], s[8:10], s[10:12], s[12:14]) return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", s[:2], s[2:4], s[4:6], s[6:8], s[8:10], s[10:12], s[12:14])
} }
// RedactIPPII converts an IP address to an md5 hashed version (first 12 chars only).
// Useful for maskiing out personally identifying information.
func RedactIPPII(pii string, hash *bool, dropPII *bool) string {
if dropPII != nil && *dropPII {
return ""
}
if hash == nil || !*hash || pii == "" {
return pii
}
s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
// Format as a "fake" IP-like string.
return fmt.Sprintf("%s.%s.%s", s[:4], s[4:8], s[8:12])
}
// getFilteredSites returns a list of sites to fetch data for. // getFilteredSites returns a list of sites to fetch data for.
// Omits requested but unconfigured sites. Grabs the full list from the // Omits requested but unconfigured sites. Grabs the full list from the
// controller and returns the sites provided in the config file. // controller and returns the sites provided in the config file.

View File

@@ -38,6 +38,7 @@ type Controller struct {
SaveAnomal *bool `json:"save_anomalies" toml:"save_anomalies" xml:"save_anomalies" yaml:"save_anomalies"` SaveAnomal *bool `json:"save_anomalies" toml:"save_anomalies" xml:"save_anomalies" yaml:"save_anomalies"`
SaveAlarms *bool `json:"save_alarms" toml:"save_alarms" xml:"save_alarms" yaml:"save_alarms"` SaveAlarms *bool `json:"save_alarms" toml:"save_alarms" xml:"save_alarms" yaml:"save_alarms"`
SaveEvents *bool `json:"save_events" toml:"save_events" xml:"save_events" yaml:"save_events"` SaveEvents *bool `json:"save_events" toml:"save_events" xml:"save_events" yaml:"save_events"`
SaveSyslog *bool `json:"save_syslog" toml:"save_syslog" xml:"save_syslog" yaml:"save_syslog"`
SaveIDs *bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"` SaveIDs *bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"`
SaveDPI *bool `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"` SaveDPI *bool `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"`
SaveRogue *bool `json:"save_rogue" toml:"save_rogue" xml:"save_rogue" yaml:"save_rogue"` SaveRogue *bool `json:"save_rogue" toml:"save_rogue" xml:"save_rogue" yaml:"save_rogue"`
@@ -244,6 +245,10 @@ func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop
c.SaveEvents = &f c.SaveEvents = &f
} }
if c.SaveSyslog == nil {
c.SaveSyslog = &f
}
if c.SaveAlarms == nil { if c.SaveAlarms == nil {
c.SaveAlarms = &f c.SaveAlarms = &f
} }
@@ -323,6 +328,10 @@ func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint
c.SaveEvents = u.Default.SaveEvents c.SaveEvents = u.Default.SaveEvents
} }
if c.SaveSyslog == nil {
c.SaveSyslog = u.Default.SaveSyslog
}
if c.SaveAlarms == nil { if c.SaveAlarms == nil {
c.SaveAlarms = u.Default.SaveAlarms c.SaveAlarms = u.Default.SaveAlarms
} }

View File

@@ -127,7 +127,7 @@ func (u *InputUnifi) logController(c *Controller) {
u.Logf(" => Username: %s (has password: %v) (has api-key: %v)", c.User, c.Pass != "", c.APIKey != "") u.Logf(" => Username: %s (has password: %v) (has api-key: %v)", c.User, c.Pass != "", c.APIKey != "")
u.Logf(" => Hash PII %v / Drop PII %v / Poll Sites: %s", *c.HashPII, *c.DropPII, strings.Join(c.Sites, ", ")) u.Logf(" => Hash PII %v / Drop PII %v / Poll Sites: %s", *c.HashPII, *c.DropPII, strings.Join(c.Sites, ", "))
u.Logf(" => Save Sites %v / Save DPI %v (metrics)", *c.SaveSites, *c.SaveDPI) u.Logf(" => Save Sites %v / Save DPI %v (metrics)", *c.SaveSites, *c.SaveDPI)
u.Logf(" => Save Events %v / Save IDs %v (logs)", *c.SaveEvents, *c.SaveIDs) u.Logf(" => Save Events %v / Save Syslog %v / Save IDs %v (logs)", *c.SaveEvents, *c.SaveSyslog, *c.SaveIDs)
u.Logf(" => Save Alarms %v / Anomalies %v (logs)", *c.SaveAlarms, *c.SaveAnomal) u.Logf(" => Save Alarms %v / Anomalies %v (logs)", *c.SaveAlarms, *c.SaveAnomal)
u.Logf(" => Save Rogue APs: %v", *c.SaveRogue) u.Logf(" => Save Rogue APs: %v", *c.SaveRogue)
} }

View File

@@ -2,9 +2,37 @@
Loki Output Plugin for UnPoller Loki Output Plugin for UnPoller
This plugin writes UniFi Events and IDS data to Loki. Maybe Alarms too. This plugin writes UniFi Events, System Logs, IDS, Alarms, and Anomalies to Loki as JSON.
Example Config: ## Log Types
| Application Label | Config Option | API | Description |
|-------------------|---------------|-----|-------------|
| `unifi_system_log` | `save_syslog` | v2 | System log events (UDM recommended) |
| `unifi_event` | `save_events` | v1 | Legacy events (older controllers) |
| `unifi_ids` | `save_ids` | v1 | Intrusion Detection System events |
| `unifi_alarm` | `save_alarms` | v1 | Alarm events |
| `unifi_anomaly` | `save_anomalies` | v1 | Anomaly events |
## Querying in Loki
All logs are stored as JSON. Use Loki's `| json` parser to extract fields:
```logql
{application="unifi_system_log"} | json
```
Filter by severity:
```logql
{application="unifi_system_log", severity="HIGH"} | json
```
Extract specific fields:
```logql
{application="unifi_system_log"} | json | line_format "{{.message}}"
```
## Example Config
```toml ```toml
[loki] [loki]
@@ -23,4 +51,25 @@ Example Config:
# Used for auth-less multi-tenant. # Used for auth-less multi-tenant.
#tenant_id = "" #tenant_id = ""
[unifi.defaults]
# For UDM/UDM-Pro/UCG devices, use save_syslog (v2 API)
save_syslog = true
# For older controllers, use save_events (v1 API)
save_events = false
# Other log types
save_ids = false
save_alarms = false
save_anomalies = false
```
## Environment Variables
```bash
UP_LOKI_URL=http://localhost:3100
UP_LOKI_INTERVAL=2m
UP_UNIFI_DEFAULT_SAVE_SYSLOG=true
UP_UNIFI_DEFAULT_SAVE_EVENTS=false
``` ```

View File

@@ -55,6 +55,8 @@ func (r *Report) ProcessEventLogs(events *poller.Events) *Logs {
r.Alarm(event, logs) r.Alarm(event, logs)
case *unifi.Anomaly: case *unifi.Anomaly:
r.Anomaly(event, logs) r.Anomaly(event, logs)
case *unifi.SystemLogEntry:
r.SystemLogEvent(event, logs)
default: // unlikely. default: // unlikely.
r.LogErrorf("unknown event type: %T", e) r.LogErrorf("unknown event type: %T", e)
} }
@@ -64,9 +66,10 @@ func (r *Report) ProcessEventLogs(events *poller.Events) *Logs {
} }
func (r *Report) String() string { func (r *Report) String() string {
return fmt.Sprintf("%s: %d, %s: %d, %s: %d, %s: %d, Dur: %v", return fmt.Sprintf("%s: %d, %s: %d, %s: %d, %s: %d, %s: %d, Dur: %v",
typeEvent, r.Counts[typeEvent], typeIDs, r.Counts[typeIDs], typeEvent, r.Counts[typeEvent], typeIDs, r.Counts[typeIDs],
typeAlarm, r.Counts[typeAlarm], typeAnomaly, r.Counts[typeAnomaly], typeAlarm, r.Counts[typeAlarm], typeAnomaly, r.Counts[typeAnomaly],
typeSystemLog, r.Counts[typeSystemLog],
time.Since(r.Start).Round(time.Millisecond)) time.Since(r.Start).Round(time.Millisecond))
} }

View File

@@ -1,6 +1,7 @@
package lokiunifi package lokiunifi
import ( import (
"encoding/json"
"strconv" "strconv"
"github.com/unpoller/unifi/v5" "github.com/unpoller/unifi/v5"
@@ -9,6 +10,7 @@ import (
const typeAlarm = "Alarm" const typeAlarm = "Alarm"
// Alarm stores a structured Alarm for batch sending to Loki. // Alarm stores a structured Alarm for batch sending to Loki.
// Logs the raw JSON for parsing with Loki's `| json` pipeline.
func (r *Report) Alarm(event *unifi.Alarm, logs *Logs) { func (r *Report) Alarm(event *unifi.Alarm, logs *Logs) {
if event.Datetime.Before(r.Oldest) { if event.Datetime.Before(r.Oldest) {
return return
@@ -16,23 +18,18 @@ func (r *Report) Alarm(event *unifi.Alarm, logs *Logs) {
r.Counts[typeAlarm]++ // increase counter and append new log line. r.Counts[typeAlarm]++ // increase counter and append new log line.
// Marshal event to JSON for the log line
msg, err := json.Marshal(event)
if err != nil {
msg = []byte(event.Msg)
}
logs.Streams = append(logs.Streams, LogStream{ logs.Streams = append(logs.Streams, LogStream{
Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Msg}}, Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), string(msg)}},
Labels: CleanLabels(map[string]string{ Labels: CleanLabels(map[string]string{
"application": "unifi_alarm", "application": "unifi_alarm",
"host": event.Host, "source": event.SourceName,
"source": event.SourceName, "site_name": event.SiteName,
"site_name": event.SiteName,
"subsystem": event.Subsystem,
"category": event.Catname.String(),
"event_type": event.EventType,
"key": event.Key,
"app_protocol": event.AppProto,
"protocol": event.Proto,
"interface": event.InIface,
"src_country": event.SrcIPCountry,
"usgip": event.USGIP,
"action": event.InnerAlertAction,
}), }),
}) })
} }

View File

@@ -1,6 +1,7 @@
package lokiunifi package lokiunifi
import ( import (
"encoding/json"
"strconv" "strconv"
"github.com/unpoller/unifi/v5" "github.com/unpoller/unifi/v5"
@@ -9,6 +10,7 @@ import (
const typeAnomaly = "Anomaly" const typeAnomaly = "Anomaly"
// Anomaly stores a structured Anomaly for batch sending to Loki. // Anomaly stores a structured Anomaly for batch sending to Loki.
// Logs the raw JSON for parsing with Loki's `| json` pipeline.
func (r *Report) Anomaly(event *unifi.Anomaly, logs *Logs) { func (r *Report) Anomaly(event *unifi.Anomaly, logs *Logs) {
if event.Datetime.Before(r.Oldest) { if event.Datetime.Before(r.Oldest) {
return return
@@ -16,13 +18,18 @@ func (r *Report) Anomaly(event *unifi.Anomaly, logs *Logs) {
r.Counts[typeAnomaly]++ // increase counter and append new log line. r.Counts[typeAnomaly]++ // increase counter and append new log line.
// Marshal event to JSON for the log line
msg, err := json.Marshal(event)
if err != nil {
msg = []byte(event.Anomaly)
}
logs.Streams = append(logs.Streams, LogStream{ logs.Streams = append(logs.Streams, LogStream{
Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Anomaly}}, Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), string(msg)}},
Labels: CleanLabels(map[string]string{ Labels: CleanLabels(map[string]string{
"application": "unifi_anomaly", "application": "unifi_anomaly",
"source": event.SourceName, "source": event.SourceName,
"site_name": event.SiteName, "site_name": event.SiteName,
"device_mac": event.DeviceMAC,
}), }),
}) })
} }

View File

@@ -1,14 +1,17 @@
package lokiunifi package lokiunifi
import ( import (
"encoding/json"
"strconv" "strconv"
"github.com/unpoller/unifi/v5" "github.com/unpoller/unifi/v5"
) )
const typeEvent = "Event" const typeEvent = "Event"
const typeSystemLog = "SystemLog"
// Event stores a structured UniFi Event for batch sending to Loki. // Event stores a structured UniFi Event for batch sending to Loki.
// Logs the raw JSON for parsing with Loki's `| json` pipeline.
func (r *Report) Event(event *unifi.Event, logs *Logs) { func (r *Report) Event(event *unifi.Event, logs *Logs) {
if event.Datetime.Before(r.Oldest) { if event.Datetime.Before(r.Oldest) {
return return
@@ -16,41 +19,45 @@ func (r *Report) Event(event *unifi.Event, logs *Logs) {
r.Counts[typeEvent]++ // increase counter and append new log line. r.Counts[typeEvent]++ // increase counter and append new log line.
// Marshal event to JSON for the log line
msg, err := json.Marshal(event)
if err != nil {
msg = []byte(event.Msg)
}
logs.Streams = append(logs.Streams, LogStream{ logs.Streams = append(logs.Streams, LogStream{
Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Msg}}, Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), string(msg)}},
Labels: CleanLabels(map[string]string{ Labels: CleanLabels(map[string]string{
"application": "unifi_event", "application": "unifi_event",
"admin": event.Admin, // username "site_name": event.SiteName,
"host": event.Host, "source": event.SourceName,
"hostname": event.Hostname, }),
"site_name": event.SiteName, })
"source": event.SourceName, }
"subsystem": event.Subsystem,
"ap_from": event.ApFrom, // SystemLogEvent stores a structured UniFi v2 System Log Entry for batch sending to Loki.
"ap_to": event.ApTo, // Logs the raw JSON for parsing with Loki's `| json` pipeline.
"ap": event.Ap, func (r *Report) SystemLogEvent(event *unifi.SystemLogEntry, logs *Logs) {
"ap_name": event.ApName, if event.Datetime().Before(r.Oldest) {
"gw": event.Gw, return
"gw_name": event.GwName, }
"sw": event.Sw,
"sw_name": event.SwName, r.Counts[typeSystemLog]++ // increase counter and append new log line.
"category": event.Catname.String(),
"radio": event.Radio, // Marshal event to JSON for the log line
"radio_from": event.RadioFrom, msg, err := json.Marshal(event)
"radio_to": event.RadioTo, if err != nil {
"key": event.Key, msg = []byte(event.TitleRaw)
"interface": event.InIface, }
"event_type": event.EventType,
"ssid": event.SSID, logs.Streams = append(logs.Streams, LogStream{
"channel": event.Channel.Txt, Entries: [][]string{{strconv.FormatInt(event.Datetime().UnixNano(), 10), string(msg)}},
"channel_from": event.ChannelFrom.Txt, Labels: CleanLabels(map[string]string{
"channel_to": event.ChannelTo.Txt, "application": "unifi_system_log",
"usgip": event.USGIP, "site_name": event.SiteName,
"network": event.Network, "source": event.SourceName,
"app_protocol": event.AppProto, "category": event.Category,
"protocol": event.Proto, "severity": event.Severity,
"action": event.InnerAlertAction,
"src_country": event.SrcIPCountry,
}), }),
}) })
} }

View File

@@ -1,6 +1,7 @@
package lokiunifi package lokiunifi
import ( import (
"encoding/json"
"strconv" "strconv"
"github.com/unpoller/unifi/v5" "github.com/unpoller/unifi/v5"
@@ -8,7 +9,8 @@ import (
const typeIDs = "IDs" const typeIDs = "IDs"
// event stores a structured event Event for batch sending to Loki. // IDs stores a structured IDS Event for batch sending to Loki.
// Logs the raw JSON for parsing with Loki's `| json` pipeline.
func (r *Report) IDs(event *unifi.IDS, logs *Logs) { func (r *Report) IDs(event *unifi.IDS, logs *Logs) {
if event.Datetime.Before(r.Oldest) { if event.Datetime.Before(r.Oldest) {
return return
@@ -16,26 +18,18 @@ func (r *Report) IDs(event *unifi.IDS, logs *Logs) {
r.Counts[typeIDs]++ // increase counter and append new log line. r.Counts[typeIDs]++ // increase counter and append new log line.
// Marshal event to JSON for the log line
msg, err := json.Marshal(event)
if err != nil {
msg = []byte(event.Msg)
}
logs.Streams = append(logs.Streams, LogStream{ logs.Streams = append(logs.Streams, LogStream{
Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Msg}}, Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), string(msg)}},
Labels: CleanLabels(map[string]string{ Labels: CleanLabels(map[string]string{
"application": "unifi_ids", "application": "unifi_ids",
"source": event.SourceName, "source": event.SourceName,
"host": event.Host, "site_name": event.SiteName,
"site_name": event.SiteName,
"subsystem": event.Subsystem,
"category": event.Catname.String(),
"event_type": event.EventType,
"key": event.Key,
"app_protocol": event.AppProto,
"protocol": event.Proto,
"interface": event.InIface,
"src_country": event.SrcIPCountry,
"src_city": event.SourceIPGeo.City,
"src_continent": event.SourceIPGeo.ContinentCode,
"src_country_code": event.SourceIPGeo.CountryCode,
"usgip": event.USGIP,
"action": event.InnerAlertAction,
}), }),
}) })
} }