| 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 | } |