diff --git a/integrations/lokiunifi/.travis.yml b/integrations/lokiunifi/.travis.yml new file mode 100644 index 00000000..749ee76f --- /dev/null +++ b/integrations/lokiunifi/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: +- 1.16.x +before_install: + # download super-linter: golangci-lint +- curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest +script: +- golangci-lint run --enable-all -D exhaustivestruct,nlreturn +- go test ./... diff --git a/integrations/lokiunifi/LICENSE b/integrations/lokiunifi/LICENSE new file mode 100644 index 00000000..e6ac092e --- /dev/null +++ b/integrations/lokiunifi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2021 David Newhall II + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/integrations/lokiunifi/README.md b/integrations/lokiunifi/README.md new file mode 100644 index 00000000..ebc5f4a6 --- /dev/null +++ b/integrations/lokiunifi/README.md @@ -0,0 +1,26 @@ +# lokiunifi + +Loki Output Plugin for UnPoller + +This plugin writes UniFi Events and IDS data to Loki. Maybe Alarms too. + +Example Config: + +```toml +[loki] + # URL is the only required setting for Loki. + url = "http://192.168.3.2:3100" + + # How often to poll UniFi and report to Loki. + interval = "2m" + + # How long to wait for Loki responses. + timeout = "5s" + + # Set these to use basic auth. + #user = "" + #pass = "" + + # Used for auth-less multi-tenant. + #tenant_id = "" +``` diff --git a/integrations/lokiunifi/client.go b/integrations/lokiunifi/client.go new file mode 100644 index 00000000..8a3c17d8 --- /dev/null +++ b/integrations/lokiunifi/client.go @@ -0,0 +1,101 @@ +package lokiunifi + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +const ( + lokiPushPath = "/loki/api/v1/push" +) + +var errStatusCode = fmt.Errorf("unexpected HTTP status code") + +// Client holds the http client for contacting Loki. +type Client struct { + *Config + *http.Client +} + +func (l *Loki) httpClient() *Client { + return &Client{ + Config: l.Config, + Client: &http.Client{ + Timeout: l.Timeout.Duration, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: !l.VerifySSL, // nolint: gosec + }, + }, + }, + } +} + +// Post marshals and posts a batch of log messages. +func (c *Client) Post(logs interface{}) error { + msg, err := json.Marshal(logs) + if err != nil { + return fmt.Errorf("json marshal: %w", err) + } + + u := strings.TrimSuffix(c.URL, lokiPushPath) + lokiPushPath + + req, err := c.NewRequest(u, "POST", "application/json", msg) + if err != nil { + return err + } + + if code, body, err := c.Do(req); err != nil { + return err + } else if code != http.StatusNoContent { + m := fmt.Sprintf("%s (%d/%s) %s, msg: %s", u, code, http.StatusText(code), + strings.TrimSpace(strings.ReplaceAll(string(body), "\n", " ")), msg) + + return fmt.Errorf("%s: %w", m, errStatusCode) + } + + return nil +} + +// NewRequest creates the http request based on input data. +func (c *Client) NewRequest(url, method, cType string, msg []byte) (*http.Request, error) { + req, err := http.NewRequest(method, url, bytes.NewBuffer(msg)) //nolint:noctx + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + if cType != "" { + req.Header.Set("Content-Type", cType) + } + + if c.Username != "" || c.Password != "" { + req.SetBasicAuth(c.Username, c.Password) + } + + if c.TenantID != "" { + req.Header.Set("X-Scope-OrgID", c.TenantID) + } + + return req, nil +} + +// Do makes an http request and returns the status code, body and/or an error. +func (c *Client) Do(req *http.Request) (int, []byte, error) { + resp, err := c.Client.Do(req) + if err != nil { + return 0, nil, fmt.Errorf("making request: %w", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, body, fmt.Errorf("reading body: %w", err) + } + + return resp.StatusCode, body, nil +} diff --git a/integrations/lokiunifi/go.mod b/integrations/lokiunifi/go.mod new file mode 100644 index 00000000..24fcf540 --- /dev/null +++ b/integrations/lokiunifi/go.mod @@ -0,0 +1,11 @@ +module github.com/unpoller/lokiunifi + +go 1.16 + +require ( + github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 + github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 + github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golift.io/cnfg v0.0.7 +) diff --git a/integrations/lokiunifi/go.sum b/integrations/lokiunifi/go.sum new file mode 100644 index 00000000..3d616959 --- /dev/null +++ b/integrations/lokiunifi/go.sum @@ -0,0 +1,58 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c h1:zqmyTlQyufRC65JnImJ6H1Sf7BDj8bG31EV919NVEQc= +github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 h1:YAv5naMdpOFahnxteFFRidZlrSEwLv8V2nBKJKmLmHg= +github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28/go.mod h1:AbDp60t5WlLSRELAliMJ0RFQpm/0yXpyolVSZqNtero= +github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 h1:XjHGfJhMwnB63DYHgtWGJgDxLhxVcAOtf+cfuvpGoyo= +github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80/go.mod h1:K9QFFGfZws4gzB+Popix19S/rBKqrtqI+tyPORyg3F0= +github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf h1:HhXi3qca3kyFEFPh0mqdr0bpQs94hJvMbUJztwPtf2A= +github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf/go.mod h1:77PywuUvspdtoRuH1htFhR3Tp0pLyWj6kJlYR4tBYho= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golift.io/cnfg v0.0.7 h1:qkNpP5Bq+5Gtoc6HcI8kapMD5zFOVan6qguxqBQF3OY= +golift.io/cnfg v0.0.7/go.mod h1:AsB0DJe7nv0bizKaoy3e3MjjOF7upTpMOMvsfv4CNNk= +golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE= +golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integrations/lokiunifi/logger.go b/integrations/lokiunifi/logger.go new file mode 100644 index 00000000..ce0322b7 --- /dev/null +++ b/integrations/lokiunifi/logger.go @@ -0,0 +1,38 @@ +package lokiunifi + +import ( + "fmt" + "time" + + "github.com/unpoller/webserver" +) + +// Logf logs a message. +func (l *Loki) Logf(msg string, v ...interface{}) { + webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ + Ts: time.Now(), + Msg: fmt.Sprintf(msg, v...), + Tags: map[string]string{"type": "info"}, + }) + l.Collect.Logf(msg, v...) +} + +// LogErrorf logs an error message. +func (l *Loki) LogErrorf(msg string, v ...interface{}) { + webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ + Ts: time.Now(), + Msg: fmt.Sprintf(msg, v...), + Tags: map[string]string{"type": "error"}, + }) + l.Collect.LogErrorf(msg, v...) +} + +// LogDebugf logs a debug message. +func (l *Loki) LogDebugf(msg string, v ...interface{}) { + webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ + Ts: time.Now(), + Msg: fmt.Sprintf(msg, v...), + Tags: map[string]string{"type": "debug"}, + }) + l.Collect.LogDebugf(msg, v...) +} diff --git a/integrations/lokiunifi/loki.go b/integrations/lokiunifi/loki.go new file mode 100644 index 00000000..37956aa7 --- /dev/null +++ b/integrations/lokiunifi/loki.go @@ -0,0 +1,143 @@ +package lokiunifi + +import ( + "fmt" + "io/ioutil" + "strconv" + "strings" + "time" + + "github.com/unpoller/poller" + "github.com/unpoller/webserver" + "golift.io/cnfg" +) + +const ( + maxInterval = 10 * time.Minute + minInterval = 10 * time.Second + defaultTimeout = 10 * time.Second + defaultInterval = 2 * time.Minute +) + +const ( + // InputName is the name of plugin that gives us data. + InputName = "unifi" + // PluginName is the name of this plugin. + PluginName = "loki" +) + +// Config is the plugin's input data. +type Config struct { + Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` + VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` + URL string `json:"url" toml:"url" xml:"url" yaml:"url"` + Username string `json:"user" toml:"user" xml:"user" yaml:"user"` + Password string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` + TenantID string `json:"tenant_id" toml:"tenant_id" xml:"tenant_id" yaml:"tenant_id"` + Interval cnfg.Duration `json:"interval" toml:"interval" xml:"interval" yaml:"interval"` + Timeout cnfg.Duration `json:"timeout" toml:"timeout" xml:"timeout" yaml:"timeout"` +} + +// Loki is the main library struct. This satisfies the poller.Output interface. +type Loki struct { + Collect poller.Collect + *Config `json:"loki" toml:"loki" xml:"loki" yaml:"loki"` + client *Client + last time.Time +} + +// init is how this modular code is initialized by the main app. +// This module adds itself as an output module to the poller core. +func init() { // nolint: gochecknoinits + l := &Loki{Config: &Config{ + Interval: cnfg.Duration{Duration: defaultInterval}, + Timeout: cnfg.Duration{Duration: defaultTimeout}, + }} + + poller.NewOutput(&poller.Output{ + Name: PluginName, + Config: l, + Method: l.Run, + }) +} + +// Run is fired from the poller library after the Config is unmarshalled. +func (l *Loki) Run(collect poller.Collect) error { + if l.Collect = collect; l.Config == nil || l.URL == "" || l.Disable { + l.Logf("Loki config missing (or disabled), Loki output disabled!") + return nil + } + + l.ValidateConfig() + + fake := *l.Config + fake.Password = strconv.FormatBool(fake.Password != "") + + webserver.UpdateOutput(&webserver.Output{Name: PluginName, Config: fake}) + l.PollController() + l.LogErrorf("Loki Output Plugin Stopped!") + + return nil +} + +// ValidateConfig sets initial "last" update time. Also creates an http client, +// makes sure URL is sane, and sets interval within min/max limits. +func (l *Loki) ValidateConfig() { + if l.Interval.Duration > maxInterval { + l.Interval.Duration = maxInterval + } else if l.Interval.Duration < minInterval { + l.Interval.Duration = minInterval + } + + if strings.HasPrefix(l.Password, "file://") { + pass, err := ioutil.ReadFile(strings.TrimPrefix(l.Password, "file://")) + if err != nil { + l.LogErrorf("Reading Loki Password File: %v", err) + } + + l.Password = strings.TrimSpace(string(pass)) + } + + l.last = time.Now().Add(-l.Interval.Duration) + l.client = l.httpClient() + l.URL = strings.TrimRight(l.URL, "/") // gets a path appended to it later. +} + +// PollController runs forever, polling UniFi for events and pushing them to Loki. +// This is started by Run(). +func (l *Loki) PollController() { + interval := l.Interval.Round(time.Second) + l.Logf("Loki Event collection started, interval: %v, URL: %s", interval, l.URL) + + ticker := time.NewTicker(interval) + for start := range ticker.C { + events, err := l.Collect.Events(&poller.Filter{Name: InputName}) + if err != nil { + l.LogErrorf("event fetch for Loki failed: %v", err) + continue + } + + err = l.ProcessEvents(l.NewReport(start), events) + if err != nil { + l.LogErrorf("%v", err) + } + } +} + +// ProcessEvents offloads some of the loop from PollController. +func (l *Loki) ProcessEvents(report *Report, events *poller.Events) error { + // Sometimes it gets stuck on old messages. This gets it past that. + if time.Since(l.last) > 4*l.Interval.Duration { + l.last = time.Now().Add(-4 * l.Interval.Duration) + } + + logs := report.ProcessEventLogs(events) + if err := l.client.Post(logs); err != nil { + return fmt.Errorf("sending to Loki failed: %w", err) + } + + l.last = report.Start + l.Logf("Events sent to Loki. %v", report) + + return nil +} diff --git a/integrations/lokiunifi/report.go b/integrations/lokiunifi/report.go new file mode 100644 index 00000000..bcf06bd2 --- /dev/null +++ b/integrations/lokiunifi/report.go @@ -0,0 +1,82 @@ +package lokiunifi + +import ( + "fmt" + "strings" + "time" + + "github.com/unpoller/poller" + "github.com/unpoller/unifi" +) + +// LogStream contains a stream of logs (like a log file). +// This app uses one stream per log entry because each log may have different labels. +type LogStream struct { + Labels map[string]string `json:"stream"` // "the file name" + Entries [][]string `json:"values"` // "the log lines" +} + +// Logs is the main logs-holding structure. This is the Loki-output format. +type Logs struct { + Streams []LogStream `json:"streams"` // "multiple files" +} + +// Report is the temporary data generated by processing events. +type Report struct { + Start time.Time + Oldest time.Time + poller.Logger + Counts map[string]int +} + +// NewReport makes a new report. +func (l *Loki) NewReport(start time.Time) *Report { + return &Report{ + Start: start, + Oldest: l.last, + Logger: l, + Counts: make(map[string]int), + } +} + +// ProcessEventLogs loops the event Logs, matches the interface type, calls the +// appropriate method for the data, and compiles the Logs into a Loki format. +// This runs once per interval, if there was no collection error. +func (r *Report) ProcessEventLogs(events *poller.Events) *Logs { + logs := &Logs{} + + for _, e := range events.Logs { + switch event := e.(type) { + case *unifi.IDS: + r.IDS(event, logs) + case *unifi.Event: + r.Event(event, logs) + case *unifi.Alarm: + r.Alarm(event, logs) + case *unifi.Anomaly: + r.Anomaly(event, logs) + default: // unlikely. + r.LogErrorf("unknown event type: %T", e) + } + } + + return logs +} + +func (r *Report) String() string { + return fmt.Sprintf("%s: %d, %s: %d, %s: %d, %s: %d, Dur: %v", + typeEvent, r.Counts[typeEvent], typeIDS, r.Counts[typeIDS], + typeAlarm, r.Counts[typeAlarm], typeAnomaly, r.Counts[typeAnomaly], + time.Since(r.Start).Round(time.Millisecond)) +} + +// CleanLabels removes any tag that is empty. +func CleanLabels(labels map[string]string) map[string]string { + for i := range labels { + if strings.TrimSpace(labels[i]) == "" { + delete(labels, i) + } + } + + return labels +} diff --git a/integrations/lokiunifi/report_alarm.go b/integrations/lokiunifi/report_alarm.go new file mode 100644 index 00000000..5c039f84 --- /dev/null +++ b/integrations/lokiunifi/report_alarm.go @@ -0,0 +1,37 @@ +package lokiunifi + +import ( + "strconv" + + "github.com/unpoller/unifi" +) + +const typeAlarm = "Alarm" + +// Alarm stores a structured Alarm for batch sending to Loki. +func (r *Report) Alarm(event *unifi.Alarm, logs *Logs) { + if event.Datetime.Before(r.Oldest) { + return + } + + r.Counts[typeAlarm]++ // increase counter and append new log line. + + logs.Streams = append(logs.Streams, LogStream{ + Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Msg}}, + Labels: CleanLabels(map[string]string{ + "application": "unifi_alarm", + "source": event.SourceName, + "site_name": event.SiteName, + "subsystem": event.Subsystem, + "category": event.Catname, + "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, + }), + }) +} diff --git a/integrations/lokiunifi/report_anomaly.go b/integrations/lokiunifi/report_anomaly.go new file mode 100644 index 00000000..bce0fc4a --- /dev/null +++ b/integrations/lokiunifi/report_anomaly.go @@ -0,0 +1,28 @@ +package lokiunifi + +import ( + "strconv" + + "github.com/unpoller/unifi" +) + +const typeAnomaly = "Anomaly" + +// Anomaly stores a structured Anomaly for batch sending to Loki. +func (r *Report) Anomaly(event *unifi.Anomaly, logs *Logs) { + if event.Datetime.Before(r.Oldest) { + return + } + + r.Counts[typeAnomaly]++ // increase counter and append new log line. + + logs.Streams = append(logs.Streams, LogStream{ + Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Anomaly}}, + Labels: CleanLabels(map[string]string{ + "application": "unifi_anomaly", + "source": event.SourceName, + "site_name": event.SiteName, + "device_mac": event.DeviceMAC, + }), + }) +} diff --git a/integrations/lokiunifi/report_event.go b/integrations/lokiunifi/report_event.go new file mode 100644 index 00000000..33037985 --- /dev/null +++ b/integrations/lokiunifi/report_event.go @@ -0,0 +1,54 @@ +package lokiunifi + +import ( + "strconv" + + "github.com/unpoller/unifi" +) + +const typeEvent = "Event" + +// Event stores a structured UniFi Event for batch sending to Loki. +func (r *Report) Event(event *unifi.Event, logs *Logs) { + if event.Datetime.Before(r.Oldest) { + return + } + + r.Counts[typeEvent]++ // increase counter and append new log line. + + logs.Streams = append(logs.Streams, LogStream{ + Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Msg}}, + Labels: CleanLabels(map[string]string{ + "application": "unifi_event", + "admin": event.Admin, // username + "site_name": event.SiteName, + "source": event.SourceName, + "subsystem": event.Subsystem, + "ap_from": event.ApFrom, + "ap_to": event.ApTo, + "ap": event.Ap, + "ap_name": event.ApName, + "gw": event.Gw, + "gw_name": event.GwName, + "sw": event.Sw, + "sw_name": event.SwName, + "category": event.Catname, + "radio": event.Radio, + "radio_from": event.RadioFrom, + "radio_to": event.RadioTo, + "key": event.Key, + "interface": event.InIface, + "event_type": event.EventType, + "ssid": event.SSID, + "channel": event.Channel.Txt, + "channel_from": event.ChannelFrom.Txt, + "channel_to": event.ChannelTo.Txt, + "usgip": event.USGIP, + "network": event.Network, + "app_protocol": event.AppProto, + "protocol": event.Proto, + "action": event.InnerAlertAction, + "src_country": event.SrcIPCountry, + }), + }) +} diff --git a/integrations/lokiunifi/report_ids.go b/integrations/lokiunifi/report_ids.go new file mode 100644 index 00000000..c43e1614 --- /dev/null +++ b/integrations/lokiunifi/report_ids.go @@ -0,0 +1,37 @@ +package lokiunifi + +import ( + "strconv" + + "github.com/unpoller/unifi" +) + +const typeIDS = "IDS" + +// event stores a structured event Event for batch sending to Loki. +func (r *Report) IDS(event *unifi.IDS, logs *Logs) { + if event.Datetime.Before(r.Oldest) { + return + } + + r.Counts[typeIDS]++ // increase counter and append new log line. + + logs.Streams = append(logs.Streams, LogStream{ + Entries: [][]string{{strconv.FormatInt(event.Datetime.UnixNano(), 10), event.Msg}}, + Labels: CleanLabels(map[string]string{ + "application": "unifi_ids", + "source": event.SourceName, + "site_name": event.SiteName, + "subsystem": event.Subsystem, + "category": event.Catname, + "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, + }), + }) +}