master
go 379 lines 11.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package smartctl
4
5 import (
6 "fmt"
7 "strings"
8
9 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
10 )
11
12 const (
13 prioDeviceSmartStatus = collectorapi.Priority + iota
14 prioDeviceAtaSmartErrorLogCount
15 prioDevicePowerOnTime
16 prioDeviceTemperature
17 prioDevicePowerCycleCount
18
19 prioDeviceScsiReadErrors
20 prioDeviceScsiWriteErrors
21 prioDeviceScsiVerifyErrors
22
23 prioDeviceSmartAttributeDecoded
24 prioDeviceSmartAttributeNormalized
25 )
26
27 var deviceChartsTmpl = collectorapi.Charts{
28 devicePowerOnTimeChartTmpl.Copy(),
29 deviceTemperatureChartTmpl.Copy(),
30 devicePowerCycleCountChartTmpl.Copy(),
31 deviceSmartStatusChartTmpl.Copy(),
32 deviceAtaSmartErrorLogCountChartTmpl.Copy(),
33 }
34
35 var (
36 deviceSmartStatusChartTmpl = collectorapi.Chart{
37 ID: "device_%s_type_%s_smart_status",
38 Title: "Device smart status",
39 Units: "status",
40 Fam: "smart status",
41 Ctx: "smartctl.device_smart_status",
42 Type: collectorapi.Line,
43 Priority: prioDeviceSmartStatus,
44 Dims: collectorapi.Dims{
45 {ID: "device_%s_type_%s_smart_status_passed", Name: "passed"},
46 {ID: "device_%s_type_%s_smart_status_failed", Name: "failed"},
47 },
48 }
49 deviceAtaSmartErrorLogCountChartTmpl = collectorapi.Chart{
50 ID: "device_%s_type_%s_ata_smart_error_log_count",
51 Title: "Device ATA smart error log count",
52 Units: "logs",
53 Fam: "smart error log",
54 Ctx: "smartctl.device_ata_smart_error_log_count",
55 Type: collectorapi.Line,
56 Priority: prioDeviceAtaSmartErrorLogCount,
57 Dims: collectorapi.Dims{
58 {ID: "device_%s_type_%s_ata_smart_error_log_summary_count", Name: "error_log"},
59 },
60 }
61 devicePowerOnTimeChartTmpl = collectorapi.Chart{
62 ID: "device_%s_type_%s_power_on_time",
63 Title: "Device power on time",
64 Units: "seconds",
65 Fam: "power on time",
66 Ctx: "smartctl.device_power_on_time",
67 Type: collectorapi.Line,
68 Priority: prioDevicePowerOnTime,
69 Dims: collectorapi.Dims{
70 {ID: "device_%s_type_%s_power_on_time", Name: "power_on_time"},
71 },
72 }
73 deviceTemperatureChartTmpl = collectorapi.Chart{
74 ID: "device_%s_type_%s_temperature",
75 Title: "Device temperature",
76 Units: "Celsius",
77 Fam: "temperature",
78 Ctx: "smartctl.device_temperature",
79 Type: collectorapi.Line,
80 Priority: prioDeviceTemperature,
81 Dims: collectorapi.Dims{
82 {ID: "device_%s_type_%s_temperature", Name: "temperature"},
83 },
84 }
85 devicePowerCycleCountChartTmpl = collectorapi.Chart{
86 ID: "device_%s_type_%s_power_cycle_count",
87 Title: "Device power cycles",
88 Units: "cycles",
89 Fam: "power cycles",
90 Ctx: "smartctl.device_power_cycles_count",
91 Type: collectorapi.Line,
92 Priority: prioDevicePowerCycleCount,
93 Dims: collectorapi.Dims{
94 {ID: "device_%s_type_%s_power_cycle_count", Name: "power"},
95 },
96 }
97 )
98
99 var deviceScsiErrorLogChartsTmpl = collectorapi.Charts{
100 deviceScsiReadErrorsChartTmpl.Copy(),
101 deviceScsiWriteErrorsChartTmpl.Copy(),
102 deviceScsiVerifyErrorsChartTmpl.Copy(),
103 }
104
105 var (
106 deviceScsiReadErrorsChartTmpl = collectorapi.Chart{
107 ID: "device_%s_type_%s_read_errors_rate",
108 Title: "Device read errors",
109 Units: "errors/s",
110 Fam: "scsi errors",
111 Ctx: "smartctl.device_read_errors_rate",
112 Type: collectorapi.Line,
113 Priority: prioDeviceScsiReadErrors,
114 Dims: collectorapi.Dims{
115 {ID: "device_%s_type_%s_scsi_error_log_read_total_errors_corrected", Name: "corrected", Algo: collectorapi.Incremental},
116 {ID: "device_%s_type_%s_scsi_error_log_read_total_uncorrected_errors", Name: "uncorrected", Algo: collectorapi.Incremental},
117 },
118 }
119 deviceScsiWriteErrorsChartTmpl = collectorapi.Chart{
120 ID: "device_%s_type_%s_write_errors_rate",
121 Title: "Device write errors",
122 Units: "errors/s",
123 Fam: "scsi errors",
124 Ctx: "smartctl.device_write_errors_rate",
125 Type: collectorapi.Line,
126 Priority: prioDeviceScsiWriteErrors,
127 Dims: collectorapi.Dims{
128 {ID: "device_%s_type_%s_scsi_error_log_write_total_errors_corrected", Name: "corrected", Algo: collectorapi.Incremental},
129 {ID: "device_%s_type_%s_scsi_error_log_write_total_uncorrected_errors", Name: "uncorrected", Algo: collectorapi.Incremental},
130 },
131 }
132 deviceScsiVerifyErrorsChartTmpl = collectorapi.Chart{
133 ID: "device_%s_type_%s_verify_errors_rate",
134 Title: "Device verify errors",
135 Units: "errors/s",
136 Fam: "scsi errors",
137 Ctx: "smartctl.device_verify_errors_rate",
138 Type: collectorapi.Line,
139 Priority: prioDeviceScsiVerifyErrors,
140 Dims: collectorapi.Dims{
141 {ID: "device_%s_type_%s_scsi_error_log_verify_total_errors_corrected", Name: "corrected", Algo: collectorapi.Incremental},
142 {ID: "device_%s_type_%s_scsi_error_log_verify_total_uncorrected_errors", Name: "uncorrected", Algo: collectorapi.Incremental},
143 },
144 }
145 )
146
147 var (
148 deviceSmartAttributeDecodedChartTmpl = collectorapi.Chart{
149 ID: "device_%s_type_%s_smart_attr_%s",
150 Title: "Device smart attribute %s",
151 Units: "value",
152 Fam: "attr %s",
153 Ctx: "smartctl.device_smart_attr_%s",
154 Type: collectorapi.Line,
155 Priority: prioDeviceSmartAttributeDecoded,
156 Dims: collectorapi.Dims{
157 {ID: "device_%s_type_%s_attr_%s_decoded", Name: "%s"},
158 },
159 }
160 deviceSmartAttributeNormalizedChartTmpl = collectorapi.Chart{
161 ID: "device_%s_type_%s_smart_attr_%s_normalized",
162 Title: "Device smart attribute normalized %s",
163 Units: "value",
164 Fam: "attr %s",
165 Ctx: "smartctl.device_smart_attr_%s_normalized",
166 Type: collectorapi.Line,
167 Priority: prioDeviceSmartAttributeNormalized,
168 Dims: collectorapi.Dims{
169 {ID: "device_%s_type_%s_attr_%s_normalized", Name: "%s"},
170 },
171 }
172 )
173
174 func (c *Collector) addDeviceCharts(dev *smartDevice) {
175 charts := collectorapi.Charts{}
176
177 if cs := c.newDeviceCharts(dev); cs != nil && len(*cs) > 0 {
178 if err := charts.Add(*cs...); err != nil {
179 c.Warning(err)
180 }
181 }
182 if cs := c.newDeviceSmartAttrCharts(dev); cs != nil && len(*cs) > 0 {
183 if err := charts.Add(*cs...); err != nil {
184 c.Warning(err)
185 }
186 }
187 if cs := c.newDeviceScsiErrorLogCharts(dev); cs != nil && len(*cs) > 0 {
188 if err := charts.Add(*cs...); err != nil {
189 c.Warning(err)
190 }
191 }
192
193 if err := c.Charts().Add(charts...); err != nil {
194 c.Warning(err)
195 }
196 }
197
198 func (c *Collector) removeDeviceCharts(scanDev *scanDevice) {
199 px := fmt.Sprintf("device_%s_%s_", scanDev.shortName(), scanDev.typ)
200
201 for _, chart := range *c.Charts() {
202 if strings.HasPrefix(chart.ID, px) {
203 chart.MarkRemove()
204 chart.MarkNotCreated()
205 }
206 }
207 }
208
209 func (c *Collector) newDeviceCharts(dev *smartDevice) *collectorapi.Charts {
210
211 charts := deviceChartsTmpl.Copy()
212
213 if _, ok := dev.powerOnTime(); !ok {
214 _ = charts.Remove(devicePowerOnTimeChartTmpl.ID)
215 }
216 if _, ok := dev.temperature(); !ok {
217 _ = charts.Remove(deviceTemperatureChartTmpl.ID)
218 }
219 if _, ok := dev.powerCycleCount(); !ok {
220 _ = charts.Remove(devicePowerCycleCountChartTmpl.ID)
221 }
222 if _, ok := dev.smartStatusPassed(); !ok {
223 _ = charts.Remove(deviceSmartStatusChartTmpl.ID)
224 }
225 if _, ok := dev.ataSmartErrorLogCount(); !ok {
226 _ = charts.Remove(deviceAtaSmartErrorLogCountChartTmpl.ID)
227 }
228
229 for _, chart := range *charts {
230 chart.ID = fmt.Sprintf(chart.ID, dev.deviceName(), dev.deviceType())
231 chart.Labels = []collectorapi.Label{
232 {Key: "device_name", Value: dev.deviceName()},
233 {Key: "device_type", Value: dev.deviceType()},
234 {Key: "model_name", Value: dev.modelName()},
235 {Key: "serial_number", Value: dev.serialNumber()},
236 }
237 for _, dim := range chart.Dims {
238 dim.ID = fmt.Sprintf(dim.ID, dev.deviceName(), dev.deviceType())
239 }
240 }
241
242 return charts
243 }
244
245 func (c *Collector) newDeviceSmartAttrCharts(dev *smartDevice) *collectorapi.Charts {
246 attrs, ok := dev.ataSmartAttributeTable()
247 if !ok {
248 return nil
249 }
250 charts := collectorapi.Charts{}
251
252 for _, attr := range attrs {
253 if !isSmartAttrValid(attr) ||
254 strings.HasPrefix(attr.name(), "Unknown") ||
255 strings.HasPrefix(attr.name(), "Not_In_Use") {
256 continue
257 }
258
259 cs := collectorapi.Charts{
260 deviceSmartAttributeDecodedChartTmpl.Copy(),
261 deviceSmartAttributeNormalizedChartTmpl.Copy(),
262 }
263
264 attrName := attributeNameMap(attr.name())
265 cleanAttrName := cleanAttributeName(attrName)
266
267 for _, chart := range cs {
268 if chart.ID == deviceSmartAttributeDecodedChartTmpl.ID {
269 chart.Units = attributeUnit(attrName)
270 }
271 chart.ID = fmt.Sprintf(chart.ID, dev.deviceName(), dev.deviceType(), cleanAttrName)
272 chart.Title = fmt.Sprintf(chart.Title, attrName)
273 chart.Fam = fmt.Sprintf(chart.Fam, cleanAttrName)
274 chart.Ctx = fmt.Sprintf(chart.Ctx, cleanAttrName)
275 chart.Labels = []collectorapi.Label{
276 {Key: "device_name", Value: dev.deviceName()},
277 {Key: "device_type", Value: dev.deviceType()},
278 {Key: "model_name", Value: dev.modelName()},
279 {Key: "serial_number", Value: dev.serialNumber()},
280 }
281 for _, dim := range chart.Dims {
282 dim.ID = fmt.Sprintf(dim.ID, dev.deviceName(), dev.deviceType(), cleanAttrName)
283 dim.Name = fmt.Sprintf(dim.Name, cleanAttrName)
284 }
285 }
286
287 if err := charts.Add(cs...); err != nil {
288 c.Warning(err)
289 }
290 }
291
292 return &charts
293 }
294
295 func (c *Collector) newDeviceScsiErrorLogCharts(dev *smartDevice) *collectorapi.Charts {
296 if dev.deviceType() != "scsi" || !dev.data.Get("scsi_error_counter_log").Exists() {
297 return nil
298 }
299
300 charts := deviceScsiErrorLogChartsTmpl.Copy()
301
302 for _, chart := range *charts {
303 chart.ID = fmt.Sprintf(chart.ID, dev.deviceName(), dev.deviceType())
304 chart.Labels = []collectorapi.Label{
305 {Key: "device_name", Value: dev.deviceName()},
306 {Key: "device_type", Value: dev.deviceType()},
307 {Key: "model_name", Value: dev.modelName()},
308 {Key: "serial_number", Value: dev.serialNumber()},
309 }
310 for _, dim := range chart.Dims {
311 dim.ID = fmt.Sprintf(dim.ID, dev.deviceName(), dev.deviceType())
312 }
313 }
314
315 return charts
316 }
317
318 var attrNameReplacer = strings.NewReplacer(" ", "_", "/", "_")
319
320 func cleanAttributeName(attrName string) string {
321 return strings.ToLower(attrNameReplacer.Replace(attrName))
322 }
323
324 func attributeUnit(attrName string) string {
325 units := map[string]string{
326 "Airflow_Temperature_Cel": "Celsius",
327 "Case_Temperature": "Celsius",
328 "Drive_Temperature": "Celsius",
329 "Temperature_Case": "Celsius",
330 "Temperature_Celsius": "Celsius",
331 "Temperature_Internal": "Celsius",
332 "Power_On_Hours": "hours",
333 "Spin_Up_Time": "milliseconds",
334 "Media_Wearout_Indicator": "percent",
335 "Percent_Life_Remaining": "percent",
336 "Percent_Lifetime_Remain": "percent",
337 "Total_LBAs_Read": "sectors",
338 "Total_LBAs_Written": "sectors",
339 "Offline_Uncorrectable": "sectors",
340 "Pending_Sector_Count": "sectors",
341 "Reallocated_Sector_Ct": "sectors",
342 "Current_Pending_Sector": "sectors",
343 "Reported_Uncorrect": "errors",
344 "Command_Timeout": "events",
345 }
346
347 if unit, ok := units[attrName]; ok {
348 return unit
349 }
350
351 // TODO: convert to bytes during data collection? (examples: NAND_Writes_32MiB, Flash_Writes_GiB)
352 if strings.HasSuffix(attrName, "MiB") || strings.HasSuffix(attrName, "GiB") {
353 if strings.Contains(attrName, "Writes") {
354 return "writes"
355 }
356 if strings.Contains(attrName, "Reads") {
357 return "reads"
358 }
359 }
360
361 if strings.Contains(attrName, "Error") {
362 return "errors"
363 }
364
365 for _, s := range []string{"_Count", "_Cnt", "_Ct"} {
366 if strings.HasSuffix(attrName, s) {
367 return "events"
368 }
369 }
370
371 return "value"
372 }
373
374 func attributeNameMap(attrName string) string {
375 // TODO: Handle Vendor-Specific S.M.A.R.T. Attribute Naming
376 // S.M.A.R.T. attribute names can vary slightly between vendors (e.g., "Thermal_Throttle_St" vs. "Thermal_Throttle_Status").
377 // This function ensures consistent naming.
378 return attrName
379 }