From 852ff184681c4f355a8e4a5989a0cfeae7e2abed Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 16:53:54 +0200 Subject: [PATCH 01/11] Add LenovoDensePower collector --- collectors/collectorManager.go | 1 + collectors/lenovoDensePower.go | 155 +++++++++++++++++++++++++++++++++ collectors/lenovoDensePower.md | 47 ++++++++++ 3 files changed, 203 insertions(+) create mode 100644 collectors/lenovoDensePower.go create mode 100644 collectors/lenovoDensePower.md diff --git a/collectors/collectorManager.go b/collectors/collectorManager.go index 679bdd2..1cff613 100644 --- a/collectors/collectorManager.go +++ b/collectors/collectorManager.go @@ -50,6 +50,7 @@ var AvailableCollectors = map[string]MetricCollector{ "nfsiostat": new(NfsIOStatCollector), "slurm_cgroup": new(SlurmCgroupCollector), "smartmon": new(SmartMonCollector), + "lenovo_dense_power": new(LenovoDensePowerCollector), } // Metric collector manager data structure diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go new file mode 100644 index 0000000..4954f9e --- /dev/null +++ b/collectors/lenovoDensePower.go @@ -0,0 +1,155 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// additional authors: +// Michael Panzlaff (NHR@FAU) + +package collectors + +import ( + "bytes" + "encoding/json" + "encoding/hex" + "fmt" + "os/exec" + "strconv" + "strings" + "time" + + cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger" + lp "github.com/ClusterCockpit/cc-lib/v2/ccMessage" +) + +type LenovoDensePowerCollector struct { + metricCollector + + config struct { + IpmitoolPath string `json:"ipmitool_path"` + Sudo bool `json:"use_sudo"` + } + + ipmitool string + + energyValLast float64 + energyTimeLast time.Time +} + +func (m *LenovoDensePowerCollector) Init(config json.RawMessage) error { + // Check if already initialized + if m.init { + return nil + } + + m.name = "LenovoDensePower" + if err := m.setup(); err != nil { + return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err) + } + m.meta = map[string]string{ + "source": m.name, + "group": "IPMI", + } + + m.config.IpmitoolPath = "ipmitool" + + if len(config) > 0 { + d := json.NewDecoder(bytes.NewReader(config)) + d.DisallowUnknownFields() + if err := d.Decode(&m.config); err != nil { + return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err) + } + } + + m.ipmitool = m.config.IpmitoolPath + + energyVal, energyTime, err := m.readEnergy() + if err != nil { + return fmt.Errorf("Energy reading test failed: %w", err) + } + + m.energyValLast = energyVal + m.energyTimeLast = energyTime + + return nil +} + +func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { + argv := make([]string, 0) + if m.config.Sudo { + argv = append(argv, "sudo", "-n") + } + + lenovoRequestEnergyMsg := []uint8{0x3a, 0x32, 4, 2, 0, 0, 0} + + argv = append(argv, m.ipmitool, "raw") + + for _, val := range lenovoRequestEnergyMsg { + argv = append(argv, strconv.Itoa(int(val))) + } + + cmd := exec.Command(argv[0], argv[1:]...) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return 0, time.Unix(0, 0), fmt.Errorf("Failed to run ipmitool: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) + } + + data, err := hex.DecodeString(strings.ReplaceAll(stdout.String(), " ", "")) + if err != nil { + return 0, time.Unix(0, 0), fmt.Errorf("Unable to decode ipmitool hex output: %w (stdout=%s)", err, stdout.String()) + } + + if len(data) != 14 { + return 0, time.Unix(0, 0), fmt.Errorf("Result must be 14 bytes as specified in the documentation") + } + + wholeJoules := (uint32(data[2]) << 0) | (uint32(data[3]) << 8) | (uint32(data[4]) << 16) | (uint32(data[5]) << 24) + milliJoules := uint16(data[6]) | (uint16(data[7]) << 8) + finalJoules := float64(wholeJoules) + float64(milliJoules) * 1e-3 + + wholeSeconds := (uint32(data[8]) << 0) | (uint32(data[9]) << 8) | (uint32(data[10]) << 16) | (uint32(data[11]) << 24) + milliSeconds := uint16(data[12]) | (uint16(data[13]) << 8) + finalTime := time.Unix(int64(wholeSeconds), (int64(milliSeconds) * 1000000)) + + return finalJoules, finalTime, nil +} + +func (m *LenovoDensePowerCollector) Read(interval time.Duration, output chan lp.CCMessage) { + // Check if already initialized + if !m.init { + return + } + + energyVal, energyTime, err := m.readEnergy() + if err != nil { + cclog.ComponentErrorf(m.name, "readEnergy failed: %v", err) + return + } + + powerVal := 0.0 + if energyTime.After(m.energyTimeLast) { + energyValDiff := energyVal - m.energyValLast + energyTimeDiff := energyTime.Sub(m.energyTimeLast) + powerVal = energyValDiff / energyTimeDiff.Seconds() + + m.energyValLast = energyVal + m.energyTimeLast = energyTime + } + + metric, err := lp.NewMetric("node_power", map[string]string{"type": "node"}, m.meta, powerVal, energyTime) + if err != nil { + cclog.ComponentErrorf(m.name, "lp.NewMetric failed: %v", err) + return + } + metric.AddMeta("unit", "Watts") + output <- metric +} + +func (m *LenovoDensePowerCollector) Close() { + m.init = false +} diff --git a/collectors/lenovoDensePower.md b/collectors/lenovoDensePower.md new file mode 100644 index 0000000..a226330 --- /dev/null +++ b/collectors/lenovoDensePower.md @@ -0,0 +1,47 @@ + + +## `lenovo_dense_power` collector + +```json + "lenovo_dense_power": { + "ipmitool_path": "/path/to/ipmitool", + "use_sudo": true, + "include_metrics" : [] + } +``` + +The `lenovo_dense_power` collector reads power from Lenovo Dense machines via `ipmitool` (`ipmitool raw ...`). This collector is known to only work on the following machines. Others are not supported, so use at your *OWN* risk: + +System | Compatibility | Power measured +--- | --- | --- +Lenovo ThinkSystem SD650 V2/V3 | untested, but should work | node + riser1 + riser2 +Lenovo ThinkSystem SD650-N V2/ SD650-I V3 | untested, but should work | node + riser1 + riser2 + gpus +Lenovo ThinkSystem SD665-N V3 | tested, known to work | node + riser1 + riser2 + gpus + +To test if your system *may* be supported, you can run the following command *at your own risk*: + +``` +ipmitool raw 0x3a 50 4 2 0 0 0 +``` + +`ipmitool` typically require root to run. +In order to run `cc-metric-collector` without root priviliges, you can enable `use_sudo`. +Add a file like this in `/etc/sudoers.d/` to allow `cc-metric-collector` to run the required commands: + +``` +# Do not log the following sudo commands from monitoring, since this causes a lot of log spam. +# However keep log_denied enabled, to detect failures +Defaults: monitoring !log_allowed, !pam_session + +# Allow to use ipmitool for Lenovo power readings +monitoring ALL = (root) NOPASSWD:/usr/bin/ipmitool raw 58 50 4 2 0 0 0 +``` From a38804ab136a9bd60aaa158c3c2212c44e8fbc98 Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 17:08:01 +0200 Subject: [PATCH 02/11] Use more compact decimals in lenovo ipmi test command --- collectors/lenovoDensePower.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collectors/lenovoDensePower.md b/collectors/lenovoDensePower.md index a226330..b107d6b 100644 --- a/collectors/lenovoDensePower.md +++ b/collectors/lenovoDensePower.md @@ -30,7 +30,7 @@ Lenovo ThinkSystem SD665-N V3 | tested, known to work | node + r To test if your system *may* be supported, you can run the following command *at your own risk*: ``` -ipmitool raw 0x3a 50 4 2 0 0 0 +ipmitool raw 58 50 4 2 0 0 0 ``` `ipmitool` typically require root to run. From 4372cb7994a8a3aa0ee8ec19b5dfb3d1f4de0c59 Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 17:08:40 +0200 Subject: [PATCH 03/11] lenovoDensePower: Strip newlines as well from output --- collectors/lenovoDensePower.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index 4954f9e..f4815bf 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -99,7 +99,7 @@ func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { return 0, time.Unix(0, 0), fmt.Errorf("Failed to run ipmitool: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) } - data, err := hex.DecodeString(strings.ReplaceAll(stdout.String(), " ", "")) + data, err := hex.DecodeString(strings.ReplaceAll(strings.TrimSpace(stdout.String()), " ", "")) if err != nil { return 0, time.Unix(0, 0), fmt.Errorf("Unable to decode ipmitool hex output: %w (stdout=%s)", err, stdout.String()) } From 9fb3d2300c87d20624eedf7e44148520885e336e Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 17:20:46 +0200 Subject: [PATCH 04/11] lenovoDensePower: Actually mark it as initialized after Init() --- collectors/lenovoDensePower.go | 1 + 1 file changed, 1 insertion(+) diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index f4815bf..f493c6d 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -69,6 +69,7 @@ func (m *LenovoDensePowerCollector) Init(config json.RawMessage) error { m.energyValLast = energyVal m.energyTimeLast = energyTime + m.init = true return nil } From f48eb458e86ade810b3edd98c18a0ca9a025b51d Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 17:28:46 +0200 Subject: [PATCH 05/11] make fmt --- collectors/collectorManager.go | 58 +++++++++++++++++----------------- collectors/lenovoDensePower.go | 14 ++++---- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/collectors/collectorManager.go b/collectors/collectorManager.go index 1cff613..88445fa 100644 --- a/collectors/collectorManager.go +++ b/collectors/collectorManager.go @@ -21,35 +21,35 @@ import ( // Map of all available metric collectors var AvailableCollectors = map[string]MetricCollector{ - "likwid": new(LikwidCollector), - "loadavg": new(LoadavgCollector), - "memstat": new(MemstatCollector), - "netstat": new(NetstatCollector), - "ibstat": new(InfinibandCollector), - "lustrestat": new(LustreCollector), - "cpustat": new(CpustatCollector), - "topprocs": new(TopProcsCollector), - "nvidia": new(NvidiaCollector), - "customcmd": new(CustomCmdCollector), - "iostat": new(IOstatCollector), - "diskstat": new(DiskstatCollector), - "tempstat": new(TempCollector), - "ipmistat": new(IpmiCollector), - "gpfs": new(GpfsCollector), - "cpufreq": new(CPUFreqCollector), - "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), - "nfs3stat": new(Nfs3Collector), - "nfs4stat": new(Nfs4Collector), - "numastats": new(NUMAStatsCollector), - "beegfs_meta": new(BeegfsMetaCollector), - "beegfs_storage": new(BeegfsStorageCollector), - "rapl": new(RAPLCollector), - "rocm_smi": new(RocmSmiCollector), - "self": new(SelfCollector), - "schedstat": new(SchedstatCollector), - "nfsiostat": new(NfsIOStatCollector), - "slurm_cgroup": new(SlurmCgroupCollector), - "smartmon": new(SmartMonCollector), + "likwid": new(LikwidCollector), + "loadavg": new(LoadavgCollector), + "memstat": new(MemstatCollector), + "netstat": new(NetstatCollector), + "ibstat": new(InfinibandCollector), + "lustrestat": new(LustreCollector), + "cpustat": new(CpustatCollector), + "topprocs": new(TopProcsCollector), + "nvidia": new(NvidiaCollector), + "customcmd": new(CustomCmdCollector), + "iostat": new(IOstatCollector), + "diskstat": new(DiskstatCollector), + "tempstat": new(TempCollector), + "ipmistat": new(IpmiCollector), + "gpfs": new(GpfsCollector), + "cpufreq": new(CPUFreqCollector), + "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), + "nfs3stat": new(Nfs3Collector), + "nfs4stat": new(Nfs4Collector), + "numastats": new(NUMAStatsCollector), + "beegfs_meta": new(BeegfsMetaCollector), + "beegfs_storage": new(BeegfsStorageCollector), + "rapl": new(RAPLCollector), + "rocm_smi": new(RocmSmiCollector), + "self": new(SelfCollector), + "schedstat": new(SchedstatCollector), + "nfsiostat": new(NfsIOStatCollector), + "slurm_cgroup": new(SlurmCgroupCollector), + "smartmon": new(SmartMonCollector), "lenovo_dense_power": new(LenovoDensePowerCollector), } diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index f493c6d..0409170 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -9,8 +9,8 @@ package collectors import ( "bytes" - "encoding/json" "encoding/hex" + "encoding/json" "fmt" "os/exec" "strconv" @@ -25,14 +25,14 @@ type LenovoDensePowerCollector struct { metricCollector config struct { - IpmitoolPath string `json:"ipmitool_path"` - Sudo bool `json:"use_sudo"` + IpmitoolPath string `json:"ipmitool_path"` + Sudo bool `json:"use_sudo"` } - ipmitool string + ipmitool string - energyValLast float64 - energyTimeLast time.Time + energyValLast float64 + energyTimeLast time.Time } func (m *LenovoDensePowerCollector) Init(config json.RawMessage) error { @@ -111,7 +111,7 @@ func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { wholeJoules := (uint32(data[2]) << 0) | (uint32(data[3]) << 8) | (uint32(data[4]) << 16) | (uint32(data[5]) << 24) milliJoules := uint16(data[6]) | (uint16(data[7]) << 8) - finalJoules := float64(wholeJoules) + float64(milliJoules) * 1e-3 + finalJoules := float64(wholeJoules) + float64(milliJoules)*1e-3 wholeSeconds := (uint32(data[8]) << 0) | (uint32(data[9]) << 8) | (uint32(data[10]) << 16) | (uint32(data[11]) << 24) milliSeconds := uint16(data[12]) | (uint16(data[13]) << 8) From 93622b72ac2e0ff4bdd9921858d4af20f41b1a13 Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 17:45:19 +0200 Subject: [PATCH 06/11] lenovoDensePower: Fix linter checks --- collectors/lenovoDensePower.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index 0409170..9590232 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -64,7 +64,7 @@ func (m *LenovoDensePowerCollector) Init(config json.RawMessage) error { energyVal, energyTime, err := m.readEnergy() if err != nil { - return fmt.Errorf("Energy reading test failed: %w", err) + return fmt.Errorf("energy reading test failed: %w", err) } m.energyValLast = energyVal @@ -97,16 +97,16 @@ func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { err := cmd.Run() if err != nil { - return 0, time.Unix(0, 0), fmt.Errorf("Failed to run ipmitool: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) + return 0, time.Unix(0, 0), fmt.Errorf("failed to run ipmitool: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) } data, err := hex.DecodeString(strings.ReplaceAll(strings.TrimSpace(stdout.String()), " ", "")) if err != nil { - return 0, time.Unix(0, 0), fmt.Errorf("Unable to decode ipmitool hex output: %w (stdout=%s)", err, stdout.String()) + return 0, time.Unix(0, 0), fmt.Errorf("unable to decode ipmitool hex output: %w (stdout=%s)", err, stdout.String()) } if len(data) != 14 { - return 0, time.Unix(0, 0), fmt.Errorf("Result must be 14 bytes as specified in the documentation") + return 0, time.Unix(0, 0), fmt.Errorf("result must be 14 bytes as specified in the documentation") } wholeJoules := (uint32(data[2]) << 0) | (uint32(data[3]) << 8) | (uint32(data[4]) << 16) | (uint32(data[5]) << 24) From 81fa732e793e0763d9352bd793391bb6a7fb6a2c Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Fri, 3 Jul 2026 18:57:06 +0200 Subject: [PATCH 07/11] lenovoDensePower: Improve documentation, fix things Fix energy and time overflow handling. Send the metric as "lenovo_node_power" per default. This should avoid clashes by default. If you want the old behavior, use a router rule to rename the metric. --- collectors/lenovoDensePower.go | 24 +++++++++++++++++++----- collectors/lenovoDensePower.md | 8 ++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index 9590232..a245831 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -41,7 +41,7 @@ func (m *LenovoDensePowerCollector) Init(config json.RawMessage) error { return nil } - m.name = "LenovoDensePower" + m.name = "LenovoDensePowerCollector" if err := m.setup(); err != nil { return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err) } @@ -80,6 +80,7 @@ func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { argv = append(argv, "sudo", "-n") } + // The Lenovo hardware wants this magic byte string lenovoRequestEnergyMsg := []uint8{0x3a, 0x32, 4, 2, 0, 0, 0} argv = append(argv, m.ipmitool, "raw") @@ -109,6 +110,12 @@ func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { return 0, time.Unix(0, 0), fmt.Errorf("result must be 14 bytes as specified in the documentation") } + // data is layed out like this: + // data[0 to 1]: FIFO index of value (for debug only) + // data[2 to 5]: Integer part of energy in J (little endian) + // data[6 to 7]: Fraction part of energy in mJ (little endian) + // data[8 to 11]: Integer part of Unix time of measurement in seconds (little endian) + // data[12 to 13]: Fraction part of Unix time of measurement in milliseconds (little endian) wholeJoules := (uint32(data[2]) << 0) | (uint32(data[3]) << 8) | (uint32(data[4]) << 16) | (uint32(data[5]) << 24) milliJoules := uint16(data[6]) | (uint16(data[7]) << 8) finalJoules := float64(wholeJoules) + float64(milliJoules)*1e-3 @@ -133,16 +140,23 @@ func (m *LenovoDensePowerCollector) Read(interval time.Duration, output chan lp. } powerVal := 0.0 - if energyTime.After(m.energyTimeLast) { - energyValDiff := energyVal - m.energyValLast + if energyTime.Compare(m.energyTimeLast) != 0 { + // Check for overflow + energyValDiff := int64(energyVal) - int64(m.energyValLast) + if energyVal < m.energyValLast { + energyValDiff += int64(0x100000000) + } energyTimeDiff := energyTime.Sub(m.energyTimeLast) - powerVal = energyValDiff / energyTimeDiff.Seconds() + if energyTime.Before(m.energyTimeLast) { + energyTimeDiff = energyTimeDiff + 0x100000000 * time.Second + } + powerVal = float64(energyValDiff) / energyTimeDiff.Seconds() m.energyValLast = energyVal m.energyTimeLast = energyTime } - metric, err := lp.NewMetric("node_power", map[string]string{"type": "node"}, m.meta, powerVal, energyTime) + metric, err := lp.NewMetric("lenovo_node_power", map[string]string{"type": "node"}, m.meta, powerVal, energyTime) if err != nil { cclog.ComponentErrorf(m.name, "lp.NewMetric failed: %v", err) return diff --git a/collectors/lenovoDensePower.md b/collectors/lenovoDensePower.md index b107d6b..609e8d3 100644 --- a/collectors/lenovoDensePower.md +++ b/collectors/lenovoDensePower.md @@ -30,10 +30,14 @@ Lenovo ThinkSystem SD665-N V3 | tested, known to work | node + r To test if your system *may* be supported, you can run the following command *at your own risk*: ``` -ipmitool raw 58 50 4 2 0 0 0 +$ ipmitool raw 58 50 4 2 0 0 0 + c4 00 83 a9 cb d3 a2 03 df db 47 6a d8 01 ``` -`ipmitool` typically require root to run. +Exactly 14 bytes should be returned. +In case not or another error occurs your hardware is likely not supported. + +In addition, `ipmitool` typically require root to run. In order to run `cc-metric-collector` without root priviliges, you can enable `use_sudo`. Add a file like this in `/etc/sudoers.d/` to allow `cc-metric-collector` to run the required commands: From bda952f85ab09b3072ff10bae5c2df4eb3173cff Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Mon, 6 Jul 2026 14:06:41 +0200 Subject: [PATCH 08/11] Add MegwareEurekaCollector --- collectors/collectorManager.go | 61 ++++++----- collectors/megwareEureka.go | 194 +++++++++++++++++++++++++++++++++ collectors/megwareEureka.md | 44 ++++++++ 3 files changed, 269 insertions(+), 30 deletions(-) create mode 100644 collectors/megwareEureka.go create mode 100644 collectors/megwareEureka.md diff --git a/collectors/collectorManager.go b/collectors/collectorManager.go index 88445fa..f352293 100644 --- a/collectors/collectorManager.go +++ b/collectors/collectorManager.go @@ -21,36 +21,37 @@ import ( // Map of all available metric collectors var AvailableCollectors = map[string]MetricCollector{ - "likwid": new(LikwidCollector), - "loadavg": new(LoadavgCollector), - "memstat": new(MemstatCollector), - "netstat": new(NetstatCollector), - "ibstat": new(InfinibandCollector), - "lustrestat": new(LustreCollector), - "cpustat": new(CpustatCollector), - "topprocs": new(TopProcsCollector), - "nvidia": new(NvidiaCollector), - "customcmd": new(CustomCmdCollector), - "iostat": new(IOstatCollector), - "diskstat": new(DiskstatCollector), - "tempstat": new(TempCollector), - "ipmistat": new(IpmiCollector), - "gpfs": new(GpfsCollector), - "cpufreq": new(CPUFreqCollector), - "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), - "nfs3stat": new(Nfs3Collector), - "nfs4stat": new(Nfs4Collector), - "numastats": new(NUMAStatsCollector), - "beegfs_meta": new(BeegfsMetaCollector), - "beegfs_storage": new(BeegfsStorageCollector), - "rapl": new(RAPLCollector), - "rocm_smi": new(RocmSmiCollector), - "self": new(SelfCollector), - "schedstat": new(SchedstatCollector), - "nfsiostat": new(NfsIOStatCollector), - "slurm_cgroup": new(SlurmCgroupCollector), - "smartmon": new(SmartMonCollector), - "lenovo_dense_power": new(LenovoDensePowerCollector), + "likwid": new(LikwidCollector), + "loadavg": new(LoadavgCollector), + "memstat": new(MemstatCollector), + "netstat": new(NetstatCollector), + "ibstat": new(InfinibandCollector), + "lustrestat": new(LustreCollector), + "cpustat": new(CpustatCollector), + "topprocs": new(TopProcsCollector), + "nvidia": new(NvidiaCollector), + "customcmd": new(CustomCmdCollector), + "iostat": new(IOstatCollector), + "diskstat": new(DiskstatCollector), + "tempstat": new(TempCollector), + "ipmistat": new(IpmiCollector), + "gpfs": new(GpfsCollector), + "cpufreq": new(CPUFreqCollector), + "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), + "nfs3stat": new(Nfs3Collector), + "nfs4stat": new(Nfs4Collector), + "numastats": new(NUMAStatsCollector), + "beegfs_meta": new(BeegfsMetaCollector), + "beegfs_storage": new(BeegfsStorageCollector), + "rapl": new(RAPLCollector), + "rocm_smi": new(RocmSmiCollector), + "self": new(SelfCollector), + "schedstat": new(SchedstatCollector), + "nfsiostat": new(NfsIOStatCollector), + "slurm_cgroup": new(SlurmCgroupCollector), + "smartmon": new(SmartMonCollector), + "lenovo_dense_power": new(LenovoDensePowerCollector), + "megware_eureka": new(MegwareEurekaCollector), } // Metric collector manager data structure diff --git a/collectors/megwareEureka.go b/collectors/megwareEureka.go new file mode 100644 index 0000000..692bff5 --- /dev/null +++ b/collectors/megwareEureka.go @@ -0,0 +1,194 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// additional authors: +// Michael Panzlaff (NHR@FAU) + +package collectors + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" + "time" + + cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger" + lp "github.com/ClusterCockpit/cc-lib/v2/ccMessage" +) + +// MPS refers to Monolithic Power Systems, from which the MP5922 is used +// to measure telemetry on the Eureka platform. +type mpsData struct { + cnt int64 `json:"Cnt"` // No idea what this is, ignore + energy float64 `json:"Energy"` + vin float64 `json:"Vin"` + iin float64 `json:"Iin"` + pin float64 `json:"Pin"` + pinAvg float64 `json:"PinAvg"` + vout float64 `json:"Vout"` + iout float64 `json:"Iout"` + pout float64 `json:"Pout"` + standbyVout float64 `json:"StandbyVout"` + standbyIout float64 `json:"StandbyIout"` + standbyPout float64 `json:"StandbyPout"` + tempBusbar float64 `json:"TempBusbar"` + tempSsd float64 `json:"TempSsd"` + tempMps float64 `json:"TempMps"` + // No idea what the ones below mean exactl. Ignore them for now. + energyTime int64 `json:"EnergyTime"` + energyAccumulator int64 `json:"EnergyAccumulator"` + energyRolloverCnt int64 `json:"EnergyRolloverCnt"` + energySampleCntU24 int64 `json:"EnergySampleCntU24"` + + timestamp time.Time +} + +type MegwareEurekaCollector struct { + metricCollector + + config struct { + U20Path string `json:"u20_path"` + Sudo bool `json:"use_sudo"` + } + + u20path string + + energyValLast float64 + energyTimeLast time.Time +} + +func (m *MegwareEurekaCollector) Init(config json.RawMessage) error { + // Check if already initialized + if m.init { + return nil + } + + m.name = "MegwareEureka" + if err := m.setup(); err != nil { + return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err) + } + m.meta = map[string]string{ + "source": m.name, + "group": "U20", + } + + m.config.U20Path = "u20" + + if len(config) > 0 { + d := json.NewDecoder(bytes.NewReader(config)) + d.DisallowUnknownFields() + if err := d.Decode(&m.config); err != nil { + return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err) + } + } + + m.u20path = m.config.U20Path + + data, err := m.readMpsData() + if err != nil { + return fmt.Errorf("Energy reading test failed: %w", err) + } + + m.energyValLast = data.energy + m.energyTimeLast = time.Now() + m.init = true + + return nil +} + +func (m *MegwareEurekaCollector) readMpsData() (*mpsData, error) { + argv := make([]string, 0) + if m.config.Sudo { + argv = append(argv, "sudo", "-n") + } + + argv = append(argv, m.u20path, "values", "--read", "GET_MPS_POLL_VALUES") + + cmd := exec.Command(argv[0], argv[1:]...) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("Failed to run u20: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) + } + + var u20output struct { + getMpsPollValues mpsData `json:"GET_MPS_POLL_VALUES"` + } + + err = json.Unmarshal([]byte(stdout.String()), &u20output) + if err != nil { + return nil, fmt.Errorf("Unable to decode u20 JSON output: %w (stdout=%s)", err, stdout.String()) + } + + u20output.getMpsPollValues.timestamp = time.Now() + + return &u20output.getMpsPollValues, nil +} + +func (m *MegwareEurekaCollector) Read(interval time.Duration, output chan lp.CCMessage) { + // Check if already initialized + if !m.init { + return + } + + data, err := m.readMpsData() + if err != nil { + cclog.ComponentErrorf(m.name, "readMpsData failed: %v", err) + return + } + + powerVal := 0.0 + + if data.timestamp.After(m.energyTimeLast) { + // Important, m.energy comes in Wh, so multiply by 3600 to get Ws (aka Joule) + energyValDiff := data.energy - m.energyValLast + energyTimeDiff := data.timestamp.Sub(m.energyTimeLast) + powerVal = energyValDiff * 3600 / energyTimeDiff.Seconds() + + m.energyValLast = data.energy + m.energyTimeLast = data.timestamp + } + + metricNamePrefix := "eureka_" + metricMap := map[string]struct{ + value any + unit string + }{ + "power": { value: powerVal, unit: "Watts" }, + "vin": { value: data.vin, unit: "Volts" }, + "iin": { value: data.iin, unit: "Amperes" }, + "pin": { value: data.pin, unit: "Watts" }, + "pin_avg": { value: data.pinAvg, unit: "Watts" }, + "vout": { value: data.vout, unit: "Volts" }, + "iout": { value: data.iout, unit: "Amperes" }, + "pout": { value: data.pout, unit: "Watts" }, + "standby_vout": { value: data.standbyVout, unit: "Volts" }, + "standby_iout": { value: data.standbyIout, unit: "Amperes" }, + "standby_pout": { value: data.standbyPout, unit: "Watts" }, + "temp_busbar": { value: data.tempBusbar, unit: "degC" }, + "temp_ssd": { value: data.tempSsd, unit: "degC" }, + "temp_mps": { value: data.tempMps, unit: "degC" }, + } + + for metricName, metricData := range metricMap { + metricName = metricNamePrefix + metricName + metric, err := lp.NewMetric(metricName, map[string]string{"type": "node"}, m.meta, metricData.value, data.timestamp) + if err != nil { + cclog.ComponentErrorf(m.name, "lp.NewMetric failed: %v", err) + return + } + metric.AddMeta("unit", metricData.unit) + output <- metric + } +} + +func (m *MegwareEurekaCollector) Close() { + m.init = false +} diff --git a/collectors/megwareEureka.md b/collectors/megwareEureka.md new file mode 100644 index 0000000..d9aad67 --- /dev/null +++ b/collectors/megwareEureka.md @@ -0,0 +1,44 @@ + + +## `lenovo_dense_power` collector + +```json + "megware_eureka": { + "u20_path": "/path/to/ipmitool", + "use_sudo": true + } +``` + +The `megware_eureka` collector reads power and other metrics from Megware Eureka machines via `u20`. +If the u20 tool is available for your machines, it's possible that this collector is compatible. +If you don't have access to u20, please contact your Megware sales person. + +You can test the collector compatibility by running the following command: + +``` +$ u20 values --read GET_MPS_POLL_VALUES +``` + +If this returns a JSON with energy, voltage and current readings, you're fine. + +In addition, `u20` typically requires root to run. +In order to run `cc-metric-collector` without root priviliges, you can enable `use_sudo`. +Add a file like this in `/etc/sudoers.d/` to allow `cc-metric-collector` to run the required commands: + +``` +# Do not log the following sudo commands from monitoring, since this causes a lot of log spam. +# However keep log_denied enabled, to detect failures +Defaults: monitoring !log_allowed, !pam_session + +# Allow to use u20 for Megware power readings +monitoring ALL = (root) NOPASSWD:/usr/bin/u20 values --read GET_MPS_POLL_VALUES +``` From 8d1df385cea0c68ad249c9791c11036d36d99b9b Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Mon, 6 Jul 2026 14:09:45 +0200 Subject: [PATCH 09/11] make fmt --- collectors/collectorManager.go | 62 ++++++++++++++++---------------- collectors/ipmiMetric.go | 12 +++---- collectors/lenovoDensePower.go | 2 +- collectors/megwareEureka.go | 64 +++++++++++++++++----------------- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/collectors/collectorManager.go b/collectors/collectorManager.go index f352293..9cce461 100644 --- a/collectors/collectorManager.go +++ b/collectors/collectorManager.go @@ -21,37 +21,37 @@ import ( // Map of all available metric collectors var AvailableCollectors = map[string]MetricCollector{ - "likwid": new(LikwidCollector), - "loadavg": new(LoadavgCollector), - "memstat": new(MemstatCollector), - "netstat": new(NetstatCollector), - "ibstat": new(InfinibandCollector), - "lustrestat": new(LustreCollector), - "cpustat": new(CpustatCollector), - "topprocs": new(TopProcsCollector), - "nvidia": new(NvidiaCollector), - "customcmd": new(CustomCmdCollector), - "iostat": new(IOstatCollector), - "diskstat": new(DiskstatCollector), - "tempstat": new(TempCollector), - "ipmistat": new(IpmiCollector), - "gpfs": new(GpfsCollector), - "cpufreq": new(CPUFreqCollector), - "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), - "nfs3stat": new(Nfs3Collector), - "nfs4stat": new(Nfs4Collector), - "numastats": new(NUMAStatsCollector), - "beegfs_meta": new(BeegfsMetaCollector), - "beegfs_storage": new(BeegfsStorageCollector), - "rapl": new(RAPLCollector), - "rocm_smi": new(RocmSmiCollector), - "self": new(SelfCollector), - "schedstat": new(SchedstatCollector), - "nfsiostat": new(NfsIOStatCollector), - "slurm_cgroup": new(SlurmCgroupCollector), - "smartmon": new(SmartMonCollector), - "lenovo_dense_power": new(LenovoDensePowerCollector), - "megware_eureka": new(MegwareEurekaCollector), + "likwid": new(LikwidCollector), + "loadavg": new(LoadavgCollector), + "memstat": new(MemstatCollector), + "netstat": new(NetstatCollector), + "ibstat": new(InfinibandCollector), + "lustrestat": new(LustreCollector), + "cpustat": new(CpustatCollector), + "topprocs": new(TopProcsCollector), + "nvidia": new(NvidiaCollector), + "customcmd": new(CustomCmdCollector), + "iostat": new(IOstatCollector), + "diskstat": new(DiskstatCollector), + "tempstat": new(TempCollector), + "ipmistat": new(IpmiCollector), + "gpfs": new(GpfsCollector), + "cpufreq": new(CPUFreqCollector), + "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), + "nfs3stat": new(Nfs3Collector), + "nfs4stat": new(Nfs4Collector), + "numastats": new(NUMAStatsCollector), + "beegfs_meta": new(BeegfsMetaCollector), + "beegfs_storage": new(BeegfsStorageCollector), + "rapl": new(RAPLCollector), + "rocm_smi": new(RocmSmiCollector), + "self": new(SelfCollector), + "schedstat": new(SchedstatCollector), + "nfsiostat": new(NfsIOStatCollector), + "slurm_cgroup": new(SlurmCgroupCollector), + "smartmon": new(SmartMonCollector), + "lenovo_dense_power": new(LenovoDensePowerCollector), + "megware_eureka": new(MegwareEurekaCollector), } // Metric collector manager data structure diff --git a/collectors/ipmiMetric.go b/collectors/ipmiMetric.go index b15aa63..51aceb7 100644 --- a/collectors/ipmiMetric.go +++ b/collectors/ipmiMetric.go @@ -28,14 +28,14 @@ type IpmiCollector struct { metricCollector config struct { - IpmitoolPath string `json:"ipmitool_path"` - IpmisensorsPath string `json:"ipmisensors_path"` - Sudo bool `json:"use_sudo"` + IpmitoolPath string `json:"ipmitool_path"` + IpmisensorsPath string `json:"ipmisensors_path"` + Sudo bool `json:"use_sudo"` IncludeMetrics []string `json:"include_metrics"` } - ipmitool string - ipmisensors string + ipmitool string + ipmisensors string includeMetrics map[string]bool } @@ -158,7 +158,7 @@ func (m *IpmiCollector) readIpmiTool(output chan lp.CCMessage) error { name := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(lv[0]), " ", "_")) if len(m.includeMetrics) > 0 && !m.includeMetrics[name] { - continue + continue } unit := strings.TrimSpace(lv[2]) diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index a245831..dc8c4aa 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -148,7 +148,7 @@ func (m *LenovoDensePowerCollector) Read(interval time.Duration, output chan lp. } energyTimeDiff := energyTime.Sub(m.energyTimeLast) if energyTime.Before(m.energyTimeLast) { - energyTimeDiff = energyTimeDiff + 0x100000000 * time.Second + energyTimeDiff = energyTimeDiff + 0x100000000*time.Second } powerVal = float64(energyValDiff) / energyTimeDiff.Seconds() diff --git a/collectors/megwareEureka.go b/collectors/megwareEureka.go index 692bff5..7119e3f 100644 --- a/collectors/megwareEureka.go +++ b/collectors/megwareEureka.go @@ -21,25 +21,25 @@ import ( // MPS refers to Monolithic Power Systems, from which the MP5922 is used // to measure telemetry on the Eureka platform. type mpsData struct { - cnt int64 `json:"Cnt"` // No idea what this is, ignore - energy float64 `json:"Energy"` - vin float64 `json:"Vin"` - iin float64 `json:"Iin"` - pin float64 `json:"Pin"` - pinAvg float64 `json:"PinAvg"` - vout float64 `json:"Vout"` - iout float64 `json:"Iout"` - pout float64 `json:"Pout"` + cnt int64 `json:"Cnt"` // No idea what this is, ignore + energy float64 `json:"Energy"` + vin float64 `json:"Vin"` + iin float64 `json:"Iin"` + pin float64 `json:"Pin"` + pinAvg float64 `json:"PinAvg"` + vout float64 `json:"Vout"` + iout float64 `json:"Iout"` + pout float64 `json:"Pout"` standbyVout float64 `json:"StandbyVout"` standbyIout float64 `json:"StandbyIout"` standbyPout float64 `json:"StandbyPout"` - tempBusbar float64 `json:"TempBusbar"` - tempSsd float64 `json:"TempSsd"` - tempMps float64 `json:"TempMps"` + tempBusbar float64 `json:"TempBusbar"` + tempSsd float64 `json:"TempSsd"` + tempMps float64 `json:"TempMps"` // No idea what the ones below mean exactl. Ignore them for now. - energyTime int64 `json:"EnergyTime"` - energyAccumulator int64 `json:"EnergyAccumulator"` - energyRolloverCnt int64 `json:"EnergyRolloverCnt"` + energyTime int64 `json:"EnergyTime"` + energyAccumulator int64 `json:"EnergyAccumulator"` + energyRolloverCnt int64 `json:"EnergyRolloverCnt"` energySampleCntU24 int64 `json:"EnergySampleCntU24"` timestamp time.Time @@ -50,7 +50,7 @@ type MegwareEurekaCollector struct { config struct { U20Path string `json:"u20_path"` - Sudo bool `json:"use_sudo"` + Sudo bool `json:"use_sudo"` } u20path string @@ -157,24 +157,24 @@ func (m *MegwareEurekaCollector) Read(interval time.Duration, output chan lp.CCM } metricNamePrefix := "eureka_" - metricMap := map[string]struct{ + metricMap := map[string]struct { value any - unit string + unit string }{ - "power": { value: powerVal, unit: "Watts" }, - "vin": { value: data.vin, unit: "Volts" }, - "iin": { value: data.iin, unit: "Amperes" }, - "pin": { value: data.pin, unit: "Watts" }, - "pin_avg": { value: data.pinAvg, unit: "Watts" }, - "vout": { value: data.vout, unit: "Volts" }, - "iout": { value: data.iout, unit: "Amperes" }, - "pout": { value: data.pout, unit: "Watts" }, - "standby_vout": { value: data.standbyVout, unit: "Volts" }, - "standby_iout": { value: data.standbyIout, unit: "Amperes" }, - "standby_pout": { value: data.standbyPout, unit: "Watts" }, - "temp_busbar": { value: data.tempBusbar, unit: "degC" }, - "temp_ssd": { value: data.tempSsd, unit: "degC" }, - "temp_mps": { value: data.tempMps, unit: "degC" }, + "power": {value: powerVal, unit: "Watts"}, + "vin": {value: data.vin, unit: "Volts"}, + "iin": {value: data.iin, unit: "Amperes"}, + "pin": {value: data.pin, unit: "Watts"}, + "pin_avg": {value: data.pinAvg, unit: "Watts"}, + "vout": {value: data.vout, unit: "Volts"}, + "iout": {value: data.iout, unit: "Amperes"}, + "pout": {value: data.pout, unit: "Watts"}, + "standby_vout": {value: data.standbyVout, unit: "Volts"}, + "standby_iout": {value: data.standbyIout, unit: "Amperes"}, + "standby_pout": {value: data.standbyPout, unit: "Watts"}, + "temp_busbar": {value: data.tempBusbar, unit: "degC"}, + "temp_ssd": {value: data.tempSsd, unit: "degC"}, + "temp_mps": {value: data.tempMps, unit: "degC"}, } for metricName, metricData := range metricMap { From 025a5c2861eb2544c9c8e8293c4314bd3df329ad Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Tue, 7 Jul 2026 15:42:23 +0200 Subject: [PATCH 10/11] megwareEureka: JSON fields must be public in order to Unmarshal --- collectors/megwareEureka.go | 89 +++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/collectors/megwareEureka.go b/collectors/megwareEureka.go index 7119e3f..52334e4 100644 --- a/collectors/megwareEureka.go +++ b/collectors/megwareEureka.go @@ -21,28 +21,28 @@ import ( // MPS refers to Monolithic Power Systems, from which the MP5922 is used // to measure telemetry on the Eureka platform. type mpsData struct { - cnt int64 `json:"Cnt"` // No idea what this is, ignore - energy float64 `json:"Energy"` - vin float64 `json:"Vin"` - iin float64 `json:"Iin"` - pin float64 `json:"Pin"` - pinAvg float64 `json:"PinAvg"` - vout float64 `json:"Vout"` - iout float64 `json:"Iout"` - pout float64 `json:"Pout"` - standbyVout float64 `json:"StandbyVout"` - standbyIout float64 `json:"StandbyIout"` - standbyPout float64 `json:"StandbyPout"` - tempBusbar float64 `json:"TempBusbar"` - tempSsd float64 `json:"TempSsd"` - tempMps float64 `json:"TempMps"` + Cnt int64 `json:"Cnt"` // No idea what this is, ignore + Energy float64 `json:"Energy"` + Vin float64 `json:"Vin"` + Iin float64 `json:"Iin"` + Pin float64 `json:"Pin"` + PinAvg float64 `json:"PinAvg"` + Vout float64 `json:"Vout"` + Iout float64 `json:"Iout"` + Pout float64 `json:"Pout"` + StandbyVout float64 `json:"StandbyVout"` + StandbyIout float64 `json:"StandbyIout"` + StandbyPout float64 `json:"StandbyPout"` + TempBusbar float64 `json:"TempBusbar"` + TempSsd float64 `json:"TempSsd"` + TempMps float64 `json:"TempMps"` // No idea what the ones below mean exactl. Ignore them for now. - energyTime int64 `json:"EnergyTime"` - energyAccumulator int64 `json:"EnergyAccumulator"` - energyRolloverCnt int64 `json:"EnergyRolloverCnt"` - energySampleCntU24 int64 `json:"EnergySampleCntU24"` + EnergyTime int64 `json:"EnergyTime"` + EnergyAccumulator int64 `json:"EnergyAccumulator"` + EnergyRolloverCnt int64 `json:"EnergyRolloverCnt"` + EnergySampleCntU24 int64 `json:"EnergySampleCntU24"` - timestamp time.Time + Timestamp time.Time } type MegwareEurekaCollector struct { @@ -91,7 +91,7 @@ func (m *MegwareEurekaCollector) Init(config json.RawMessage) error { return fmt.Errorf("Energy reading test failed: %w", err) } - m.energyValLast = data.energy + m.energyValLast = data.Energy m.energyTimeLast = time.Now() m.init = true @@ -119,7 +119,7 @@ func (m *MegwareEurekaCollector) readMpsData() (*mpsData, error) { } var u20output struct { - getMpsPollValues mpsData `json:"GET_MPS_POLL_VALUES"` + GetMpsPollValues mpsData `json:"GET_MPS_POLL_VALUES"` } err = json.Unmarshal([]byte(stdout.String()), &u20output) @@ -127,9 +127,12 @@ func (m *MegwareEurekaCollector) readMpsData() (*mpsData, error) { return nil, fmt.Errorf("Unable to decode u20 JSON output: %w (stdout=%s)", err, stdout.String()) } - u20output.getMpsPollValues.timestamp = time.Now() + fmt.Printf("string: %+v\n", stdout.String()) + fmt.Printf("obj: %+v\n", u20output) - return &u20output.getMpsPollValues, nil + u20output.GetMpsPollValues.Timestamp = time.Now() + + return &u20output.GetMpsPollValues, nil } func (m *MegwareEurekaCollector) Read(interval time.Duration, output chan lp.CCMessage) { @@ -146,14 +149,14 @@ func (m *MegwareEurekaCollector) Read(interval time.Duration, output chan lp.CCM powerVal := 0.0 - if data.timestamp.After(m.energyTimeLast) { + if data.Timestamp.After(m.energyTimeLast) { // Important, m.energy comes in Wh, so multiply by 3600 to get Ws (aka Joule) - energyValDiff := data.energy - m.energyValLast - energyTimeDiff := data.timestamp.Sub(m.energyTimeLast) + energyValDiff := data.Energy - m.energyValLast + energyTimeDiff := data.Timestamp.Sub(m.energyTimeLast) powerVal = energyValDiff * 3600 / energyTimeDiff.Seconds() - m.energyValLast = data.energy - m.energyTimeLast = data.timestamp + m.energyValLast = data.Energy + m.energyTimeLast = data.Timestamp } metricNamePrefix := "eureka_" @@ -162,24 +165,24 @@ func (m *MegwareEurekaCollector) Read(interval time.Duration, output chan lp.CCM unit string }{ "power": {value: powerVal, unit: "Watts"}, - "vin": {value: data.vin, unit: "Volts"}, - "iin": {value: data.iin, unit: "Amperes"}, - "pin": {value: data.pin, unit: "Watts"}, - "pin_avg": {value: data.pinAvg, unit: "Watts"}, - "vout": {value: data.vout, unit: "Volts"}, - "iout": {value: data.iout, unit: "Amperes"}, - "pout": {value: data.pout, unit: "Watts"}, - "standby_vout": {value: data.standbyVout, unit: "Volts"}, - "standby_iout": {value: data.standbyIout, unit: "Amperes"}, - "standby_pout": {value: data.standbyPout, unit: "Watts"}, - "temp_busbar": {value: data.tempBusbar, unit: "degC"}, - "temp_ssd": {value: data.tempSsd, unit: "degC"}, - "temp_mps": {value: data.tempMps, unit: "degC"}, + "vin": {value: data.Vin, unit: "Volts"}, + "iin": {value: data.Iin, unit: "Amperes"}, + "pin": {value: data.Pin, unit: "Watts"}, + "pin_avg": {value: data.PinAvg, unit: "Watts"}, + "vout": {value: data.Vout, unit: "Volts"}, + "iout": {value: data.Iout, unit: "Amperes"}, + "pout": {value: data.Pout, unit: "Watts"}, + "standby_vout": {value: data.StandbyVout, unit: "Volts"}, + "standby_iout": {value: data.StandbyIout, unit: "Amperes"}, + "standby_pout": {value: data.StandbyPout, unit: "Watts"}, + "temp_busbar": {value: data.TempBusbar, unit: "degC"}, + "temp_ssd": {value: data.TempSsd, unit: "degC"}, + "temp_mps": {value: data.TempMps, unit: "degC"}, } for metricName, metricData := range metricMap { metricName = metricNamePrefix + metricName - metric, err := lp.NewMetric(metricName, map[string]string{"type": "node"}, m.meta, metricData.value, data.timestamp) + metric, err := lp.NewMetric(metricName, map[string]string{"type": "node"}, m.meta, metricData.value, data.Timestamp) if err != nil { cclog.ComponentErrorf(m.name, "lp.NewMetric failed: %v", err) return From f6d33fb29fed591da019103aa24e22d5b6e9648b Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Tue, 7 Jul 2026 16:51:06 +0200 Subject: [PATCH 11/11] Fix golangci-lint checks --- collectors/lenovoDensePower.go | 2 +- collectors/megwareEureka.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/collectors/lenovoDensePower.go b/collectors/lenovoDensePower.go index dc8c4aa..c0980bb 100644 --- a/collectors/lenovoDensePower.go +++ b/collectors/lenovoDensePower.go @@ -110,7 +110,7 @@ func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) { return 0, time.Unix(0, 0), fmt.Errorf("result must be 14 bytes as specified in the documentation") } - // data is layed out like this: + // data is laid out like this: // data[0 to 1]: FIFO index of value (for debug only) // data[2 to 5]: Integer part of energy in J (little endian) // data[6 to 7]: Fraction part of energy in mJ (little endian) diff --git a/collectors/megwareEureka.go b/collectors/megwareEureka.go index 52334e4..d7b255f 100644 --- a/collectors/megwareEureka.go +++ b/collectors/megwareEureka.go @@ -88,7 +88,7 @@ func (m *MegwareEurekaCollector) Init(config json.RawMessage) error { data, err := m.readMpsData() if err != nil { - return fmt.Errorf("Energy reading test failed: %w", err) + return fmt.Errorf("energy reading test failed: %w", err) } m.energyValLast = data.Energy @@ -115,16 +115,16 @@ func (m *MegwareEurekaCollector) readMpsData() (*mpsData, error) { err := cmd.Run() if err != nil { - return nil, fmt.Errorf("Failed to run u20: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) + return nil, fmt.Errorf("failed to run u20: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) } var u20output struct { GetMpsPollValues mpsData `json:"GET_MPS_POLL_VALUES"` } - err = json.Unmarshal([]byte(stdout.String()), &u20output) + err = json.Unmarshal(stdout.Bytes(), &u20output) if err != nil { - return nil, fmt.Errorf("Unable to decode u20 JSON output: %w (stdout=%s)", err, stdout.String()) + return nil, fmt.Errorf("unable to decode u20 JSON output: %w (stdout=%s)", err, stdout.String()) } fmt.Printf("string: %+v\n", stdout.String())