drive-health-metrics/schema.go
James Coleman ddafa90a02
Some checks failed
Go package / build (push) Has been cancelled
first commit
2026-06-22 17:16:34 -05:00

158 lines
6.4 KiB
Go

package main
import "strconv"
// colKind is a column's value type. It drives formatting (CSV), typing (InfluxDB
// "i" suffix / JSON number / bool), and the tag-vs-field and label-vs-gauge
// splits the outputs derive from the table.
type colKind int
const (
kindStr colKind = iota
kindInt
kindFloat
kindBool
)
// column is the single source of truth for one output column: its name, value
// type, where it appears, and how to read it from a Drive. Every output (CSV,
// InfluxDB line protocol/JSON, Prometheus) is driven by this one table, so a new
// column is added in exactly one place.
//
// The output partitions are derived, not duplicated:
// - InfluxDB tag : kindStr column with influxTag set.
// - InfluxDB field : any non-csvOnly column that is not a tag.
// - Prometheus label : every non-csvOnly kindStr column.
// - Prometheus gauge : every non-csvOnly numeric (non-kindStr) column.
// - csvOnly : present in CSV only (timestamp, risk_reasons).
type column struct {
name string
kind colKind
influxTag bool
csvOnly bool
// value reads the column from d. It returns the typed Go value (string,
// *int, int, *float64, float64, or bool); a nil pointer means "unknown" and
// renders blank / is skipped by the metric outputs.
value func(d *Drive) any
}
// columns is the ordered output schema. CSV emits these in this order; the
// metric outputs sort their own tag/field sets independently.
var columns = []column{
{name: "collected_at", kind: kindStr, csvOnly: true, value: func(d *Drive) any { return d.CollectedAt }},
{name: "hostname", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Hostname }},
{name: "device_path", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.DevicePath }},
{name: "dtype", kind: kindStr, value: func(d *Drive) any { return d.Dtype }},
{name: "enclosure_slot", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.enclosureSlot() }},
{name: "device_id", kind: kindStr, value: func(d *Drive) any { return d.DeviceID }},
{name: "serial", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Serial }},
{name: "model", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Model }},
{name: "firmware", kind: kindStr, value: func(d *Drive) any { return d.Firmware }},
{name: "capacity", kind: kindStr, value: func(d *Drive) any { return d.Capacity }},
{name: "rotation", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Rotation }},
{name: "smart_health", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.SmartHealth }},
{name: "defect_total", kind: kindInt, value: func(d *Drive) any { return d.DefectTotal }},
{name: "udma_crc_errors", kind: kindInt, value: func(d *Drive) any { return d.UdmaCrc }},
{name: "media_errors_ctrl", kind: kindInt, value: func(d *Drive) any { return d.MediaErrCtrl }},
{name: "other_errors_ctrl", kind: kindInt, value: func(d *Drive) any { return d.OtherErrCtrl }},
{name: "predictive_failure_ctrl", kind: kindInt, value: func(d *Drive) any { return d.PredictiveFailureCtrl }},
{name: "smart_alert_ctrl", kind: kindBool, value: func(d *Drive) any { return d.SmartAlertCtrl }},
{name: "fw_state", kind: kindStr, value: func(d *Drive) any { return d.FwState }},
{name: "wear_pct_consumed", kind: kindInt, value: func(d *Drive) any { return d.WearPctConsumed }},
{name: "wear_src", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.WearSrc }},
{name: "unused_reserve_pct", kind: kindInt, value: func(d *Drive) any { return d.UnusedReservePct }},
{name: "host_written_tb", kind: kindFloat, value: func(d *Drive) any { return d.HostWrittenTB }},
{name: "nvme_critical_warning", kind: kindInt, value: func(d *Drive) any { return d.NvmeCriticalWarning }},
{name: "nvme_avail_spare", kind: kindInt, value: func(d *Drive) any { return d.NvmeAvailSpare }},
{name: "nvme_avail_spare_thresh", kind: kindInt, value: func(d *Drive) any { return d.NvmeAvailSpareThresh }},
{name: "power_on_years", kind: kindFloat, value: func(d *Drive) any { return d.PowerOnYears }},
{name: "power_cycle_count", kind: kindInt, value: func(d *Drive) any { return d.PowerCycleCount }},
{name: "temp_c", kind: kindInt, value: func(d *Drive) any { return d.TempC }},
{name: "risk_score", kind: kindInt, value: func(d *Drive) any { return d.RiskScore }},
{name: "recommendation", kind: kindStr, influxTag: true, value: func(d *Drive) any { return d.Recommendation }},
{name: "risk_reasons", kind: kindStr, csvOnly: true, value: func(d *Drive) any { return d.RiskReasons }},
}
// raw returns the column's typed value for d, or nil when the value is unknown
// (a nil pointer) or a blank string. Callers that need a presence test treat a
// nil return as "absent".
func (c column) raw(d *Drive) any {
switch t := c.value(d).(type) {
case nil:
return nil
case *int:
if t == nil {
return nil
}
return *t
case *float64:
if t == nil {
return nil
}
return *t
case string:
if t == "" {
return nil
}
return t
default:
return t // int, float64, bool — always present.
}
}
// format renders a raw value as its display string ("" for an absent value),
// matching the CSV/line-protocol textual form.
func format(v any) string {
switch t := v.(type) {
case nil:
return ""
case string:
return t
case int:
return strconv.Itoa(t)
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case bool:
if t {
return "true"
}
return "false"
default:
return ""
}
}
// field returns the column's display string for d, or "" when unknown. It is the
// canonical text form shared by the CSV output and the test helpers.
func (d *Drive) field(name string) string {
for _, c := range columns {
if c.name == name {
return format(c.raw(d))
}
}
return ""
}
// labelColumns returns the string columns carried as Prometheus labels and (the
// influxTag subset) as InfluxDB tags — every non-csvOnly kindStr column.
func labelColumns() []column {
var out []column
for _, c := range columns {
if c.kind == kindStr && !c.csvOnly {
out = append(out, c)
}
}
return out
}
// gaugeColumns returns the numeric columns emitted as Prometheus gauges and
// InfluxDB fields — every non-csvOnly column that is not a string.
func gaugeColumns() []column {
var out []column
for _, c := range columns {
if c.kind != kindStr && !c.csvOnly {
out = append(out, c)
}
}
return out
}