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>
This commit is contained in:
parent
cedc52fc89
commit
22cdc248be
18
go.mod
18
go.mod
|
|
@ -13,6 +13,12 @@ require (
|
|||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/unpoller/unifi/v5 v5.20.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
||||
go.opentelemetry.io/otel/metric v1.42.0
|
||||
go.opentelemetry.io/otel/sdk v1.42.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/term v0.41.0
|
||||
golift.io/cnfg v0.2.5
|
||||
|
|
@ -22,8 +28,19 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -37,7 +54,6 @@ require (
|
|||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oapi-codegen/runtime v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
|
|
|||
44
go.sum
44
go.sum
|
|
@ -13,21 +13,31 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/flaticols/countrycodes v0.0.2 h1:vedxSqHwG3r7lwUK2bfGFWkVcFv7QuSCKFMkywI/rIE=
|
||||
github.com/flaticols/countrycodes v0.0.2/go.mod h1:HCwEez5Z+nf062EOWMPqEh1uLb5QdZSTQmTrq4avBOA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
|
||||
|
|
@ -59,8 +69,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
|||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
|
|
@ -80,6 +90,24 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
|||
github.com/unpoller/unifi/v5 v5.20.0 h1:lbjjHrFZgINGjk+A+efyYsdo21qdQg+WLAXZJpBvqMI=
|
||||
github.com/unpoller/unifi/v5 v5.20.0/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
|
|
@ -110,6 +138,8 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
|||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
|
|
@ -122,6 +152,14 @@ golift.io/cnfgfile v0.0.0-20240713024420-a5436d84eb48 h1:c7cJWRr0cUnFHKtq072esKz
|
|||
golift.io/cnfgfile v0.0.0-20240713024420-a5436d84eb48/go.mod h1:zHm9o8SkZ6Mm5DfGahsrEJPsogyR0qItP59s5lJ98/I=
|
||||
golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE=
|
||||
golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
1
main.go
1
main.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
_ "github.com/unpoller/unpoller/pkg/datadogunifi"
|
||||
_ "github.com/unpoller/unpoller/pkg/influxunifi"
|
||||
_ "github.com/unpoller/unpoller/pkg/lokiunifi"
|
||||
_ "github.com/unpoller/unpoller/pkg/otelunifi"
|
||||
_ "github.com/unpoller/unpoller/pkg/promunifi"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `disable = false` (or `UP_OTEL_DISABLE=false`) to enable it.
|
||||
|
||||
### TOML
|
||||
|
||||
```toml
|
||||
[otel]
|
||||
url = "http://localhost:4318" # OTLP HTTP endpoint (default)
|
||||
protocol = "http" # "http" (default) or "grpc"
|
||||
interval = "30s"
|
||||
timeout = "10s"
|
||||
disable = false
|
||||
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
|
||||
disable: false
|
||||
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_DISABLE` | `true` | Set to `false` 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_DISABLE=false` 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"
|
||||
disable = false
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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...)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
|
||||
// Disable when true disables this output plugin.
|
||||
Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
|
||||
|
||||
// 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 not disabled.
|
||||
func (u *OtelOutput) Enabled() bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if u.Config == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return !u.Disable
|
||||
}
|
||||
|
||||
// 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 config missing (or disabled), OTel output disabled!")
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
ctx 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++
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue