Skip to content

Commit

Permalink
Cleanup Exporter a bit
Browse files Browse the repository at this point in the history
- make WithInterfaces configurable
- move unwiedly prom.MustNewConstMetric into a helper
- reduce unnecessay allocation in intoDesc
- make API tokens configurable via environment
  • Loading branch information
dmke committed Apr 1, 2022
1 parent 086ff40 commit e245d74
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 49 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,33 @@ Location of the YAML config file to load.

Log verbosity level. Defaults to `info`. Use `debug` to get more details.

### WAN Interface Statistics

- Config: `interfaces`
- Args: `--with-interfaces`
- Env: `UNMS_EXPORTER_INTERFACES`

Enable exporting WAN interface metrics. Defaults to `true`.

### UNMS API Tokens

- Config: `token`
- Env: `UNMS_EXPORTER_TOKEN`
- use a comma-separated list of `instance=token` values

Configures an API token per UNMS instance.

Example:

```yaml
token:
"my-unms-instance.example.org": "my token"
my-unms-instance.example.org: "my token"
unms.example.com: "token123"
```
```console
$ UNMS_EXPORTER_TOKEN="my-unms-instance.example.org=my token,unms.example.com=token123" \
unms-exporter
```

## Prometheus Scrape Setup
Expand Down Expand Up @@ -76,6 +92,7 @@ scrape_configs:
## Available Metrics
### Device wide
- `device_cpu`: CPU load average in percent
- `device_ram`: RAM usage in percent
- `device_enabled`: Indicating if device is enabled in UNMS
Expand All @@ -99,7 +116,8 @@ scrape_configs:

### WAN Interface

If an interface is marked as the WAN interface, these metrics are populated.
If an interface is marked as the WAN interface, and WAN interface statistics
are enabled, these metrics are populated.

- `wan_rx_bytes`: Bytes received since last reset
- `wan_tx_bytes`: Bytes transmitted since last reset
Expand Down
38 changes: 38 additions & 0 deletions exporter/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package exporter

import (
"context"
"time"

"github.com/ffddorf/unms-exporter/client/devices"
"github.com/ffddorf/unms-exporter/models"
)

type Device struct {
*models.DeviceStatusOverview
}

func (e *Exporter) fetchDeviceData() ([]Device, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

params := &devices.GetDevicesParams{
WithInterfaces: &defaultWithInterfaces,
Context: ctx,
}
devicesResponse, err := e.api.Devices.GetDevices(params)
if err != nil {
return nil, err
}

data := make([]Device, 0, len(devicesResponse.Payload))
for _, overview := range devicesResponse.Payload {
if overview.Identification == nil {
continue
}
dev := Device{overview}
data = append(data, dev)
}

return data, nil
}
76 changes: 40 additions & 36 deletions exporter/exporter.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package exporter

import (
"context"
"fmt"
"strconv"
"time"

"github.com/ffddorf/unms-exporter/client"
"github.com/ffddorf/unms-exporter/client/devices"
"github.com/ffddorf/unms-exporter/models"
openapi "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
Expand All @@ -33,7 +31,7 @@ var defaultLabels = []string{
}

func (s metricSpec) intoDesc(name string) *prom.Desc {
labels := make([]string, 0, len(s.labels)+2)
labels := make([]string, 0, len(s.labels)+len(defaultLabels))
labels = append(labels, defaultLabels...)
labels = append(labels, s.labels...)
return prom.NewDesc(namespace+"_"+name, s.help, labels, prom.Labels{})
Expand Down Expand Up @@ -74,6 +72,8 @@ type Exporter struct {
api *client.UNMSAPI
metrics map[string]*prom.Desc

withInterfaces bool

// Internal metrics about the exporter
im internalMetrics
log logrus.FieldLogger
Expand All @@ -99,7 +99,13 @@ func New(log logrus.FieldLogger, host string, token string) *Exporter {

im := newInternalMetrics()

return &Exporter{api, metrics, im, log}
return &Exporter{
api: api,
metrics: metrics,
withInterfaces: defaultWithInterfaces,
im: im,
log: log,
}
}

func (e *Exporter) Describe(out chan<- *prom.Desc) {
Expand All @@ -112,8 +118,7 @@ func (e *Exporter) Describe(out chan<- *prom.Desc) {
func (e *Exporter) Collect(out chan<- prom.Metric) {
defer e.im.Collect(out)

err := e.collectImpl(out)
if err != nil {
if err := e.collectImpl(out); err != nil {
e.log.WithError(err).Warn("Metric collection failed")
e.im.errors.Inc()
} else {
Expand Down Expand Up @@ -143,20 +148,19 @@ var (
defaultWithInterfaces = true
)

func (e *Exporter) collectImpl(out chan<- prom.Metric) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
func (e *Exporter) WithInterfaces(enable bool) *Exporter { e.withInterfaces = enable; return e }

params := &devices.GetDevicesParams{
WithInterfaces: &defaultWithInterfaces,
Context: ctx,
}
devices, err := e.api.Devices.GetDevices(params)
func (e *Exporter) newMetric(name string, typ prom.ValueType, val float64, labels ...string) prom.Metric {
return prom.MustNewConstMetric(e.metrics[name], typ, val, labels...)
}

func (e *Exporter) collectImpl(out chan<- prom.Metric) error {
devices, err := e.fetchDeviceData()
if err != nil {
return err
}

for _, device := range devices.Payload {
for _, device := range devices {
if device.Identification == nil {
continue
}
Expand All @@ -181,18 +185,18 @@ func (e *Exporter) collectImpl(out chan<- prom.Metric) error {
siteName,
}

out <- prom.MustNewConstMetric(e.metrics["device_enabled"], prom.GaugeValue, boolToGauge(derefOrFalse(device.Enabled)), deviceLabels...)
out <- e.newMetric("device_enabled", prom.GaugeValue, boolToGauge(derefOrFalse(device.Enabled)), deviceLabels...)
if device.Meta != nil {
out <- prom.MustNewConstMetric(e.metrics["device_maintenance"], prom.GaugeValue, boolToGauge(derefOrFalse(device.Meta.Maintenance)), deviceLabels...)
out <- e.newMetric("device_maintenance", prom.GaugeValue, boolToGauge(derefOrFalse(device.Meta.Maintenance)), deviceLabels...)
}
if device.Overview != nil {
out <- prom.MustNewConstMetric(e.metrics["device_cpu"], prom.GaugeValue, device.Overview.CPU, deviceLabels...)
out <- prom.MustNewConstMetric(e.metrics["device_ram"], prom.GaugeValue, device.Overview.RAM, deviceLabels...)
out <- prom.MustNewConstMetric(e.metrics["device_uptime"], prom.GaugeValue, device.Overview.Uptime, deviceLabels...)
out <- prom.MustNewConstMetric(e.metrics["device_last_seen"], prom.CounterValue, timeToGauge(device.Overview.LastSeen), deviceLabels...)
out <- e.newMetric("device_cpu", prom.GaugeValue, device.Overview.CPU, deviceLabels...)
out <- e.newMetric("device_ram", prom.GaugeValue, device.Overview.RAM, deviceLabels...)
out <- e.newMetric("device_uptime", prom.GaugeValue, device.Overview.Uptime, deviceLabels...)
out <- e.newMetric("device_last_seen", prom.CounterValue, timeToGauge(device.Overview.LastSeen), deviceLabels...)
}
if device.LatestBackup != nil && device.LatestBackup.Timestamp != nil {
out <- prom.MustNewConstMetric(e.metrics["device_last_backup"], prom.GaugeValue, timeToGauge(*device.LatestBackup.Timestamp), deviceLabels...)
out <- e.newMetric("device_last_backup", prom.GaugeValue, timeToGauge(*device.LatestBackup.Timestamp), deviceLabels...)
}

seenInterfaces := make(map[string]struct{})
Expand Down Expand Up @@ -223,29 +227,29 @@ func (e *Exporter) collectImpl(out chan<- prom.Metric) error {
intf.Identification.Type, // ifType
)

out <- prom.MustNewConstMetric(e.metrics["interface_enabled"], prom.GaugeValue, boolToGauge(intf.Enabled), intfLabels...)
out <- e.newMetric("interface_enabled", prom.GaugeValue, boolToGauge(intf.Enabled), intfLabels...)
if intf.Status != nil {
out <- prom.MustNewConstMetric(e.metrics["interface_plugged"], prom.GaugeValue, boolToGauge(intf.Status.Plugged), intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_up"], prom.GaugeValue, boolToGauge(intf.Status.Status == "active"), intfLabels...)
out <- e.newMetric("interface_plugged", prom.GaugeValue, boolToGauge(intf.Status.Plugged), intfLabels...)
out <- e.newMetric("interface_up", prom.GaugeValue, boolToGauge(intf.Status.Status == "active"), intfLabels...)
}

if intf.Statistics != nil {
out <- prom.MustNewConstMetric(e.metrics["interface_dropped"], prom.CounterValue, intf.Statistics.Dropped, intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_errors"], prom.CounterValue, intf.Statistics.Errors, intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_rx_bytes"], prom.CounterValue, intf.Statistics.Rxbytes, intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_tx_bytes"], prom.CounterValue, intf.Statistics.Txbytes, intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_rx_rate"], prom.GaugeValue, intf.Statistics.Rxrate, intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_tx_rate"], prom.GaugeValue, intf.Statistics.Txrate, intfLabels...)
out <- prom.MustNewConstMetric(e.metrics["interface_poe_power"], prom.GaugeValue, intf.Statistics.PoePower, intfLabels...)
out <- e.newMetric("interface_dropped", prom.CounterValue, intf.Statistics.Dropped, intfLabels...)
out <- e.newMetric("interface_errors", prom.CounterValue, intf.Statistics.Errors, intfLabels...)
out <- e.newMetric("interface_rx_bytes", prom.CounterValue, intf.Statistics.Rxbytes, intfLabels...)
out <- e.newMetric("interface_tx_bytes", prom.CounterValue, intf.Statistics.Txbytes, intfLabels...)
out <- e.newMetric("interface_rx_rate", prom.GaugeValue, intf.Statistics.Rxrate, intfLabels...)
out <- e.newMetric("interface_tx_rate", prom.GaugeValue, intf.Statistics.Txrate, intfLabels...)
out <- e.newMetric("interface_poe_power", prom.GaugeValue, intf.Statistics.PoePower, intfLabels...)
}
}

// WAN metrics
if wanIF != nil && wanIF.Statistics != nil {
out <- prom.MustNewConstMetric(e.metrics["wan_rx_bytes"], prom.CounterValue, wanIF.Statistics.Rxbytes, deviceLabels...)
out <- prom.MustNewConstMetric(e.metrics["wan_tx_bytes"], prom.CounterValue, wanIF.Statistics.Txbytes, deviceLabels...)
out <- prom.MustNewConstMetric(e.metrics["wan_rx_rate"], prom.GaugeValue, wanIF.Statistics.Rxrate, deviceLabels...)
out <- prom.MustNewConstMetric(e.metrics["wan_tx_rate"], prom.GaugeValue, wanIF.Statistics.Txrate, deviceLabels...)
out <- e.newMetric("wan_rx_bytes", prom.CounterValue, wanIF.Statistics.Rxbytes, deviceLabels...)
out <- e.newMetric("wan_tx_bytes", prom.CounterValue, wanIF.Statistics.Txbytes, deviceLabels...)
out <- e.newMetric("wan_rx_rate", prom.GaugeValue, wanIF.Statistics.Rxrate, deviceLabels...)
out <- e.newMetric("wan_tx_rate", prom.GaugeValue, wanIF.Statistics.Txrate, deviceLabels...)
}
}

Expand Down
38 changes: 27 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,27 @@ import (
)

type config struct {
ServerAddr string `mapstructure:"listen" split_words:"true"`
LogLevel logrus.Level `mapstructure:"log_level" split_words:"true"`
ServerAddr string `mapstructure:"listen" split_words:"true"`
LogLevel logrus.Level `mapstructure:"log_level" split_words:"true"`
WithInterfaces bool `mapstructure:"interfaces" envconfig:"interfaces" default:"true"`
TokenPerHost tokenMap `mapstructure:"token" envconfig:"token"`
}

type tokenMap map[string]string

var errInvalidTokenFormat = errors.New("invalid API token format: must be instance=token")

TokenPerHost map[string]string `mapstructure:"token" envconfig:"-"`
func (toks *tokenMap) Decode(v string) error {
t := make(map[string]string)
for _, pair := range strings.Split(v, ",") {
kv := strings.Split(pair, "=")
if len(kv) != 2 {
return errInvalidTokenFormat
}
t[kv[0]] = kv[1]
}
*toks = tokenMap(t)
return nil
}

const envPrefix = "UNMS_EXPORTER"
Expand All @@ -29,20 +46,19 @@ func (c *config) validate() error {
if len(c.TokenPerHost) < 1 {
return errors.New("No token configured")
}

if c.ServerAddr == "" {
return errors.New("Server addr cannot be nil")
}

return nil
}

func main() {
log := logrus.New()

conf := &config{
ServerAddr: "[::]:9806",
LogLevel: logrus.InfoLevel,
ServerAddr: "[::]:9806",
LogLevel: logrus.InfoLevel,
WithInterfaces: true,
}

if err := envconfig.Process(envPrefix, conf); err != nil {
Expand All @@ -52,6 +68,7 @@ func main() {
flags := pflag.NewFlagSet("unms_exporter", pflag.ContinueOnError)
flags.StringP("listen", "l", conf.ServerAddr, "Address for the exporter to listen on")
flags.StringP("config", "c", "", "Config file to use")
flags.Bool("with-interfaces", conf.WithInterfaces, "Enable exporting WAN interface metrics")
if err := flags.Parse(os.Args[1:]); err != nil {
log.WithError(err).Fatal("failed to parse flags")
}
Expand Down Expand Up @@ -85,10 +102,9 @@ func main() {
prometheus.NewBuildInfoCollector(),
prometheus.NewGoCollector(),
)
export := exporter.New(
log.WithField("component", "exporter").WithField("host", host),
host, token,
)
logger := log.WithField("component", "exporter").WithField("host", host)
export := exporter.New(logger, host, token).
WithInterfaces(conf.WithInterfaces)
if err := registry.Register(export); err != nil {
log.WithField("host", host).WithError(err).Fatal("failed to register exporter")
}
Expand Down

0 comments on commit e245d74

Please sign in to comment.