mirror of
https://github.com/unpoller/unpoller.git
synced 2026-03-31 06:24:21 -04:00
feat(otelunifi): add OpenTelemetry output plugin (#978)
* feat(otelunifi): add OpenTelemetry output plugin Adds a new push-based output plugin that exports UniFi metrics to any OTLP-compatible backend (Grafana Alloy/Mimir, Honeycomb, Datadog via OTel, New Relic, etc.) using the Go OpenTelemetry SDK v1.42. Config (default disabled): [otel] url = "http://localhost:4318" protocol = "http" # or "grpc" interval = "30s" timeout = "10s" disable = false api_key = "" # optional Bearer auth Env var prefix: UP_OTEL_* Exported metrics: - Sites: user/guest/IoT counts, AP/GW/SW counts, latency, uptime, tx/rx rates per subsystem - Clients: uptime, rx/tx bytes & rates; signal/noise/RSSI for wireless - UAP: up, uptime, CPU/mem, load, per-radio channel/power, per-VAP station count/satisfaction/bytes - USW: up, uptime, CPU/mem, load, aggregate rx/tx bytes, per-port up/speed/bytes/packets/errors/dropped/PoE - USG: up, uptime, CPU/mem, load, per-WAN rx/tx bytes/packets/errors - UDM/UXG: up, uptime, CPU/mem, load averages Closes #933 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix(otelunifi): rename unused ctx parameter to _ in recordGauge Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix(otelunifi): replace Disable with Enable (default false) Plugin is opt-in: set enable=true / UP_OTEL_ENABLE=true to activate. Closes part of #933. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
main.go
1
main.go
@@ -13,6 +13,7 @@ import (
|
|||||||
_ "github.com/unpoller/unpoller/pkg/datadogunifi"
|
_ "github.com/unpoller/unpoller/pkg/datadogunifi"
|
||||||
_ "github.com/unpoller/unpoller/pkg/influxunifi"
|
_ "github.com/unpoller/unpoller/pkg/influxunifi"
|
||||||
_ "github.com/unpoller/unpoller/pkg/lokiunifi"
|
_ "github.com/unpoller/unpoller/pkg/lokiunifi"
|
||||||
|
_ "github.com/unpoller/unpoller/pkg/otelunifi"
|
||||||
_ "github.com/unpoller/unpoller/pkg/promunifi"
|
_ "github.com/unpoller/unpoller/pkg/promunifi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
150
pkg/otelunifi/README.md
Normal file
150
pkg/otelunifi/README.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# otelunifi — OpenTelemetry Output Plugin
|
||||||
|
|
||||||
|
Exports UniFi metrics to any [OpenTelemetry Protocol (OTLP)](https://opentelemetry.io/docs/specs/otel/protocol/) compatible backend via push, using the Go OTel SDK.
|
||||||
|
|
||||||
|
Compatible backends include Grafana Alloy/Mimir, Honeycomb, Datadog (via OTel collector), Grafana Tempo, New Relic, Lightstep, and any vendor that accepts OTLP.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The plugin is **disabled by default**. Set `enable = true` (or `UP_OTEL_ENABLE=true`) to enable it.
|
||||||
|
|
||||||
|
### TOML
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[otel]
|
||||||
|
url = "http://localhost:4318" # OTLP HTTP endpoint (default)
|
||||||
|
protocol = "http" # "http" (default) or "grpc"
|
||||||
|
interval = "30s"
|
||||||
|
timeout = "10s"
|
||||||
|
enable = true
|
||||||
|
dead_ports = false
|
||||||
|
|
||||||
|
# Optional bearer token for authenticated collectors (e.g. Grafana Cloud)
|
||||||
|
api_key = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### YAML
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
otel:
|
||||||
|
url: "http://localhost:4318"
|
||||||
|
protocol: http
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
enable: true
|
||||||
|
dead_ports: false
|
||||||
|
api_key: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
All config keys use the `UP_OTEL_` prefix:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `UP_OTEL_URL` | `http://localhost:4318` | OTLP endpoint URL |
|
||||||
|
| `UP_OTEL_PROTOCOL` | `http` | Transport: `http` or `grpc` |
|
||||||
|
| `UP_OTEL_INTERVAL` | `30s` | Push interval |
|
||||||
|
| `UP_OTEL_TIMEOUT` | `10s` | Per-export timeout |
|
||||||
|
| `UP_OTEL_ENABLE` | `false` | Set to `true` to enable |
|
||||||
|
| `UP_OTEL_API_KEY` | `` | Bearer token for auth |
|
||||||
|
| `UP_OTEL_DEAD_PORTS` | `false` | Include down/disabled switch ports |
|
||||||
|
|
||||||
|
## Protocol Notes
|
||||||
|
|
||||||
|
- **HTTP** (`protocol = "http"`): Sends to `<url>/v1/metrics`. Default port `4318`.
|
||||||
|
- **gRPC** (`protocol = "grpc"`): Sends to `<host>:<port>`. Default `localhost:4317`. The URL for gRPC should be `host:port` (no scheme).
|
||||||
|
|
||||||
|
## Exported Metrics
|
||||||
|
|
||||||
|
All metrics use the `unifi_` prefix and carry identifying attributes (labels).
|
||||||
|
|
||||||
|
### Site metrics (`unifi_site_*`)
|
||||||
|
|
||||||
|
Attributes: `site_name`, `source`, `subsystem`, `status`
|
||||||
|
|
||||||
|
| Metric | Description |
|
||||||
|
|---|---|
|
||||||
|
| `unifi_site_users` | Connected user count |
|
||||||
|
| `unifi_site_guests` | Connected guest count |
|
||||||
|
| `unifi_site_iot` | IoT device count |
|
||||||
|
| `unifi_site_aps` | Access point count |
|
||||||
|
| `unifi_site_gateways` | Gateway count |
|
||||||
|
| `unifi_site_switches` | Switch count |
|
||||||
|
| `unifi_site_adopted` | Adopted device count |
|
||||||
|
| `unifi_site_disconnected` | Disconnected device count |
|
||||||
|
| `unifi_site_latency_seconds` | WAN latency |
|
||||||
|
| `unifi_site_uptime_seconds` | Site uptime |
|
||||||
|
| `unifi_site_tx_bytes_rate` | Transmit bytes rate |
|
||||||
|
| `unifi_site_rx_bytes_rate` | Receive bytes rate |
|
||||||
|
|
||||||
|
### Client metrics (`unifi_client_*`)
|
||||||
|
|
||||||
|
Attributes: `mac`, `name`, `ip`, `site_name`, `source`, `oui`, `network`, `ap_name`, `sw_name`, `wired`
|
||||||
|
|
||||||
|
Wireless-only additional attributes: `essid`, `radio`, `radio_proto`
|
||||||
|
|
||||||
|
| Metric | Description |
|
||||||
|
|---|---|
|
||||||
|
| `unifi_client_uptime_seconds` | Client uptime |
|
||||||
|
| `unifi_client_rx_bytes` | Total bytes received |
|
||||||
|
| `unifi_client_tx_bytes` | Total bytes transmitted |
|
||||||
|
| `unifi_client_rx_bytes_rate` | Receive rate |
|
||||||
|
| `unifi_client_tx_bytes_rate` | Transmit rate |
|
||||||
|
| `unifi_client_signal_db` | Signal strength (wireless) |
|
||||||
|
| `unifi_client_noise_db` | Noise floor (wireless) |
|
||||||
|
| `unifi_client_rssi_db` | RSSI (wireless) |
|
||||||
|
| `unifi_client_tx_rate_bps` | TX rate (wireless) |
|
||||||
|
| `unifi_client_rx_rate_bps` | RX rate (wireless) |
|
||||||
|
|
||||||
|
### Device metrics
|
||||||
|
|
||||||
|
#### UAP (`unifi_device_uap_*`)
|
||||||
|
|
||||||
|
Attributes: `mac`, `name`, `model`, `version`, `type`, `ip`, `site_name`, `source`
|
||||||
|
|
||||||
|
Includes: `up`, `uptime_seconds`, `cpu_utilization`, `mem_utilization`, `load_avg_{1,5,15}`, per-radio `channel`/`tx_power_dbm`, per-VAP `num_stations`/`satisfaction`/`rx_bytes`/`tx_bytes`.
|
||||||
|
|
||||||
|
#### USW (`unifi_device_usw_*`)
|
||||||
|
|
||||||
|
Attributes: `mac`, `name`, `model`, `version`, `type`, `ip`, `site_name`, `source`
|
||||||
|
|
||||||
|
Includes: `up`, `uptime_seconds`, `cpu_utilization`, `mem_utilization`, `load_avg_1`, `rx_bytes`, `tx_bytes`, and per-port metrics (`port_up`, `port_speed_mbps`, `port_rx_bytes`, `port_tx_bytes`, `port_poe_*`, etc.).
|
||||||
|
|
||||||
|
#### USG (`unifi_device_usg_*`)
|
||||||
|
|
||||||
|
Includes: `up`, `uptime_seconds`, `cpu_utilization`, `mem_utilization`, and per-WAN interface (`wan_rx_bytes`, `wan_tx_bytes`, `wan_rx_packets`, `wan_tx_packets`, `wan_rx_errors`, `wan_tx_errors`, `wan_speed_mbps`).
|
||||||
|
|
||||||
|
#### UDM (`unifi_device_udm_*`)
|
||||||
|
|
||||||
|
Includes: `up`, `uptime_seconds`, `cpu_utilization`, `mem_utilization`, `load_avg_{1,5,15}`.
|
||||||
|
|
||||||
|
#### UXG (`unifi_device_uxg_*`)
|
||||||
|
|
||||||
|
Includes: `up`, `uptime_seconds`, `cpu_utilization`, `mem_utilization`, `load_avg_1`.
|
||||||
|
|
||||||
|
## Example: Grafana Alloy
|
||||||
|
|
||||||
|
```alloy
|
||||||
|
otelcol.receiver.otlp "default" {
|
||||||
|
grpc { endpoint = "0.0.0.0:4317" }
|
||||||
|
http { endpoint = "0.0.0.0:4318" }
|
||||||
|
|
||||||
|
output {
|
||||||
|
metrics = [otelcol.exporter.prometheus.default.input]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `UP_OTEL_URL=http://alloy-host:4318` and `UP_OTEL_ENABLE=true` in unpoller's environment.
|
||||||
|
|
||||||
|
## Example: Grafana Cloud (OTLP with auth)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[otel]
|
||||||
|
url = "https://otlp-gateway-prod-us-central-0.grafana.net/otlp"
|
||||||
|
protocol = "http"
|
||||||
|
api_key = "instanceID:grafana_cloud_api_token"
|
||||||
|
interval = "60s"
|
||||||
|
enable = true
|
||||||
|
```
|
||||||
6
pkg/otelunifi/events.go
Normal file
6
pkg/otelunifi/events.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
// Events handling is intentionally a no-op for now.
|
||||||
|
// UniFi events (alarms, IDS, anomalies) are log-like data that would be
|
||||||
|
// better suited for an OTel Logs signal once the Go OTel SDK stabilises
|
||||||
|
// the log bridge API. This file exists as a placeholder.
|
||||||
47
pkg/otelunifi/logger.go
Normal file
47
pkg/otelunifi/logger.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/unpoller/unpoller/pkg/webserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logf logs an informational message.
|
||||||
|
func (u *OtelOutput) Logf(msg string, v ...any) {
|
||||||
|
webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{
|
||||||
|
Ts: time.Now(),
|
||||||
|
Msg: fmt.Sprintf(msg, v...),
|
||||||
|
Tags: map[string]string{"type": "info"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if u.Collector != nil {
|
||||||
|
u.Collector.Logf(msg, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogErrorf logs an error message.
|
||||||
|
func (u *OtelOutput) LogErrorf(msg string, v ...any) {
|
||||||
|
webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{
|
||||||
|
Ts: time.Now(),
|
||||||
|
Msg: fmt.Sprintf(msg, v...),
|
||||||
|
Tags: map[string]string{"type": "error"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if u.Collector != nil {
|
||||||
|
u.Collector.LogErrorf(msg, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebugf logs a debug message.
|
||||||
|
func (u *OtelOutput) LogDebugf(msg string, v ...any) {
|
||||||
|
webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{
|
||||||
|
Ts: time.Now(),
|
||||||
|
Msg: fmt.Sprintf(msg, v...),
|
||||||
|
Tags: map[string]string{"type": "debug"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if u.Collector != nil {
|
||||||
|
u.Collector.LogDebugf(msg, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
298
pkg/otelunifi/otelunifi.go
Normal file
298
pkg/otelunifi/otelunifi.go
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// Package otelunifi provides the methods to turn UniFi measurements into
|
||||||
|
// OpenTelemetry metrics and export them via OTLP.
|
||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
"golift.io/cnfg"
|
||||||
|
|
||||||
|
"github.com/unpoller/unpoller/pkg/poller"
|
||||||
|
"github.com/unpoller/unpoller/pkg/webserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginName is the name of this plugin.
|
||||||
|
const PluginName = "otel"
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultInterval = 30 * time.Second
|
||||||
|
minimumInterval = 10 * time.Second
|
||||||
|
defaultOTLPHTTPURL = "http://localhost:4318"
|
||||||
|
defaultOTLPGRPCURL = "localhost:4317"
|
||||||
|
protoHTTP = "http"
|
||||||
|
protoGRPC = "grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config defines the data needed to export metrics via OpenTelemetry.
|
||||||
|
type Config struct {
|
||||||
|
// URL is the OTLP endpoint to send metrics to.
|
||||||
|
// For HTTP: http://localhost:4318
|
||||||
|
// For gRPC: localhost:4317
|
||||||
|
URL string `json:"url,omitempty" toml:"url,omitempty" xml:"url" yaml:"url"`
|
||||||
|
|
||||||
|
// APIKey is an optional bearer token / API key for authentication.
|
||||||
|
// Sent as the "Authorization: Bearer <key>" header.
|
||||||
|
APIKey string `json:"api_key,omitempty" toml:"api_key,omitempty" xml:"api_key" yaml:"api_key"`
|
||||||
|
|
||||||
|
// Interval controls the push interval for sending metrics to the OTLP endpoint.
|
||||||
|
Interval cnfg.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"`
|
||||||
|
|
||||||
|
// Timeout is the per-export deadline.
|
||||||
|
Timeout cnfg.Duration `json:"timeout,omitempty" toml:"timeout,omitempty" xml:"timeout" yaml:"timeout"`
|
||||||
|
|
||||||
|
// Protocol selects the OTLP transport protocol: "http" (default) or "grpc".
|
||||||
|
Protocol string `json:"protocol,omitempty" toml:"protocol,omitempty" xml:"protocol" yaml:"protocol"`
|
||||||
|
|
||||||
|
// Enable when true enables this output plugin.
|
||||||
|
Enable bool `json:"enable" toml:"enable" xml:"enable,attr" yaml:"enable"`
|
||||||
|
|
||||||
|
// DeadPorts when true will save data for dead ports, for example ports that are down or disabled.
|
||||||
|
DeadPorts bool `json:"dead_ports" toml:"dead_ports" xml:"dead_ports" yaml:"dead_ports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OtelUnifi wraps the config for nested TOML/JSON/YAML config file support.
|
||||||
|
type OtelUnifi struct {
|
||||||
|
*Config `json:"otel" toml:"otel" xml:"otel" yaml:"otel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OtelOutput is the working struct for this plugin.
|
||||||
|
type OtelOutput struct {
|
||||||
|
Collector poller.Collect
|
||||||
|
LastCheck time.Time
|
||||||
|
provider *sdkmetric.MeterProvider
|
||||||
|
*OtelUnifi
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ poller.OutputPlugin = &OtelOutput{}
|
||||||
|
|
||||||
|
func init() { //nolint:gochecknoinits
|
||||||
|
u := &OtelOutput{OtelUnifi: &OtelUnifi{Config: &Config{}}, LastCheck: time.Now()}
|
||||||
|
|
||||||
|
poller.NewOutput(&poller.Output{
|
||||||
|
Name: PluginName,
|
||||||
|
Config: u.OtelUnifi,
|
||||||
|
OutputPlugin: u,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled returns true when the plugin is configured and enabled.
|
||||||
|
func (u *OtelOutput) Enabled() bool {
|
||||||
|
if u == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Config == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Enable
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugOutput validates the plugin configuration without starting the run loop.
|
||||||
|
func (u *OtelOutput) DebugOutput() (bool, error) {
|
||||||
|
if u == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.Enabled() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.setConfigDefaults()
|
||||||
|
|
||||||
|
if u.URL == "" {
|
||||||
|
return false, fmt.Errorf("otel: URL must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := u.Protocol
|
||||||
|
if proto != protoHTTP && proto != protoGRPC {
|
||||||
|
return false, fmt.Errorf("otel: protocol must be %q or %q, got %q", protoHTTP, protoGRPC, proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run is the main loop called by the poller core.
|
||||||
|
func (u *OtelOutput) Run(c poller.Collect) error {
|
||||||
|
u.Collector = c
|
||||||
|
|
||||||
|
if !u.Enabled() {
|
||||||
|
u.LogDebugf("OTel output not enabled, skipping.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Logf("OpenTelemetry (OTel) output plugin enabled")
|
||||||
|
u.setConfigDefaults()
|
||||||
|
|
||||||
|
if err := u.setupProvider(); err != nil {
|
||||||
|
return fmt.Errorf("otel: setup provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := u.provider.Shutdown(ctx); err != nil {
|
||||||
|
u.LogErrorf("otel: shutdown provider: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
webserver.UpdateOutput(&webserver.Output{Name: PluginName, Config: u.Config})
|
||||||
|
u.pollController()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollController runs the ticker loop, pushing metrics on each tick.
|
||||||
|
func (u *OtelOutput) pollController() {
|
||||||
|
interval := u.Interval.Duration.Round(time.Second)
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
u.Logf("OTel->OTLP started, protocol: %s, interval: %v, url: %s",
|
||||||
|
u.Protocol, interval, u.URL)
|
||||||
|
|
||||||
|
for u.LastCheck = range ticker.C {
|
||||||
|
u.poll(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// poll fetches metrics once and sends them to the OTLP endpoint.
|
||||||
|
func (u *OtelOutput) poll(interval time.Duration) {
|
||||||
|
metrics, err := u.Collector.Metrics(&poller.Filter{Name: "unifi"})
|
||||||
|
if err != nil {
|
||||||
|
u.LogErrorf("metric fetch for OTel failed: %v", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := u.Collector.Events(&poller.Filter{Name: "unifi", Dur: interval})
|
||||||
|
if err != nil {
|
||||||
|
u.LogErrorf("event fetch for OTel failed: %v", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := u.reportMetrics(metrics, events)
|
||||||
|
if err != nil {
|
||||||
|
u.LogErrorf("otel report: %v", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Logf("OTel Metrics Exported. %v", report)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupProvider creates and registers the OTel MeterProvider with an OTLP exporter.
|
||||||
|
func (u *OtelOutput) setupProvider() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
exp, err := u.buildExporter(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building exporter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := resource.New(ctx,
|
||||||
|
resource.WithAttributes(
|
||||||
|
semconv.ServiceName(poller.AppName),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building resource: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.provider = sdkmetric.NewMeterProvider(
|
||||||
|
sdkmetric.WithReader(
|
||||||
|
sdkmetric.NewPeriodicReader(exp,
|
||||||
|
sdkmetric.WithInterval(u.Interval.Duration),
|
||||||
|
sdkmetric.WithTimeout(u.Timeout.Duration),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
sdkmetric.WithResource(res),
|
||||||
|
)
|
||||||
|
|
||||||
|
otel.SetMeterProvider(u.provider)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildExporter creates either an HTTP or gRPC OTLP exporter.
|
||||||
|
func (u *OtelOutput) buildExporter(ctx context.Context) (sdkmetric.Exporter, error) {
|
||||||
|
switch u.Protocol {
|
||||||
|
case protoGRPC:
|
||||||
|
opts := []otlpmetricgrpc.Option{
|
||||||
|
otlpmetricgrpc.WithEndpoint(u.URL),
|
||||||
|
otlpmetricgrpc.WithInsecure(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.APIKey != "" {
|
||||||
|
opts = append(opts, otlpmetricgrpc.WithHeaders(map[string]string{
|
||||||
|
"Authorization": "Bearer " + u.APIKey,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := otlpmetricgrpc.New(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("grpc exporter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exp, nil
|
||||||
|
|
||||||
|
default: // http
|
||||||
|
opts := []otlpmetrichttp.Option{
|
||||||
|
otlpmetrichttp.WithEndpoint(u.URL),
|
||||||
|
otlpmetrichttp.WithInsecure(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.APIKey != "" {
|
||||||
|
opts = append(opts, otlpmetrichttp.WithHeaders(map[string]string{
|
||||||
|
"Authorization": "Bearer " + u.APIKey,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := otlpmetrichttp.New(ctx, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("http exporter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setConfigDefaults fills in zero-value fields with sensible defaults.
|
||||||
|
func (u *OtelOutput) setConfigDefaults() {
|
||||||
|
if u.Protocol == "" {
|
||||||
|
u.Protocol = protoHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.URL == "" {
|
||||||
|
switch u.Protocol {
|
||||||
|
case protoGRPC:
|
||||||
|
u.URL = defaultOTLPGRPCURL
|
||||||
|
default:
|
||||||
|
u.URL = defaultOTLPHTTPURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Interval.Duration == 0 {
|
||||||
|
u.Interval = cnfg.Duration{Duration: defaultInterval}
|
||||||
|
} else if u.Interval.Duration < minimumInterval {
|
||||||
|
u.Interval = cnfg.Duration{Duration: minimumInterval}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Interval = cnfg.Duration{Duration: u.Interval.Duration.Round(time.Second)}
|
||||||
|
|
||||||
|
if u.Timeout.Duration == 0 {
|
||||||
|
u.Timeout = cnfg.Duration{Duration: 10 * time.Second}
|
||||||
|
}
|
||||||
|
}
|
||||||
233
pkg/otelunifi/report.go
Normal file
233
pkg/otelunifi/report.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
"github.com/unpoller/unpoller/pkg/poller"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Report accumulates counters that are printed to a log line.
|
||||||
|
type Report struct {
|
||||||
|
Total int // Total count of metrics recorded.
|
||||||
|
Errors int // Total count of errors recording metrics.
|
||||||
|
Sites int // Total count of sites exported.
|
||||||
|
Clients int // Total count of clients exported.
|
||||||
|
UAP int // Total count of UAP devices exported.
|
||||||
|
USW int // Total count of USW devices exported.
|
||||||
|
USG int // Total count of USG devices exported.
|
||||||
|
UDM int // Total count of UDM devices exported.
|
||||||
|
UXG int // Total count of UXG devices exported.
|
||||||
|
Elapsed time.Duration // Duration elapsed collecting and exporting.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Report) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Sites: %d, Clients: %d, UAP: %d, USW: %d, USG/UDM/UXG: %d/%d/%d, Metrics: %d, Errs: %d, Elapsed: %v",
|
||||||
|
r.Sites, r.Clients, r.UAP, r.USW, r.USG, r.UDM, r.UXG,
|
||||||
|
r.Total, r.Errors, r.Elapsed.Round(time.Millisecond),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportMetrics converts poller.Metrics to OTel measurements.
|
||||||
|
func (u *OtelOutput) reportMetrics(m *poller.Metrics, _ *poller.Events) (*Report, error) {
|
||||||
|
r := &Report{}
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
meter := otel.GetMeterProvider().Meter(PluginName)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
u.exportSites(ctx, meter, m, r)
|
||||||
|
u.exportClients(ctx, meter, m, r)
|
||||||
|
u.exportDevices(ctx, meter, m, r)
|
||||||
|
|
||||||
|
r.Elapsed = time.Since(start)
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportSites emits site-level gauge metrics.
|
||||||
|
func (u *OtelOutput) exportSites(ctx context.Context, meter metric.Meter, m *poller.Metrics, r *Report) {
|
||||||
|
for _, item := range m.Sites {
|
||||||
|
s, ok := item.(*unifi.Site)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Sites++
|
||||||
|
|
||||||
|
for _, h := range s.Health {
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("subsystem", h.Subsystem),
|
||||||
|
attribute.String("status", h.Status),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_users",
|
||||||
|
"Number of users on the site subsystem", h.NumUser.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_guests",
|
||||||
|
"Number of guests on the site subsystem", h.NumGuest.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_iot",
|
||||||
|
"Number of IoT devices on the site subsystem", h.NumIot.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_aps",
|
||||||
|
"Number of access points", h.NumAp.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_gateways",
|
||||||
|
"Number of gateways", h.NumGw.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_switches",
|
||||||
|
"Number of switches", h.NumSw.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_adopted",
|
||||||
|
"Number of adopted devices", h.NumAdopted.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_disconnected",
|
||||||
|
"Number of disconnected devices", h.NumDisconnected.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_pending",
|
||||||
|
"Number of pending devices", h.NumPending.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_disabled",
|
||||||
|
"Number of disabled devices", h.NumDisabled.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_latency_seconds",
|
||||||
|
"Site WAN latency in seconds", h.Latency.Val/1000, attrs) //nolint:mnd
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_uptime_seconds",
|
||||||
|
"Site uptime in seconds", h.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_tx_bytes_rate",
|
||||||
|
"Site transmit bytes rate", h.TxBytesR.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_site_rx_bytes_rate",
|
||||||
|
"Site receive bytes rate", h.RxBytesR.Val, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportClients emits per-client gauge metrics.
|
||||||
|
func (u *OtelOutput) exportClients(ctx context.Context, meter metric.Meter, m *poller.Metrics, r *Report) {
|
||||||
|
for _, item := range m.Clients {
|
||||||
|
c, ok := item.(*unifi.Client)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Clients++
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", c.Mac),
|
||||||
|
attribute.String("site_name", c.SiteName),
|
||||||
|
attribute.String("source", c.SourceName),
|
||||||
|
attribute.String("name", c.Name),
|
||||||
|
attribute.String("ip", c.IP),
|
||||||
|
attribute.String("oui", c.Oui),
|
||||||
|
attribute.String("network", c.Network),
|
||||||
|
attribute.String("ap_name", c.ApName),
|
||||||
|
attribute.String("sw_name", c.SwName),
|
||||||
|
attribute.Bool("wired", c.IsWired.Val),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_uptime_seconds",
|
||||||
|
"Client uptime in seconds", c.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_rx_bytes",
|
||||||
|
"Client total bytes received", c.RxBytes.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_tx_bytes",
|
||||||
|
"Client total bytes transmitted", c.TxBytes.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_rx_bytes_rate",
|
||||||
|
"Client receive bytes rate", c.RxBytesR.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_tx_bytes_rate",
|
||||||
|
"Client transmit bytes rate", c.TxBytesR.Val, attrs)
|
||||||
|
|
||||||
|
if !c.IsWired.Val {
|
||||||
|
wifiAttrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", c.Mac),
|
||||||
|
attribute.String("site_name", c.SiteName),
|
||||||
|
attribute.String("source", c.SourceName),
|
||||||
|
attribute.String("name", c.Name),
|
||||||
|
attribute.String("ip", c.IP),
|
||||||
|
attribute.String("oui", c.Oui),
|
||||||
|
attribute.String("network", c.Network),
|
||||||
|
attribute.String("ap_name", c.ApName),
|
||||||
|
attribute.String("sw_name", c.SwName),
|
||||||
|
attribute.Bool("wired", false),
|
||||||
|
attribute.String("essid", c.Essid),
|
||||||
|
attribute.String("radio", c.Radio),
|
||||||
|
attribute.String("radio_proto", c.RadioProto),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_signal_db",
|
||||||
|
"Client signal strength in dBm", c.Signal.Val, wifiAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_noise_db",
|
||||||
|
"Client AP noise floor in dBm", c.Noise.Val, wifiAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_rssi_db",
|
||||||
|
"Client RSSI in dBm", c.Rssi.Val, wifiAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_tx_rate_bps",
|
||||||
|
"Client transmit rate in bps", c.TxRate.Val, wifiAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_client_rx_rate_bps",
|
||||||
|
"Client receive rate in bps", c.RxRate.Val, wifiAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportDevices routes each device to its type-specific exporter.
|
||||||
|
func (u *OtelOutput) exportDevices(ctx context.Context, meter metric.Meter, m *poller.Metrics, r *Report) {
|
||||||
|
for _, item := range m.Devices {
|
||||||
|
switch d := item.(type) {
|
||||||
|
case *unifi.UAP:
|
||||||
|
r.UAP++
|
||||||
|
u.exportUAP(ctx, meter, r, d)
|
||||||
|
|
||||||
|
case *unifi.USW:
|
||||||
|
r.USW++
|
||||||
|
u.exportUSW(ctx, meter, r, d)
|
||||||
|
|
||||||
|
case *unifi.USG:
|
||||||
|
r.USG++
|
||||||
|
u.exportUSG(ctx, meter, r, d)
|
||||||
|
|
||||||
|
case *unifi.UDM:
|
||||||
|
r.UDM++
|
||||||
|
u.exportUDM(ctx, meter, r, d)
|
||||||
|
|
||||||
|
case *unifi.UXG:
|
||||||
|
r.UXG++
|
||||||
|
u.exportUXG(ctx, meter, r, d)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if u.Collector.Poller().LogUnknownTypes {
|
||||||
|
u.LogDebugf("otel: unknown device type: %T", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordGauge is a helper that records a single float64 gauge observation.
|
||||||
|
func (u *OtelOutput) recordGauge(
|
||||||
|
_ context.Context,
|
||||||
|
meter metric.Meter,
|
||||||
|
r *Report,
|
||||||
|
name, description string,
|
||||||
|
value float64,
|
||||||
|
attrs attribute.Set,
|
||||||
|
) {
|
||||||
|
g, err := meter.Float64ObservableGauge(name, metric.WithDescription(description))
|
||||||
|
if err != nil {
|
||||||
|
r.Errors++
|
||||||
|
u.LogDebugf("otel: creating gauge %s: %v", name, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {
|
||||||
|
o.ObserveFloat64(g, value, metric.WithAttributeSet(attrs))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, g)
|
||||||
|
if err != nil {
|
||||||
|
r.Errors++
|
||||||
|
u.LogDebugf("otel: registering callback for %s: %v", name, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Total++
|
||||||
|
}
|
||||||
87
pkg/otelunifi/uap.go
Normal file
87
pkg/otelunifi/uap.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportUAP emits metrics for a wireless access point.
|
||||||
|
func (u *OtelOutput) exportUAP(ctx context.Context, meter metric.Meter, r *Report, s *unifi.UAP) {
|
||||||
|
if !s.Adopted.Val || s.Locating.Val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("model", s.Model),
|
||||||
|
attribute.String("version", s.Version),
|
||||||
|
attribute.String("type", s.Type),
|
||||||
|
attribute.String("ip", s.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_uptime_seconds",
|
||||||
|
"UAP uptime in seconds", s.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_cpu_utilization",
|
||||||
|
"UAP CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_mem_utilization",
|
||||||
|
"UAP memory utilization percentage", s.SystemStats.Mem.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_load_avg_1",
|
||||||
|
"UAP load average 1-minute", s.SysStats.Loadavg1.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_load_avg_5",
|
||||||
|
"UAP load average 5-minute", s.SysStats.Loadavg5.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_load_avg_15",
|
||||||
|
"UAP load average 15-minute", s.SysStats.Loadavg15.Val, attrs)
|
||||||
|
|
||||||
|
up := 0.0
|
||||||
|
if s.State.Val == 1 {
|
||||||
|
up = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_up",
|
||||||
|
"Whether UAP is up (1) or down (0)", up, attrs)
|
||||||
|
|
||||||
|
for _, radio := range s.RadioTable {
|
||||||
|
radioAttrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("radio", radio.Radio),
|
||||||
|
attribute.String("radio_name", radio.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_radio_channel",
|
||||||
|
"UAP radio channel", float64(radio.Channel.Val), radioAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_radio_tx_power_dbm",
|
||||||
|
"UAP radio transmit power in dBm", radio.TxPower.Val, radioAttrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vap := range s.VapTable {
|
||||||
|
vapAttrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("essid", vap.Essid),
|
||||||
|
attribute.String("bssid", vap.Bssid),
|
||||||
|
attribute.String("radio", vap.Radio),
|
||||||
|
)
|
||||||
|
|
||||||
|
// NumSta is a plain int in the unifi library
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_vap_num_stations",
|
||||||
|
"UAP VAP connected station count", float64(vap.NumSta), vapAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_vap_satisfaction",
|
||||||
|
"UAP VAP client satisfaction score", vap.Satisfaction.Val, vapAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_vap_rx_bytes",
|
||||||
|
"UAP VAP receive bytes total", vap.RxBytes.Val, vapAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uap_vap_tx_bytes",
|
||||||
|
"UAP VAP transmit bytes total", vap.TxBytes.Val, vapAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
pkg/otelunifi/udm.go
Normal file
82
pkg/otelunifi/udm.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportUDM emits metrics for a UniFi Dream Machine (all variants).
|
||||||
|
func (u *OtelOutput) exportUDM(ctx context.Context, meter metric.Meter, r *Report, s *unifi.UDM) {
|
||||||
|
if !s.Adopted.Val || s.Locating.Val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("model", s.Model),
|
||||||
|
attribute.String("version", s.Version),
|
||||||
|
attribute.String("type", s.Type),
|
||||||
|
attribute.String("ip", s.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
up := 0.0
|
||||||
|
if s.State.Val == 1 {
|
||||||
|
up = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_up",
|
||||||
|
"Whether UDM is up (1) or down (0)", up, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_uptime_seconds",
|
||||||
|
"UDM uptime in seconds", s.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_cpu_utilization",
|
||||||
|
"UDM CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_mem_utilization",
|
||||||
|
"UDM memory utilization percentage", s.SystemStats.Mem.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_load_avg_1",
|
||||||
|
"UDM load average 1-minute", s.SysStats.Loadavg1.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_load_avg_5",
|
||||||
|
"UDM load average 5-minute", s.SysStats.Loadavg5.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_udm_load_avg_15",
|
||||||
|
"UDM load average 15-minute", s.SysStats.Loadavg15.Val, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportUXG emits metrics for a UniFi Next-Gen Gateway.
|
||||||
|
func (u *OtelOutput) exportUXG(ctx context.Context, meter metric.Meter, r *Report, s *unifi.UXG) {
|
||||||
|
if !s.Adopted.Val || s.Locating.Val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("model", s.Model),
|
||||||
|
attribute.String("version", s.Version),
|
||||||
|
attribute.String("type", s.Type),
|
||||||
|
attribute.String("ip", s.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
up := 0.0
|
||||||
|
if s.State.Val == 1 {
|
||||||
|
up = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uxg_up",
|
||||||
|
"Whether UXG is up (1) or down (0)", up, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uxg_uptime_seconds",
|
||||||
|
"UXG uptime in seconds", s.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uxg_cpu_utilization",
|
||||||
|
"UXG CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uxg_mem_utilization",
|
||||||
|
"UXG memory utilization percentage", s.SystemStats.Mem.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_uxg_load_avg_1",
|
||||||
|
"UXG load average 1-minute", s.SysStats.Loadavg1.Val, attrs)
|
||||||
|
}
|
||||||
87
pkg/otelunifi/usg.go
Normal file
87
pkg/otelunifi/usg.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportUSG emits metrics for a UniFi Security Gateway.
|
||||||
|
func (u *OtelOutput) exportUSG(ctx context.Context, meter metric.Meter, r *Report, s *unifi.USG) {
|
||||||
|
if !s.Adopted.Val || s.Locating.Val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("model", s.Model),
|
||||||
|
attribute.String("version", s.Version),
|
||||||
|
attribute.String("type", s.Type),
|
||||||
|
attribute.String("ip", s.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
up := 0.0
|
||||||
|
if s.State.Val == 1 {
|
||||||
|
up = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_up",
|
||||||
|
"Whether USG is up (1) or down (0)", up, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_uptime_seconds",
|
||||||
|
"USG uptime in seconds", s.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_cpu_utilization",
|
||||||
|
"USG CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_mem_utilization",
|
||||||
|
"USG memory utilization percentage", s.SystemStats.Mem.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_load_avg_1",
|
||||||
|
"USG load average 1-minute", s.SysStats.Loadavg1.Val, attrs)
|
||||||
|
|
||||||
|
// Export WAN1 metrics
|
||||||
|
u.exportUSGWan(ctx, meter, r, s, s.Wan1, "wan1")
|
||||||
|
// Export WAN2 metrics if present
|
||||||
|
u.exportUSGWan(ctx, meter, r, s, s.Wan2, "wan2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportUSGWan emits metrics for a single WAN interface on a USG.
|
||||||
|
func (u *OtelOutput) exportUSGWan(
|
||||||
|
ctx context.Context,
|
||||||
|
meter metric.Meter,
|
||||||
|
r *Report,
|
||||||
|
s *unifi.USG,
|
||||||
|
wan unifi.Wan,
|
||||||
|
ifaceName string,
|
||||||
|
) {
|
||||||
|
if wan.IP == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wanAttrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("iface", ifaceName),
|
||||||
|
attribute.String("ip", wan.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_rx_bytes",
|
||||||
|
"USG WAN interface receive bytes total", wan.RxBytes.Val, wanAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_tx_bytes",
|
||||||
|
"USG WAN interface transmit bytes total", wan.TxBytes.Val, wanAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_rx_packets",
|
||||||
|
"USG WAN interface receive packets total", wan.RxPackets.Val, wanAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_tx_packets",
|
||||||
|
"USG WAN interface transmit packets total", wan.TxPackets.Val, wanAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_rx_errors",
|
||||||
|
"USG WAN interface receive errors total", wan.RxErrors.Val, wanAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_tx_errors",
|
||||||
|
"USG WAN interface transmit errors total", wan.TxErrors.Val, wanAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usg_wan_speed_mbps",
|
||||||
|
"USG WAN interface link speed in Mbps", wan.Speed.Val, wanAttrs)
|
||||||
|
}
|
||||||
120
pkg/otelunifi/usw.go
Normal file
120
pkg/otelunifi/usw.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportUSW emits metrics for a UniFi switch.
|
||||||
|
func (u *OtelOutput) exportUSW(ctx context.Context, meter metric.Meter, r *Report, s *unifi.USW) {
|
||||||
|
if !s.Adopted.Val || s.Locating.Val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("model", s.Model),
|
||||||
|
attribute.String("version", s.Version),
|
||||||
|
attribute.String("type", s.Type),
|
||||||
|
attribute.String("ip", s.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
up := 0.0
|
||||||
|
if s.State.Val == 1 {
|
||||||
|
up = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_up",
|
||||||
|
"Whether USW is up (1) or down (0)", up, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_uptime_seconds",
|
||||||
|
"USW uptime in seconds", s.Uptime.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_cpu_utilization",
|
||||||
|
"USW CPU utilization percentage", s.SystemStats.CPU.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_mem_utilization",
|
||||||
|
"USW memory utilization percentage", s.SystemStats.Mem.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_load_avg_1",
|
||||||
|
"USW load average 1-minute", s.SysStats.Loadavg1.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_rx_bytes",
|
||||||
|
"USW total receive bytes", s.Stat.Sw.RxBytes.Val, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_tx_bytes",
|
||||||
|
"USW total transmit bytes", s.Stat.Sw.TxBytes.Val, attrs)
|
||||||
|
|
||||||
|
if !u.DeadPorts {
|
||||||
|
for _, p := range s.PortTable {
|
||||||
|
if !p.Up.Val || !p.Enable.Val {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.exportUSWPort(ctx, meter, r, s, p)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, p := range s.PortTable {
|
||||||
|
u.exportUSWPort(ctx, meter, r, s, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportUSWPort emits metrics for a single switch port.
|
||||||
|
func (u *OtelOutput) exportUSWPort(
|
||||||
|
ctx context.Context,
|
||||||
|
meter metric.Meter,
|
||||||
|
r *Report,
|
||||||
|
s *unifi.USW,
|
||||||
|
p unifi.Port,
|
||||||
|
) {
|
||||||
|
portAttrs := attribute.NewSet(
|
||||||
|
attribute.String("mac", s.Mac),
|
||||||
|
attribute.String("site_name", s.SiteName),
|
||||||
|
attribute.String("source", s.SourceName),
|
||||||
|
attribute.String("name", s.Name),
|
||||||
|
attribute.String("port_name", p.Name),
|
||||||
|
attribute.Int64("port_num", int64(p.PortIdx.Val)),
|
||||||
|
attribute.String("port_mac", p.Mac),
|
||||||
|
attribute.String("port_ip", p.IP),
|
||||||
|
)
|
||||||
|
|
||||||
|
portUp := 0.0
|
||||||
|
if p.Up.Val {
|
||||||
|
portUp = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_up",
|
||||||
|
"Whether switch port is up (1) or down (0)", portUp, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_speed_mbps",
|
||||||
|
"Switch port speed in Mbps", p.Speed.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_rx_bytes",
|
||||||
|
"Switch port receive bytes total", p.RxBytes.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_tx_bytes",
|
||||||
|
"Switch port transmit bytes total", p.TxBytes.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_rx_bytes_rate",
|
||||||
|
"Switch port receive bytes rate", p.RxBytesR.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_tx_bytes_rate",
|
||||||
|
"Switch port transmit bytes rate", p.TxBytesR.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_rx_packets",
|
||||||
|
"Switch port receive packets total", p.RxPackets.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_tx_packets",
|
||||||
|
"Switch port transmit packets total", p.TxPackets.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_rx_errors",
|
||||||
|
"Switch port receive errors total", p.RxErrors.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_tx_errors",
|
||||||
|
"Switch port transmit errors total", p.TxErrors.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_rx_dropped",
|
||||||
|
"Switch port receive dropped total", p.RxDropped.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_tx_dropped",
|
||||||
|
"Switch port transmit dropped total", p.TxDropped.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_poe_current_amps",
|
||||||
|
"Switch port PoE current in amps", p.PoeCurrent.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_poe_power_watts",
|
||||||
|
"Switch port PoE power in watts", p.PoePower.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_poe_voltage",
|
||||||
|
"Switch port PoE voltage", p.PoeVoltage.Val, portAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_device_usw_port_satisfaction",
|
||||||
|
"Switch port satisfaction score", p.Satisfaction.Val, portAttrs)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user