Merge branch 'master' of ../lokiunifi into merge-them-all
This commit is contained in:
commit
24595013fa
|
|
@ -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 ./...
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 = ""
|
||||
```
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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...)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue