158 lines
6.4 KiB
Go
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
|
|
}
|