mirror of
https://github.com/unpoller/unpoller.git
synced 2026-10-18 22:40:03 +02:00
feat(unas): export the SSD cache and its cache-bay SSDs (#1109)
This commit is contained in:
@@ -310,7 +310,7 @@
|
||||
# This section is entirely optional. The plugin is off unless you set enable = true and
|
||||
# add at least one [[unas.device]] below; otherwise it stays silent and inert.
|
||||
# Metrics land under the unifi_unas_ prefix in Prometheus, and in the unas_device,
|
||||
# unas_pool, unas_disk, and unas_share measurements in InfluxDB.
|
||||
# unas_pool, unas_pool_cache, unas_disk, and unas_share measurements in InfluxDB.
|
||||
#
|
||||
#[unas]
|
||||
# enable = true
|
||||
|
||||
@@ -72,3 +72,5 @@ require (
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
|
||||
replace github.com/unpoller/unifi/v6 => github.com/cpcloud/unifi/v6 v6.0.0-20261017135054-b7989cc3fe67
|
||||
|
||||
@@ -25,6 +25,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
||||
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/cpcloud/unifi/v6 v6.0.0-20261017135054-b7989cc3fe67 h1:rEiPcEkXizLqLkEKgaND9ozy9WJZMNjSp6on85/PHNY=
|
||||
github.com/cpcloud/unifi/v6 v6.0.0-20261017135054-b7989cc3fe67/go.mod h1:fJ1lU/mpKrQ8Jv3b2yq399ATlrUuiOZAMbKWcw/1GaU=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -121,8 +123,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/unpoller/unifi/v6 v6.3.1 h1:gUBOYmSl/kmTAlSVfTEaDYqGU6iao5biMDeuQcCZmLE=
|
||||
github.com/unpoller/unifi/v6 v6.3.1/go.mod h1:fJ1lU/mpKrQ8Jv3b2yq399ATlrUuiOZAMbKWcw/1GaU=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
|
||||
+64
-23
@@ -77,37 +77,78 @@ func (u *DatadogUnifi) batchUNASPools(r report, d *unifi.UNASDevice, s *unifi.UN
|
||||
_ = r.reportGauge(raidName("expected_protection"), rg.ExpectedProtection.Val, raidTags)
|
||||
_ = r.reportGauge(raidName("progress"), rg.Progress.Val, raidTags)
|
||||
}
|
||||
|
||||
if c := p.Cache; c != nil {
|
||||
u.batchUNASPoolCache(r, d, p.ID, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DatadogUnifi) batchUNASPoolCache(r report, d *unifi.UNASDevice, poolID string, c *unifi.UNASPoolCache) {
|
||||
metricName := metricNamespace("unas_pool_cache")
|
||||
|
||||
tags := tagMapToTags(cleanTags(map[string]string{
|
||||
"source": d.SourceName,
|
||||
"name": d.Name(),
|
||||
"pool_id": poolID,
|
||||
"mode": c.Mode,
|
||||
"status": c.Status,
|
||||
}))
|
||||
|
||||
_ = r.reportGauge(metricName("size"), c.Size.Val, tags)
|
||||
_ = r.reportGauge(metricName("used"), c.Composition.Used.Val, tags)
|
||||
_ = r.reportGauge(metricName("dirty"), c.Composition.Dirty.Val, tags)
|
||||
_ = r.reportGauge(metricName("read_hit_rate"), c.Composition.ReadHitRate.Val, tags)
|
||||
_ = r.reportGauge(metricName("write_hit_rate"), c.Composition.WriteHitRate.Val, tags)
|
||||
}
|
||||
|
||||
func (u *DatadogUnifi) batchUNASDisks(r report, d *unifi.UNASDevice, s *unifi.UNASStorage) {
|
||||
metricName := metricNamespace("unas_disk")
|
||||
|
||||
for _, disk := range s.Disks {
|
||||
tags := tagMapToTags(cleanTags(map[string]string{
|
||||
"source": d.SourceName,
|
||||
"name": d.Name(),
|
||||
"slot_id": disk.SlotID,
|
||||
"location": disk.Location,
|
||||
"pool_id": disk.PoolID,
|
||||
"disk_type": disk.Type,
|
||||
"state": disk.State,
|
||||
"model": disk.Model,
|
||||
"serial": disk.Serial,
|
||||
}))
|
||||
|
||||
_ = r.reportGauge(metricName("size"), disk.Size.Val, tags)
|
||||
_ = r.reportGauge(metricName("temperature"), disk.Temperature.Val, tags)
|
||||
_ = r.reportGauge(metricName("health_score"), disk.HealthScore.Val, tags)
|
||||
_ = r.reportGauge(metricName("power_on_hours"), disk.PowerOnHours.Val, tags)
|
||||
_ = r.reportGauge(metricName("rpm"), disk.RPM.Val, tags)
|
||||
_ = r.reportGauge(metricName("bad_sector_count"), disk.BadSectorCount.Val, tags)
|
||||
_ = r.reportGauge(metricName("uncorrectable_sector_count"), disk.UncorrectableSectorCount.Val, tags)
|
||||
_ = r.reportGauge(metricName("read_error_rate"), disk.ReadErrorRate.Val, tags)
|
||||
_ = r.reportGauge(metricName("smart_read_error_count"), disk.SmartReadErrorCount.Val, tags)
|
||||
_ = r.reportGauge(metricName("read_kbps"), disk.ReadKBPS.Val, tags)
|
||||
_ = r.reportGauge(metricName("write_kbps"), disk.WriteKBPS.Val, tags)
|
||||
u.batchUNASDisk(r, d, disk)
|
||||
}
|
||||
|
||||
// Cache bays hold SSDs with the same fields as pool disks; disk_type tells them apart.
|
||||
for _, disk := range s.CacheSlots {
|
||||
if disk.State == "empty" {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := u.batchUNASDisk(r, d, disk)
|
||||
_ = r.reportGauge(metricName("life_span"), disk.LifeSpan.Val, tags)
|
||||
}
|
||||
}
|
||||
|
||||
// batchUNASDisk reports one disk's gauges and returns its tags.
|
||||
func (u *DatadogUnifi) batchUNASDisk(r report, d *unifi.UNASDevice, disk unifi.UNASDisk) []string {
|
||||
metricName := metricNamespace("unas_disk")
|
||||
|
||||
tags := tagMapToTags(cleanTags(map[string]string{
|
||||
"source": d.SourceName,
|
||||
"name": d.Name(),
|
||||
"slot_id": disk.SlotID,
|
||||
"location": disk.Location,
|
||||
"pool_id": disk.PoolID,
|
||||
"disk_type": disk.Type,
|
||||
"state": disk.State,
|
||||
"model": disk.Model,
|
||||
"serial": disk.Serial,
|
||||
}))
|
||||
|
||||
_ = r.reportGauge(metricName("size"), disk.Size.Val, tags)
|
||||
_ = r.reportGauge(metricName("temperature"), disk.Temperature.Val, tags)
|
||||
_ = r.reportGauge(metricName("health_score"), disk.HealthScore.Val, tags)
|
||||
_ = r.reportGauge(metricName("power_on_hours"), disk.PowerOnHours.Val, tags)
|
||||
_ = r.reportGauge(metricName("rpm"), disk.RPM.Val, tags)
|
||||
_ = r.reportGauge(metricName("bad_sector_count"), disk.BadSectorCount.Val, tags)
|
||||
_ = r.reportGauge(metricName("uncorrectable_sector_count"), disk.UncorrectableSectorCount.Val, tags)
|
||||
_ = r.reportGauge(metricName("read_error_rate"), disk.ReadErrorRate.Val, tags)
|
||||
_ = r.reportGauge(metricName("smart_read_error_count"), disk.SmartReadErrorCount.Val, tags)
|
||||
_ = r.reportGauge(metricName("read_kbps"), disk.ReadKBPS.Val, tags)
|
||||
_ = r.reportGauge(metricName("write_kbps"), disk.WriteKBPS.Val, tags)
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func (u *DatadogUnifi) batchUNASShares(r report, d *unifi.UNASDevice) {
|
||||
|
||||
+65
-30
@@ -114,38 +114,73 @@ func (u *InfluxUnifi) batchUNASStorage(r report, d *unifi.UNASDevice, s *unifi.U
|
||||
},
|
||||
Fields: fields,
|
||||
})
|
||||
|
||||
if c := p.Cache; c != nil {
|
||||
r.send(&metric{
|
||||
Table: "unas_pool_cache",
|
||||
Tags: map[string]string{
|
||||
"source": d.SourceName,
|
||||
"name": d.Name(),
|
||||
"pool_id": p.ID,
|
||||
"mode": c.Mode,
|
||||
"status": c.Status,
|
||||
},
|
||||
Fields: map[string]any{
|
||||
"size": c.Size.Val,
|
||||
"used": c.Composition.Used.Val,
|
||||
"dirty": c.Composition.Dirty.Val,
|
||||
"read_hit_rate": c.Composition.ReadHitRate.Val,
|
||||
"write_hit_rate": c.Composition.WriteHitRate.Val,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, disk := range s.Disks {
|
||||
r.send(&metric{
|
||||
Table: "unas_disk",
|
||||
Tags: map[string]string{
|
||||
"source": d.SourceName,
|
||||
"name": d.Name(),
|
||||
"slot_id": disk.SlotID,
|
||||
"location": disk.Location,
|
||||
"pool_id": disk.PoolID,
|
||||
"disk_type": disk.Type,
|
||||
"state": disk.State,
|
||||
"model": disk.Model,
|
||||
"serial": disk.Serial,
|
||||
"firmware": disk.Firmware,
|
||||
},
|
||||
Fields: map[string]any{
|
||||
"size": disk.Size.Val,
|
||||
"temperature": disk.Temperature.Val,
|
||||
"health_score": disk.HealthScore.Val,
|
||||
"power_on_hours": disk.PowerOnHours.Val,
|
||||
"rpm": disk.RPM.Val,
|
||||
"bad_sector_count": disk.BadSectorCount.Val,
|
||||
"uncorrectable_sector_count": disk.UncorrectableSectorCount.Val,
|
||||
"read_error_rate": disk.ReadErrorRate.Val,
|
||||
"smart_read_error_count": disk.SmartReadErrorCount.Val,
|
||||
"read_kbps": disk.ReadKBPS.Val,
|
||||
"write_kbps": disk.WriteKBPS.Val,
|
||||
"is_global_hot_spare": disk.IsGlobalHotSpare.Val,
|
||||
"is_local_hot_spare": disk.IsLocalHotSpare.Val,
|
||||
},
|
||||
})
|
||||
r.send(unasDiskMetric(d, disk))
|
||||
}
|
||||
|
||||
// Cache bays hold SSDs with the same fields as pool disks; disk_type tells them apart.
|
||||
for _, disk := range s.CacheSlots {
|
||||
if disk.State == "empty" {
|
||||
continue
|
||||
}
|
||||
|
||||
m := unasDiskMetric(d, disk)
|
||||
m.Fields["life_span"] = disk.LifeSpan.Val
|
||||
r.send(m)
|
||||
}
|
||||
}
|
||||
|
||||
func unasDiskMetric(d *unifi.UNASDevice, disk unifi.UNASDisk) *metric {
|
||||
return &metric{
|
||||
Table: "unas_disk",
|
||||
Tags: map[string]string{
|
||||
"source": d.SourceName,
|
||||
"name": d.Name(),
|
||||
"slot_id": disk.SlotID,
|
||||
"location": disk.Location,
|
||||
"pool_id": disk.PoolID,
|
||||
"disk_type": disk.Type,
|
||||
"state": disk.State,
|
||||
"model": disk.Model,
|
||||
"serial": disk.Serial,
|
||||
"firmware": disk.Firmware,
|
||||
},
|
||||
Fields: map[string]any{
|
||||
"size": disk.Size.Val,
|
||||
"temperature": disk.Temperature.Val,
|
||||
"health_score": disk.HealthScore.Val,
|
||||
"power_on_hours": disk.PowerOnHours.Val,
|
||||
"rpm": disk.RPM.Val,
|
||||
"bad_sector_count": disk.BadSectorCount.Val,
|
||||
"uncorrectable_sector_count": disk.UncorrectableSectorCount.Val,
|
||||
"read_error_rate": disk.ReadErrorRate.Val,
|
||||
"smart_read_error_count": disk.SmartReadErrorCount.Val,
|
||||
"read_kbps": disk.ReadKBPS.Val,
|
||||
"write_kbps": disk.WriteKBPS.Val,
|
||||
"is_global_hot_spare": disk.IsGlobalHotSpare.Val,
|
||||
"is_local_hot_spare": disk.IsLocalHotSpare.Val,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -74,7 +74,7 @@ Data comes from the UniFi Drive API. Four endpoints are polled per cycle:
|
||||
| Endpoint | Yields |
|
||||
|---|---|
|
||||
| `/proxy/drive/api/v2/systems/device-info` | name, model, version, firmware, status, CPU load and temperature, memory |
|
||||
| `/proxy/drive/api/v2/storage` | pools (capacity, usage, status, RAID groups) and disks (health score, temperature, power-on hours, RPM, size, bad and uncorrectable sectors, read/write KB/s) |
|
||||
| `/proxy/drive/api/v2/storage` | pools (capacity, usage, status, RAID groups, SSD cache size, fill, dirty data, and hit rates), disks (health score, temperature, power-on hours, RPM, size, bad and uncorrectable sectors, read/write KB/s), and the SSDs in the cache bays (the same disk fields plus remaining endurance) |
|
||||
| `/proxy/users/drive/api/v2/drives` | per-share id, name, type, status, quota, usage, member count |
|
||||
| `/proxy/drive/api/v2/systems/network-io` | receive and transmit KB/s |
|
||||
|
||||
@@ -91,12 +91,18 @@ bare `unas_` prefix — the Grafana dashboard in
|
||||
[#785](https://github.com/unpoller/unpoller/issues/785) needs its queries adjusted
|
||||
accordingly. Consistency with every other unpoller metric wins here.
|
||||
|
||||
The SSD cache in front of a pool reports as `unifi_unas_pool_cache_*`. Cache-bay SSDs report
|
||||
as `unifi_unas_disk_*` with `disk_type` set to the SSD's type, plus
|
||||
`unifi_unas_disk_life_span_percent`. A cache bay and a drive bay can share a `slot_id`, so
|
||||
filter on `disk_type` to tell them apart.
|
||||
|
||||
### InfluxDB and DataDog
|
||||
|
||||
Four measurements / metric prefixes: `unas_device`, `unas_pool`, `unas_disk`, and
|
||||
`unas_share` (DataDog also emits `unas_raid_group`). They are kept separate because each has
|
||||
its own tag set; folding them together would give every point the union of those tags with
|
||||
most of them empty.
|
||||
Five measurements / metric prefixes: `unas_device`, `unas_pool`, `unas_pool_cache`,
|
||||
`unas_disk`, and `unas_share` (DataDog also emits `unas_raid_group`). Cache-bay SSDs report
|
||||
under `unas_disk` with `disk_type` set to the SSD's type and an extra `life_span` field. The
|
||||
measurements are kept separate because each has its own tag set; folding them together would
|
||||
give every point the union of those tags with most of them empty.
|
||||
|
||||
Loki and OpenTelemetry are not wired up: Loki carries events only, and a UNAS console exposes
|
||||
no event endpoints in this version.
|
||||
|
||||
+67
-16
@@ -27,6 +27,12 @@ type unasDevice struct {
|
||||
RaidCurrentProtection *prometheus.Desc
|
||||
RaidExpectedProtection *prometheus.Desc
|
||||
RaidProgress *prometheus.Desc
|
||||
// SSD caches in front of storage pools.
|
||||
PoolCacheSize *prometheus.Desc
|
||||
PoolCacheUsed *prometheus.Desc
|
||||
PoolCacheDirty *prometheus.Desc
|
||||
PoolCacheReadHitRate *prometheus.Desc
|
||||
PoolCacheWriteHitRate *prometheus.Desc
|
||||
// Physical disks.
|
||||
DiskSize *prometheus.Desc
|
||||
DiskTemperature *prometheus.Desc
|
||||
@@ -39,6 +45,7 @@ type unasDevice struct {
|
||||
DiskSmartReadErrorCount *prometheus.Desc
|
||||
DiskReadKBPS *prometheus.Desc
|
||||
DiskWriteKBPS *prometheus.Desc
|
||||
DiskLifeSpan *prometheus.Desc
|
||||
// Shares ("drives").
|
||||
ShareQuota *prometheus.Desc
|
||||
ShareUsage *prometheus.Desc
|
||||
@@ -51,6 +58,7 @@ func descUNASDevice(ns string) *unasDevice { // nolint: funlen
|
||||
console := []string{"source", "name"}
|
||||
pool := []string{"source", "name", "pool_id", "pool_type", "status"}
|
||||
raid := []string{"source", "name", "pool_id", "raid_group_id", "current_level", "config_level"}
|
||||
cache := []string{"source", "name", "pool_id", "mode", "status"}
|
||||
disk := []string{"source", "name", "slot_id", "pool_id", "disk_type", "state", "model", "serial"}
|
||||
share := []string{"source", "name", "share_id", "share_name", "share_type", "status"}
|
||||
|
||||
@@ -82,6 +90,16 @@ func descUNASDevice(ns string) *unasDevice { // nolint: funlen
|
||||
"UNAS RAID group expected redundancy", raid, nil),
|
||||
RaidProgress: prometheus.NewDesc(ns+"raid_group_progress_percent",
|
||||
"UNAS RAID group rebuild or expansion progress", raid, nil),
|
||||
PoolCacheSize: prometheus.NewDesc(ns+"pool_cache_size_bytes",
|
||||
"UNAS storage pool SSD cache size", cache, nil),
|
||||
PoolCacheUsed: prometheus.NewDesc(ns+"pool_cache_used_bytes",
|
||||
"UNAS storage pool SSD cache bytes holding cached data", cache, nil),
|
||||
PoolCacheDirty: prometheus.NewDesc(ns+"pool_cache_dirty_bytes",
|
||||
"UNAS storage pool SSD cache bytes not yet written to the pool's disks", cache, nil),
|
||||
PoolCacheReadHitRate: prometheus.NewDesc(ns+"pool_cache_read_hit_percent",
|
||||
"UNAS storage pool SSD cache read hit rate as reported by the console", cache, nil),
|
||||
PoolCacheWriteHitRate: prometheus.NewDesc(ns+"pool_cache_write_hit_percent",
|
||||
"UNAS storage pool SSD cache write hit rate as reported by the console", cache, nil),
|
||||
DiskSize: prometheus.NewDesc(ns+"disk_size_bytes",
|
||||
"UNAS disk size", disk, nil),
|
||||
DiskTemperature: prometheus.NewDesc(ns+"disk_temperature_celsius",
|
||||
@@ -104,6 +122,8 @@ func descUNASDevice(ns string) *unasDevice { // nolint: funlen
|
||||
"UNAS disk instantaneous read throughput in KB/s", disk, nil),
|
||||
DiskWriteKBPS: prometheus.NewDesc(ns+"disk_write_kbps",
|
||||
"UNAS disk instantaneous write throughput in KB/s", disk, nil),
|
||||
DiskLifeSpan: prometheus.NewDesc(ns+"disk_life_span_percent",
|
||||
"UNAS SSD rated endurance left", disk, nil),
|
||||
ShareQuota: prometheus.NewDesc(ns+"share_quota_bytes",
|
||||
"UNAS share quota (0 when unlimited)", share, nil),
|
||||
ShareUsage: prometheus.NewDesc(ns+"share_usage_bytes",
|
||||
@@ -184,26 +204,57 @@ func (u *promUnifi) exportUNASStorage(r report, d *unifi.UNASDevice, s *unifi.UN
|
||||
{u.UNASDevice.RaidProgress, gauge, rg.Progress, raid},
|
||||
})
|
||||
}
|
||||
|
||||
if c := p.Cache; c != nil {
|
||||
cache := []string{d.SourceName, name, p.ID, c.Mode, c.Status}
|
||||
|
||||
r.send([]*metric{
|
||||
{u.UNASDevice.PoolCacheSize, gauge, c.Size, cache},
|
||||
{u.UNASDevice.PoolCacheUsed, gauge, c.Composition.Used, cache},
|
||||
{u.UNASDevice.PoolCacheDirty, gauge, c.Composition.Dirty, cache},
|
||||
{u.UNASDevice.PoolCacheReadHitRate, gauge, c.Composition.ReadHitRate, cache},
|
||||
{u.UNASDevice.PoolCacheWriteHitRate, gauge, c.Composition.WriteHitRate, cache},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, disk := range s.Disks {
|
||||
labels := []string{
|
||||
d.SourceName, name, disk.SlotID, disk.PoolID,
|
||||
disk.Type, disk.State, disk.Model, disk.Serial,
|
||||
u.exportUNASDisk(r, d, disk)
|
||||
}
|
||||
|
||||
// Cache slots hold SSDs with the same fields as the pool's disks; the
|
||||
// disk_type label tells them apart.
|
||||
for _, disk := range s.CacheSlots {
|
||||
if disk.State == "empty" {
|
||||
continue
|
||||
}
|
||||
|
||||
r.send([]*metric{
|
||||
{u.UNASDevice.DiskSize, gauge, disk.Size, labels},
|
||||
{u.UNASDevice.DiskTemperature, gauge, disk.Temperature, labels},
|
||||
{u.UNASDevice.DiskHealthScore, gauge, disk.HealthScore, labels},
|
||||
{u.UNASDevice.DiskPowerOnHours, gauge, disk.PowerOnHours, labels},
|
||||
{u.UNASDevice.DiskRPM, gauge, disk.RPM, labels},
|
||||
{u.UNASDevice.DiskBadSectorCount, gauge, disk.BadSectorCount, labels},
|
||||
{u.UNASDevice.DiskUncorrectableSectorCount, gauge, disk.UncorrectableSectorCount, labels},
|
||||
{u.UNASDevice.DiskReadErrorRate, gauge, disk.ReadErrorRate, labels},
|
||||
{u.UNASDevice.DiskSmartReadErrorCount, gauge, disk.SmartReadErrorCount, labels},
|
||||
{u.UNASDevice.DiskReadKBPS, gauge, disk.ReadKBPS, labels},
|
||||
{u.UNASDevice.DiskWriteKBPS, gauge, disk.WriteKBPS, labels},
|
||||
})
|
||||
u.exportUNASDisk(r, d, disk)
|
||||
r.send([]*metric{{u.UNASDevice.DiskLifeSpan, gauge, disk.LifeSpan, unasDiskLabels(d, disk)}})
|
||||
}
|
||||
}
|
||||
|
||||
func unasDiskLabels(d *unifi.UNASDevice, disk unifi.UNASDisk) []string {
|
||||
return []string{
|
||||
d.SourceName, d.Name(), disk.SlotID, disk.PoolID,
|
||||
disk.Type, disk.State, disk.Model, disk.Serial,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *promUnifi) exportUNASDisk(r report, d *unifi.UNASDevice, disk unifi.UNASDisk) {
|
||||
labels := unasDiskLabels(d, disk)
|
||||
|
||||
r.send([]*metric{
|
||||
{u.UNASDevice.DiskSize, gauge, disk.Size, labels},
|
||||
{u.UNASDevice.DiskTemperature, gauge, disk.Temperature, labels},
|
||||
{u.UNASDevice.DiskHealthScore, gauge, disk.HealthScore, labels},
|
||||
{u.UNASDevice.DiskPowerOnHours, gauge, disk.PowerOnHours, labels},
|
||||
{u.UNASDevice.DiskRPM, gauge, disk.RPM, labels},
|
||||
{u.UNASDevice.DiskBadSectorCount, gauge, disk.BadSectorCount, labels},
|
||||
{u.UNASDevice.DiskUncorrectableSectorCount, gauge, disk.UncorrectableSectorCount, labels},
|
||||
{u.UNASDevice.DiskReadErrorRate, gauge, disk.ReadErrorRate, labels},
|
||||
{u.UNASDevice.DiskSmartReadErrorCount, gauge, disk.SmartReadErrorCount, labels},
|
||||
{u.UNASDevice.DiskReadKBPS, gauge, disk.ReadKBPS, labels},
|
||||
{u.UNASDevice.DiskWriteKBPS, gauge, disk.WriteKBPS, labels},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,3 +101,44 @@ func TestExportUNASDeviceTolerantOfNilSections(t *testing.T) {
|
||||
u.exportUNASDevice(empty, nil)
|
||||
assert.Empty(t, empty.sent)
|
||||
}
|
||||
|
||||
// The SSD cache reports on its own label set, and an occupied cache bay reports as a disk
|
||||
// with disk_type SSD plus its remaining endurance. An empty bay reports nothing.
|
||||
func TestExportUNASSSDCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := testUNASDevice()
|
||||
d.Storage.Pools[0].Cache = &unifi.UNASPoolCache{
|
||||
Status: "fullyOperational",
|
||||
Mode: "readOnly",
|
||||
Size: unifi.FlexInt{Val: 1000},
|
||||
}
|
||||
d.Storage.Pools[0].Cache.Composition.ReadHitRate = unifi.FlexInt{Val: 87}
|
||||
d.Storage.CacheSlots = []unifi.UNASDisk{
|
||||
{SlotID: "1", Type: "SSD", State: "healthy", Serial: "SSD-1", LifeSpan: unifi.FlexInt{Val: 97}},
|
||||
{SlotID: "2", State: "empty"},
|
||||
}
|
||||
|
||||
r := &fakeReport{}
|
||||
u := &promUnifi{UNASDevice: descUNASDevice("unifi_")}
|
||||
u.exportUNASDevice(r, d)
|
||||
|
||||
byDesc := map[*prometheus.Desc][]*metric{}
|
||||
for _, m := range r.sent {
|
||||
byDesc[m.Desc] = append(byDesc[m.Desc], m)
|
||||
}
|
||||
|
||||
hit := byDesc[u.UNASDevice.PoolCacheReadHitRate]
|
||||
require.Len(t, hit, 1)
|
||||
assert.Equal(t, unifi.FlexInt{Val: 87}, hit[0].Value)
|
||||
assert.Equal(t, []string{"https://unas.example", "UNAS-Pro", "pool-1", "readOnly", "fullyOperational"},
|
||||
hit[0].Labels)
|
||||
|
||||
life := byDesc[u.UNASDevice.DiskLifeSpan]
|
||||
require.Len(t, life, 1, "only the occupied bay reports endurance")
|
||||
assert.Equal(t, unifi.FlexInt{Val: 97}, life[0].Value)
|
||||
assert.Equal(t, "SSD", life[0].Labels[4])
|
||||
|
||||
// The pool's hard drive and the occupied bay each report temperature; the empty bay does not.
|
||||
assert.Len(t, byDesc[u.UNASDevice.DiskTemperature], 2)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user