161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
package lokiunifi
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/unpoller/unpoller/pkg/poller"
|
|
"github.com/unpoller/unpoller/pkg/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
|
|
}
|
|
|
|
var _ poller.OutputPlugin = &Loki{}
|
|
|
|
// 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,
|
|
OutputPlugin: l,
|
|
})
|
|
}
|
|
|
|
func (l *Loki) Enabled() bool {
|
|
if l == nil {
|
|
return false
|
|
}
|
|
if l.Config == nil {
|
|
return false
|
|
}
|
|
if l.URL == "" {
|
|
return false
|
|
}
|
|
return !l.Disable
|
|
}
|
|
|
|
// Run is fired from the poller library after the Config is unmarshalled.
|
|
func (l *Loki) Run(collect poller.Collect) error {
|
|
l.Collect = collect
|
|
if !l.Enabled() {
|
|
l.LogDebugf("Loki config missing (or disabled), Loki output disabled!")
|
|
return nil
|
|
}
|
|
l.Logf("Loki enabled")
|
|
|
|
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 := os.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
|
|
}
|