master
go 146 lines 4.89 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package nvme
4
5 import (
6 "errors"
7 "fmt"
8 "path/filepath"
9 "strconv"
10 "strings"
11 "time"
12 "unicode"
13
14 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
15 )
16
17 func (c *Collector) collect() (map[string]int64, error) {
18 if c.exec == nil {
19 return nil, errors.New("nvme-cli is not initialized (nil)")
20 }
21
22 now := time.Now()
23 if c.forceListDevices || now.Sub(c.listDevicesTime) > c.listDevicesEvery {
24 c.forceListDevices = false
25 c.listDevicesTime = now
26 if err := c.listNVMeDevices(); err != nil {
27 return nil, err
28 }
29 }
30
31 mx := make(map[string]int64)
32
33 for path := range c.devicePaths {
34 if err := c.collectNVMeDevice(mx, path); err != nil {
35 c.Error(err)
36 c.forceListDevices = true
37 continue
38 }
39 }
40
41 return mx, nil
42 }
43
44 func (c *Collector) collectNVMeDevice(mx map[string]int64, devicePath string) error {
45 stats, err := c.exec.smartLog(devicePath)
46 if err != nil {
47 return fmt.Errorf("exec nvme smart-log for '%s': %v", devicePath, err)
48 }
49
50 dev := extractDeviceFromPath(devicePath)
51
52 mx["device_"+dev+"_temperature"] = int64(float64(parseValue(stats.Temperature)) - 273.15) // Kelvin => Celsius
53 mx["device_"+dev+"_percentage_used"] = parseValue(stats.PercentUsed)
54 mx["device_"+dev+"_available_spare"] = parseValue(stats.AvailSpare)
55 mx["device_"+dev+"_data_units_read"] = parseValue(stats.DataUnitsRead) * 1000 * 512 // units => bytes
56 mx["device_"+dev+"_data_units_written"] = parseValue(stats.DataUnitsWritten) * 1000 * 512 // units => bytes
57 mx["device_"+dev+"_host_read_commands"] = parseValue(stats.HostReadCommands)
58 mx["device_"+dev+"_host_write_commands"] = parseValue(stats.HostWriteCommands)
59 mx["device_"+dev+"_power_cycles"] = parseValue(stats.PowerCycles)
60 mx["device_"+dev+"_power_on_time"] = parseValue(stats.PowerOnHours) * 3600 // hours => seconds
61 mx["device_"+dev+"_unsafe_shutdowns"] = parseValue(stats.UnsafeShutdowns)
62 mx["device_"+dev+"_media_errors"] = parseValue(stats.MediaErrors)
63 mx["device_"+dev+"_num_err_log_entries"] = parseValue(stats.NumErrLogEntries)
64 mx["device_"+dev+"_controller_busy_time"] = parseValue(stats.ControllerBusyTime) * 60 // minutes => seconds
65 mx["device_"+dev+"_warning_temp_time"] = parseValue(stats.WarningTempTime) * 60 // minutes => seconds
66 mx["device_"+dev+"_critical_comp_time"] = parseValue(stats.CriticalCompTime) * 60 // minutes => seconds
67 mx["device_"+dev+"_thm_temp1_trans_count"] = parseValue(stats.ThmTemp1TransCount)
68 mx["device_"+dev+"_thm_temp2_trans_count"] = parseValue(stats.ThmTemp2TransCount)
69 mx["device_"+dev+"_thm_temp1_total_time"] = parseValue(stats.ThmTemp1TotalTime) // seconds
70 mx["device_"+dev+"_thm_temp2_total_time"] = parseValue(stats.ThmTemp2TotalTime) // seconds
71
72 mx["device_"+dev+"_critical_warning_available_spare"] = oldmetrix.Bool(parseValue(stats.CriticalWarningValue)&1 != 0)
73 mx["device_"+dev+"_critical_warning_temp_threshold"] = oldmetrix.Bool(parseValue(stats.CriticalWarningValue)&(1<<1) != 0)
74 mx["device_"+dev+"_critical_warning_nvm_subsystem_reliability"] = oldmetrix.Bool(parseValue(stats.CriticalWarningValue)&(1<<2) != 0)
75 mx["device_"+dev+"_critical_warning_read_only"] = oldmetrix.Bool(parseValue(stats.CriticalWarningValue)&(1<<3) != 0)
76 mx["device_"+dev+"_critical_warning_volatile_mem_backup_failed"] = oldmetrix.Bool(parseValue(stats.CriticalWarningValue)&(1<<4) != 0)
77 mx["device_"+dev+"_critical_warning_persistent_memory_read_only"] = oldmetrix.Bool(parseValue(stats.CriticalWarningValue)&(1<<5) != 0)
78
79 return nil
80 }
81
82 func (c *Collector) listNVMeDevices() error {
83 devList, err := c.exec.list()
84 if err != nil {
85 return fmt.Errorf("exec nvme list: %v", err)
86 }
87
88 c.Debugf("found %d NVMe devices (%v)", len(devList.Devices), devList.Devices)
89
90 seen := make(map[string]bool)
91
92 for _, dev := range devList.Devices {
93 path := extractControllerPathFromDevicePath(dev.DevicePath)
94 if path == "" {
95 continue
96 }
97
98 seen[path] = true
99 if !c.devicePaths[path] {
100 c.devicePaths[path] = true
101 c.addDeviceCharts(path, dev.ModelNumber)
102 }
103 }
104
105 for path := range c.devicePaths {
106 if !seen[path] {
107 delete(c.devicePaths, path)
108 c.removeDeviceCharts(path)
109 }
110 }
111
112 return nil
113 }
114
115 func extractControllerPathFromDevicePath(devicePath string) string {
116 if !strings.Contains(devicePath, "nvme") {
117 return ""
118 }
119
120 // "/dev/nvme0n1" -> "/dev/nvme0"
121 // "/dev/nvme10n1" -> "/dev/nvme10"
122
123 // Find where "nX" (namespace part) starts
124 idx := strings.LastIndex(devicePath, "n")
125 if idx <= 0 || idx+1 >= len(devicePath) {
126 return devicePath
127 }
128
129 // Check if character before and after 'n' is a digit (indicating namespace)
130 before, after := devicePath[idx-1], devicePath[idx+1]
131 if !unicode.IsDigit(rune(before)) || !unicode.IsDigit(rune(after)) {
132 return devicePath
133 }
134
135 return devicePath[:idx]
136 }
137
138 func extractDeviceFromPath(devicePath string) string {
139 _, name := filepath.Split(devicePath)
140 return name
141 }
142
143 func parseValue(s nvmeNumber) int64 {
144 v, _ := strconv.ParseFloat(string(s), 64)
145 return int64(v)
146 }