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 }