mirror of
https://github.com/ClusterCockpit/cc-metric-collector.git
synced 2026-07-18 15:40:38 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a859af358 | |||
| c4fb350ad2 | |||
| 14000c0ff0 | |||
| f6d33fb29f | |||
| 025a5c2861 | |||
| 8d1df385ce | |||
| bda952f85a | |||
| 81fa732e79 | |||
| 93622b72ac | |||
| f48eb458e8 | |||
| 9fb3d2300c | |||
| 4372cb7994 | |||
| a38804ab13 | |||
| 852ff18468 | |||
| 61570f9ecf | |||
| 3098e16ded | |||
| d53ec9233d | |||
| aaabcc639f | |||
| 14d624fd22 | |||
| 9677a916be | |||
| 8b373cc2c0 | |||
| f6260a8fb2 |
@@ -21,35 +21,37 @@ import (
|
|||||||
|
|
||||||
// Map of all available metric collectors
|
// Map of all available metric collectors
|
||||||
var AvailableCollectors = map[string]MetricCollector{
|
var AvailableCollectors = map[string]MetricCollector{
|
||||||
"likwid": new(LikwidCollector),
|
"likwid": new(LikwidCollector),
|
||||||
"loadavg": new(LoadavgCollector),
|
"loadavg": new(LoadavgCollector),
|
||||||
"memstat": new(MemstatCollector),
|
"memstat": new(MemstatCollector),
|
||||||
"netstat": new(NetstatCollector),
|
"netstat": new(NetstatCollector),
|
||||||
"ibstat": new(InfinibandCollector),
|
"ibstat": new(InfinibandCollector),
|
||||||
"lustrestat": new(LustreCollector),
|
"lustrestat": new(LustreCollector),
|
||||||
"cpustat": new(CpustatCollector),
|
"cpustat": new(CpustatCollector),
|
||||||
"topprocs": new(TopProcsCollector),
|
"topprocs": new(TopProcsCollector),
|
||||||
"nvidia": new(NvidiaCollector),
|
"nvidia": new(NvidiaCollector),
|
||||||
"customcmd": new(CustomCmdCollector),
|
"customcmd": new(CustomCmdCollector),
|
||||||
"iostat": new(IOstatCollector),
|
"iostat": new(IOstatCollector),
|
||||||
"diskstat": new(DiskstatCollector),
|
"diskstat": new(DiskstatCollector),
|
||||||
"tempstat": new(TempCollector),
|
"tempstat": new(TempCollector),
|
||||||
"ipmistat": new(IpmiCollector),
|
"ipmistat": new(IpmiCollector),
|
||||||
"gpfs": new(GpfsCollector),
|
"gpfs": new(GpfsCollector),
|
||||||
"cpufreq": new(CPUFreqCollector),
|
"cpufreq": new(CPUFreqCollector),
|
||||||
"cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector),
|
"cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector),
|
||||||
"nfs3stat": new(Nfs3Collector),
|
"nfs3stat": new(Nfs3Collector),
|
||||||
"nfs4stat": new(Nfs4Collector),
|
"nfs4stat": new(Nfs4Collector),
|
||||||
"numastats": new(NUMAStatsCollector),
|
"numastats": new(NUMAStatsCollector),
|
||||||
"beegfs_meta": new(BeegfsMetaCollector),
|
"beegfs_meta": new(BeegfsMetaCollector),
|
||||||
"beegfs_storage": new(BeegfsStorageCollector),
|
"beegfs_storage": new(BeegfsStorageCollector),
|
||||||
"rapl": new(RAPLCollector),
|
"rapl": new(RAPLCollector),
|
||||||
"rocm_smi": new(RocmSmiCollector),
|
"rocm_smi": new(RocmSmiCollector),
|
||||||
"self": new(SelfCollector),
|
"self": new(SelfCollector),
|
||||||
"schedstat": new(SchedstatCollector),
|
"schedstat": new(SchedstatCollector),
|
||||||
"nfsiostat": new(NfsIOStatCollector),
|
"nfsiostat": new(NfsIOStatCollector),
|
||||||
"slurm_cgroup": new(SlurmCgroupCollector),
|
"slurm_cgroup": new(SlurmCgroupCollector),
|
||||||
"smartmon": new(SmartMonCollector),
|
"smartmon": new(SmartMonCollector),
|
||||||
|
"lenovo_dense_power": new(LenovoDensePowerCollector),
|
||||||
|
"megware_eureka": new(MegwareEurekaCollector),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metric collector manager data structure
|
// Metric collector manager data structure
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ func (m *CPUFreqCpuInfoCollector) Init(_ json.RawMessage) error {
|
|||||||
physicalPackageID = ""
|
physicalPackageID = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("%s Init(): Call to scanner.Err failed: %w", m.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
return fmt.Errorf("%s Init(): Call to file.Close() failed: %w", m.name, err)
|
return fmt.Errorf("%s Init(): Call to file.Close() failed: %w", m.name, err)
|
||||||
@@ -179,6 +182,9 @@ func (m *CPUFreqCpuInfoCollector) Read(interval time.Duration, output chan lp.CC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
cclog.ComponentErrorf(m.name, "Read(): Call to scanner.Err failed: %s", err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CPUFreqCpuInfoCollector) Close() {
|
func (m *CPUFreqCpuInfoCollector) Close() {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type CpustatCollector struct {
|
|||||||
func (m *CpustatCollector) Init(config json.RawMessage) error {
|
func (m *CpustatCollector) Init(config json.RawMessage) error {
|
||||||
m.name = "CpustatCollector"
|
m.name = "CpustatCollector"
|
||||||
if err := m.setup(); err != nil {
|
if err := m.setup(); err != nil {
|
||||||
return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err)
|
return fmt.Errorf("%s Init(): setup() call failed: %s", m.name, err.Error())
|
||||||
}
|
}
|
||||||
m.parallel = true
|
m.parallel = true
|
||||||
m.meta = map[string]string{
|
m.meta = map[string]string{
|
||||||
@@ -58,7 +58,7 @@ func (m *CpustatCollector) Init(config json.RawMessage) error {
|
|||||||
d := json.NewDecoder(bytes.NewReader(config))
|
d := json.NewDecoder(bytes.NewReader(config))
|
||||||
d.DisallowUnknownFields()
|
d.DisallowUnknownFields()
|
||||||
if err := d.Decode(&m.config); err != nil {
|
if err := d.Decode(&m.config); err != nil {
|
||||||
return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err)
|
return fmt.Errorf("%s Init(): Error decoding JSON config: %s", m.name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matches := map[string]int{
|
matches := map[string]int{
|
||||||
@@ -85,7 +85,7 @@ func (m *CpustatCollector) Init(config json.RawMessage) error {
|
|||||||
// Check input file
|
// Check input file
|
||||||
file, err := os.Open(CPUSTATFILE)
|
file, err := os.Open(CPUSTATFILE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s Init(): Failed to open file '%s': %w", m.name, CPUSTATFILE, err)
|
return fmt.Errorf("%s Init(): Failed to open file '%s': %s", m.name, CPUSTATFILE, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-generate tags for all CPUs
|
// Pre-generate tags for all CPUs
|
||||||
@@ -117,10 +117,13 @@ func (m *CpustatCollector) Init(config json.RawMessage) error {
|
|||||||
num_cpus++
|
num_cpus++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("%s Init(): Call to scanner.Err failed: %s", m.name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// Close file
|
// Close file
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
return fmt.Errorf("%s Init(): Failed to close file '%s': %w", m.name, CPUSTATFILE, err)
|
return fmt.Errorf("%s Init(): Failed to close file '%s': %s", m.name, CPUSTATFILE, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
m.lastTimestamp = time.Now()
|
m.lastTimestamp = time.Now()
|
||||||
@@ -173,13 +176,13 @@ func (m *CpustatCollector) Read(interval time.Duration, output chan lp.CCMessage
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError(
|
cclog.ComponentError(
|
||||||
m.name,
|
m.name,
|
||||||
fmt.Sprintf("Read(): Failed to open file '%s': %v", CPUSTATFILE, err))
|
fmt.Sprintf("Read(): Failed to open file '%s': %v", CPUSTATFILE, err.Error()))
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
cclog.ComponentError(
|
cclog.ComponentError(
|
||||||
m.name,
|
m.name,
|
||||||
fmt.Sprintf("Read(): Failed to close file '%s': %v", string(CPUSTATFILE), err))
|
fmt.Sprintf("Read(): Failed to close file '%s': %v", string(CPUSTATFILE), err.Error()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -194,6 +197,9 @@ func (m *CpustatCollector) Read(interval time.Duration, output chan lp.CCMessage
|
|||||||
num_cpus++
|
num_cpus++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
cclog.ComponentErrorf(m.name, "Init(): Call to scanner.Err failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if !m.config.excludeNumCPUs {
|
if !m.config.excludeNumCPUs {
|
||||||
if num_cpus_metric, err := lp.NewMetric("num_cpus", m.nodetags, m.meta, num_cpus, now); err == nil {
|
if num_cpus_metric, err := lp.NewMetric("num_cpus", m.nodetags, m.meta, num_cpus, now); err == nil {
|
||||||
|
|||||||
@@ -147,6 +147,11 @@ mountLoop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
cclog.ComponentErrorf(
|
||||||
|
m.name,
|
||||||
|
"Read(): Call to scanner.Err failed: %s", err.Error())
|
||||||
|
}
|
||||||
if m.allowedMetrics["part_max_used"] {
|
if m.allowedMetrics["part_max_used"] {
|
||||||
y, err := lp.NewMetric("part_max_used", map[string]string{"type": "node"}, m.meta, int(part_max_used), time.Now())
|
y, err := lp.NewMetric("part_max_used", map[string]string{"type": "node"}, m.meta, int(part_max_used), time.Now())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ func (m *IOstatCollector) Init(config json.RawMessage) error {
|
|||||||
m.parallel = true
|
m.parallel = true
|
||||||
m.meta = map[string]string{"source": m.name, "group": "Disk"}
|
m.meta = map[string]string{"source": m.name, "group": "Disk"}
|
||||||
if err := m.setup(); err != nil {
|
if err := m.setup(); err != nil {
|
||||||
return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err)
|
return fmt.Errorf("%s Init(): setup() call failed: %s", m.name, err.Error())
|
||||||
}
|
}
|
||||||
if len(config) > 0 {
|
if len(config) > 0 {
|
||||||
d := json.NewDecoder(bytes.NewReader(config))
|
d := json.NewDecoder(bytes.NewReader(config))
|
||||||
d.DisallowUnknownFields()
|
d.DisallowUnknownFields()
|
||||||
if err := d.Decode(&m.config); err != nil {
|
if err := d.Decode(&m.config); err != nil {
|
||||||
return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err)
|
return fmt.Errorf("%s Init(): Error decoding JSON config: %s", m.name, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// https://www.kernel.org/doc/html/latest/admin-guide/iostats.html
|
// https://www.kernel.org/doc/html/latest/admin-guide/iostats.html
|
||||||
@@ -89,7 +89,7 @@ func (m *IOstatCollector) Init(config json.RawMessage) error {
|
|||||||
}
|
}
|
||||||
file, err := os.Open(IOSTATFILE)
|
file, err := os.Open(IOSTATFILE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s Init(): Failed to open file \"%s\": %w", m.name, IOSTATFILE, err)
|
return fmt.Errorf("%s Init(): Failed to open file \"%s\": %s", m.name, IOSTATFILE, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
@@ -130,8 +130,11 @@ func (m *IOstatCollector) Init(config json.RawMessage) error {
|
|||||||
lastValues: lastValues,
|
lastValues: lastValues,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("%s Init(): Failed to scan content of file %q: %s", m.name, IOSTATFILE, err.Error())
|
||||||
|
}
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
return fmt.Errorf("%s Init(): Failed to close file \"%s\": %w", m.name, IOSTATFILE, err)
|
return fmt.Errorf("%s Init(): Failed to close file \"%s\": %s", m.name, IOSTATFILE, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
m.init = true
|
m.init = true
|
||||||
@@ -147,14 +150,14 @@ func (m *IOstatCollector) Read(interval time.Duration, output chan lp.CCMessage)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError(
|
cclog.ComponentError(
|
||||||
m.name,
|
m.name,
|
||||||
fmt.Sprintf("Read(): Failed to open file '%s': %v", IOSTATFILE, err))
|
fmt.Sprintf("Read(): Failed to open file '%s': %s", IOSTATFILE, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
cclog.ComponentError(
|
cclog.ComponentError(
|
||||||
m.name,
|
m.name,
|
||||||
fmt.Sprintf("Read(): Failed to close file '%s': %v", IOSTATFILE, err))
|
fmt.Sprintf("Read(): Failed to close file '%s': %s", IOSTATFILE, err.Error()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -198,6 +201,11 @@ func (m *IOstatCollector) Read(interval time.Duration, output chan lp.CCMessage)
|
|||||||
}
|
}
|
||||||
m.devices[device] = entry
|
m.devices[device] = entry
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
cclog.ComponentErrorf(
|
||||||
|
m.name,
|
||||||
|
"Read(): Failed to scan content of file %q: %s", IOSTATFILE, err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *IOstatCollector) Close() {
|
func (m *IOstatCollector) Close() {
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ type IpmiCollector struct {
|
|||||||
metricCollector
|
metricCollector
|
||||||
|
|
||||||
config struct {
|
config struct {
|
||||||
IpmitoolPath string `json:"ipmitool_path"`
|
IpmitoolPath string `json:"ipmitool_path"`
|
||||||
IpmisensorsPath string `json:"ipmisensors_path"`
|
IpmisensorsPath string `json:"ipmisensors_path"`
|
||||||
Sudo bool `json:"use_sudo"`
|
Sudo bool `json:"use_sudo"`
|
||||||
|
IncludeMetrics []string `json:"include_metrics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ipmitool string
|
ipmitool string
|
||||||
ipmisensors string
|
ipmisensors string
|
||||||
|
includeMetrics map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *IpmiCollector) Init(config json.RawMessage) error {
|
func (m *IpmiCollector) Init(config json.RawMessage) error {
|
||||||
@@ -64,6 +66,15 @@ func (m *IpmiCollector) Init(config json.RawMessage) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read metrics to include
|
||||||
|
m.includeMetrics = make(map[string]bool)
|
||||||
|
for _, metric := range m.config.IncludeMetrics {
|
||||||
|
metric = strings.ToLower(strings.TrimSpace(metric))
|
||||||
|
if metric != "" {
|
||||||
|
m.includeMetrics[metric] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.ipmitool = m.config.IpmitoolPath
|
m.ipmitool = m.config.IpmitoolPath
|
||||||
m.ipmisensors = m.config.IpmisensorsPath
|
m.ipmisensors = m.config.IpmisensorsPath
|
||||||
|
|
||||||
@@ -145,6 +156,11 @@ func (m *IpmiCollector) readIpmiTool(output chan lp.CCMessage) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(lv[0]), " ", "_"))
|
name := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(lv[0]), " ", "_"))
|
||||||
|
|
||||||
|
if len(m.includeMetrics) > 0 && !m.includeMetrics[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
unit := strings.TrimSpace(lv[2])
|
unit := strings.TrimSpace(lv[2])
|
||||||
switch unit {
|
switch unit {
|
||||||
case "Volts":
|
case "Volts":
|
||||||
@@ -165,6 +181,9 @@ func (m *IpmiCollector) readIpmiTool(output chan lp.CCMessage) error {
|
|||||||
y.AddMeta("unit", unit)
|
y.AddMeta("unit", unit)
|
||||||
output <- y
|
output <- y
|
||||||
}
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to scan output of command: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for command end
|
// Wait for command end
|
||||||
if err := command.Wait(); err != nil {
|
if err := command.Wait(); err != nil {
|
||||||
@@ -209,6 +228,11 @@ func (m *IpmiCollector) readIpmiSensors(output chan lp.CCMessage) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.ToLower(strings.ReplaceAll(lv[1], " ", "_"))
|
name := strings.ToLower(strings.ReplaceAll(lv[1], " ", "_"))
|
||||||
|
|
||||||
|
if len(m.includeMetrics) > 0 && !m.includeMetrics[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
y, err := lp.NewMetric(name, map[string]string{"type": "node"}, m.meta, v, time.Now())
|
y, err := lp.NewMetric(name, map[string]string{"type": "node"}, m.meta, v, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentErrorf(m.name, "Failed to create message: %v", err)
|
cclog.ComponentErrorf(m.name, "Failed to create message: %v", err)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ hugo_path: docs/reference/cc-metric-collector/collectors/ipmi.md
|
|||||||
"ipmistat": {
|
"ipmistat": {
|
||||||
"ipmitool_path": "/path/to/ipmitool",
|
"ipmitool_path": "/path/to/ipmitool",
|
||||||
"ipmisensors_path": "/path/to/ipmi-sensors",
|
"ipmisensors_path": "/path/to/ipmi-sensors",
|
||||||
"use_sudo": true
|
"use_sudo": true,
|
||||||
|
"include_metrics" : []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,3 +37,5 @@ Defaults: monitoring !log_allowed, !pam_session
|
|||||||
monitoring ALL = (root) NOPASSWD:/usr/bin/ipmitool sensor
|
monitoring ALL = (root) NOPASSWD:/usr/bin/ipmitool sensor
|
||||||
monitoring ALL = (root) NOPASSWD:/usr/sbin/ipmi-sensors --comma-separated-output --sdr-cache-recreate
|
monitoring ALL = (root) NOPASSWD:/usr/sbin/ipmi-sensors --comma-separated-output --sdr-cache-recreate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `include_ipmi_metrics` contains any entry, ipmistat collector will only submit these metrics. Any other values will get discarded.
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// 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/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"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 = "LenovoDensePowerCollector"
|
||||||
|
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
|
||||||
|
m.init = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LenovoDensePowerCollector) readEnergy() (float64, time.Time, error) {
|
||||||
|
argv := make([]string, 0)
|
||||||
|
if m.config.Sudo {
|
||||||
|
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")
|
||||||
|
|
||||||
|
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(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())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) != 14 {
|
||||||
|
return 0, time.Unix(0, 0), fmt.Errorf("result must be 14 bytes as specified in the documentation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
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("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
|
||||||
|
}
|
||||||
|
metric.AddMeta("unit", "Watts")
|
||||||
|
output <- metric
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LenovoDensePowerCollector) Close() {
|
||||||
|
m.init = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<!--
|
||||||
|
---
|
||||||
|
title: Lenovo Dense Power collector
|
||||||
|
description: Collect power of Lenovo Dense machines using ipmitool raw
|
||||||
|
categories: [cc-metric-collector]
|
||||||
|
tags: ['Admin']
|
||||||
|
weight: 2
|
||||||
|
hugo_path: docs/reference/cc-metric-collector/collectors/lenovoDensePower.md
|
||||||
|
---
|
||||||
|
-->
|
||||||
|
|
||||||
|
## `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 58 50 4 2 0 0 0
|
||||||
|
c4 00 83 a9 cb d3 a2 03 df db 47 6a d8 01
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 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
|
||||||
|
```
|
||||||
@@ -38,7 +38,7 @@ import (
|
|||||||
topo "github.com/ClusterCockpit/cc-metric-collector/pkg/ccTopology"
|
topo "github.com/ClusterCockpit/cc-metric-collector/pkg/ccTopology"
|
||||||
"github.com/NVIDIA/go-nvml/pkg/dl"
|
"github.com/NVIDIA/go-nvml/pkg/dl"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"golang.design/x/thread"
|
"golang.design/x/runtime/thread"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// 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(stdout.Bytes(), &u20output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode u20 JSON output: %w (stdout=%s)", err, stdout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("string: %+v\n", stdout.String())
|
||||||
|
fmt.Printf("obj: %+v\n", u20output)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!--
|
||||||
|
---
|
||||||
|
title: Megware Eureka collector
|
||||||
|
description: Collect power and other metrics of Megware Eureka machines using u20
|
||||||
|
categories: [cc-metric-collector]
|
||||||
|
tags: ['Admin']
|
||||||
|
weight: 2
|
||||||
|
hugo_path: docs/reference/cc-metric-collector/collectors/lenovoDensePower.md
|
||||||
|
---
|
||||||
|
-->
|
||||||
|
|
||||||
|
## `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
|
||||||
|
```
|
||||||
+77
-210
@@ -22,24 +22,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// running average power limit (RAPL) monitoring attributes for a zone
|
// running average power limit (RAPL) monitoring attributes for a zone
|
||||||
// Only for Intel systems
|
|
||||||
|
|
||||||
type RAPLZoneInfo struct {
|
type RAPLZoneInfo struct {
|
||||||
energy int64 // current reading of the energy counter in micro joules
|
|
||||||
maxEnergyRange int64 // Range of the above energy counter in micro-joules
|
|
||||||
energyTimestamp time.Time // timestamp when energy counter was read
|
|
||||||
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
|
||||||
shortTermFilepath string // path to short term power limit
|
|
||||||
longTermFilepath string // path to long term power limit
|
|
||||||
enabledFilepath string // path to check whether limits are enabled
|
|
||||||
name string
|
|
||||||
|
|
||||||
// tags describing the RAPL zone:
|
// tags describing the RAPL zone:
|
||||||
// * zone_name, subzone_name: e.g. psys, dram, core, uncore, package-0
|
// * zone_name, subzone_name: e.g. psys, dram, core, uncore, package-0
|
||||||
// * zone_id: e.g. 0:1 (zone 0 sub zone 1)
|
// * zone_id: e.g. 0:1 (zone 0 sub zone 1)
|
||||||
// type=socket for dram, core, uncore, package-* and type=node for psys
|
tags map[string]string
|
||||||
// type-id=socket id
|
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
||||||
tags map[string]string
|
energy int64 // current reading of the energy counter in micro joules
|
||||||
|
energyTimestamp time.Time // timestamp when energy counter was read
|
||||||
|
maxEnergyRange int64 // Range of the above energy counter in micro-joules
|
||||||
}
|
}
|
||||||
|
|
||||||
type RAPLCollector struct {
|
type RAPLCollector struct {
|
||||||
@@ -51,40 +42,12 @@ type RAPLCollector struct {
|
|||||||
// * 0:1 for zone 0 subzone 1
|
// * 0:1 for zone 0 subzone 1
|
||||||
ExcludeByID []string `json:"exclude_device_by_id,omitempty"`
|
ExcludeByID []string `json:"exclude_device_by_id,omitempty"`
|
||||||
// Exclude names for RAPL zones, e.g. psys, dram, core, uncore, package-0
|
// Exclude names for RAPL zones, e.g. psys, dram, core, uncore, package-0
|
||||||
ExcludeByName []string `json:"exclude_device_by_name,omitempty"`
|
ExcludeByName []string `json:"exclude_device_by_name,omitempty"`
|
||||||
SkipEnergyReading bool `json:"skip_energy_reading,omitempty"`
|
|
||||||
SkipLimitsReading bool `json:"skip_limits_reading,omitempty"`
|
|
||||||
OnlyEnabledLimits bool `json:"only_enabled_limits,omitempty"`
|
|
||||||
}
|
}
|
||||||
raplZoneInfo []RAPLZoneInfo
|
RAPLZoneInfo []RAPLZoneInfo
|
||||||
meta map[string]string // default meta information
|
meta map[string]string // default meta information
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the path to the power limit file for zone selectable by limit name
|
|
||||||
// Common limit names for Intel systems are
|
|
||||||
// - long_term
|
|
||||||
// - short_term
|
|
||||||
// Does not support AMD as AMD systems do not provide the power limits
|
|
||||||
// through sysfs
|
|
||||||
func ZoneLimitFile(folder string, limit_name string) string {
|
|
||||||
nameGlob := filepath.Join(folder, "constraint_*_name")
|
|
||||||
candidates, err := filepath.Glob(nameGlob)
|
|
||||||
if err == nil {
|
|
||||||
for _, c := range candidates {
|
|
||||||
if v, err := os.ReadFile(c); err == nil {
|
|
||||||
if strings.TrimSpace(string(v)) == limit_name {
|
|
||||||
var i int
|
|
||||||
n, err := fmt.Sscanf(filepath.Base(c), "constraint_%d_name", &i)
|
|
||||||
if err == nil && n == 1 {
|
|
||||||
return filepath.Join(folder, fmt.Sprintf("constraint_%d_power_limit_uw", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the running average power limit (RAPL) collector
|
// Init initializes the running average power limit (RAPL) collector
|
||||||
func (m *RAPLCollector) Init(config json.RawMessage) error {
|
func (m *RAPLCollector) Init(config json.RawMessage) error {
|
||||||
// Check if already initialized
|
// Check if already initialized
|
||||||
@@ -104,9 +67,6 @@ func (m *RAPLCollector) Init(config json.RawMessage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read in the JSON configuration
|
// Read in the JSON configuration
|
||||||
m.config.SkipEnergyReading = false
|
|
||||||
m.config.SkipLimitsReading = false
|
|
||||||
m.config.OnlyEnabledLimits = true
|
|
||||||
if len(config) > 0 {
|
if len(config) > 0 {
|
||||||
d := json.NewDecoder(bytes.NewReader(config))
|
d := json.NewDecoder(bytes.NewReader(config))
|
||||||
d.DisallowUnknownFields()
|
d.DisallowUnknownFields()
|
||||||
@@ -133,63 +93,49 @@ func (m *RAPLCollector) Init(config json.RawMessage) error {
|
|||||||
// See: https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes
|
// See: https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes
|
||||||
readZoneInfo := func(zonePath string) (
|
readZoneInfo := func(zonePath string) (
|
||||||
z struct {
|
z struct {
|
||||||
name string // zones name e.g. psys, dram, core, uncore, package-0
|
name string // zones name e.g. psys, dram, core, uncore, package-0
|
||||||
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
||||||
energy int64 // current reading of the energy counter in micro joules
|
energy int64 // current reading of the energy counter in micro joules
|
||||||
energyTimestamp time.Time // timestamp when energy counter was read
|
energyTimestamp time.Time // timestamp when energy counter was read
|
||||||
maxEnergyRange int64 // Range of the above energy counter in micro-joules
|
maxEnergyRange int64 // Range of the above energy counter in micro-joules
|
||||||
shortTermFilepath string
|
ok bool // Are all information available?
|
||||||
longTermFilepath string
|
|
||||||
enabledFilepath string
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
// zones name e.g. psys, dram, core, uncore, package-0
|
// zones name e.g. psys, dram, core, uncore, package-0
|
||||||
|
foundName := false
|
||||||
if v, err :=
|
if v, err := os.ReadFile(
|
||||||
os.ReadFile(
|
filepath.Join(zonePath, "name")); err == nil {
|
||||||
filepath.Join(zonePath, "name")); err == nil {
|
foundName = true
|
||||||
z.name = strings.TrimSpace(string(v))
|
z.name = strings.TrimSpace(string(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.config.SkipEnergyReading {
|
// path to a file containing the zones current energy counter in micro joules
|
||||||
// path to a file containing the zones current energy counter in micro joules
|
z.energyFilepath = filepath.Join(zonePath, "energy_uj")
|
||||||
z.energyFilepath = filepath.Join(zonePath, "energy_uj")
|
|
||||||
// current reading of the energy counter in micro joules
|
// current reading of the energy counter in micro joules
|
||||||
if v, err := os.ReadFile(z.energyFilepath); err == nil {
|
foundEnergy := false
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
if v, err := os.ReadFile(z.energyFilepath); err == nil {
|
||||||
z.energy = i
|
// timestamp when energy counter was read
|
||||||
// timestamp when energy counter was read
|
z.energyTimestamp = time.Now()
|
||||||
z.energyTimestamp = time.Now()
|
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||||
}
|
foundEnergy = true
|
||||||
} else {
|
z.energy = i
|
||||||
cclog.ComponentErrorf(m.name, "Cannot read energy file for %s: %v", z.name, err.Error())
|
|
||||||
}
|
}
|
||||||
// Range of the above energy counter in micro-joules
|
|
||||||
if v, err :=
|
|
||||||
os.ReadFile(
|
|
||||||
filepath.Join(zonePath, "max_energy_range_uj")); err == nil {
|
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
|
||||||
z.maxEnergyRange = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cclog.ComponentDebugf(m.name, "Energy readings for %s disabled", zonePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.config.SkipLimitsReading {
|
// Range of the above energy counter in micro-joules
|
||||||
z.shortTermFilepath = ZoneLimitFile(zonePath, "short_term")
|
foundMaxEnergyRange := false
|
||||||
if _, err := os.Stat(z.shortTermFilepath); err != nil {
|
if v, err := os.ReadFile(
|
||||||
z.shortTermFilepath = ""
|
filepath.Join(zonePath, "max_energy_range_uj")); err == nil {
|
||||||
|
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||||
|
foundMaxEnergyRange = true
|
||||||
|
z.maxEnergyRange = i
|
||||||
}
|
}
|
||||||
z.longTermFilepath = ZoneLimitFile(zonePath, "long_term")
|
|
||||||
if _, err := os.Stat(z.longTermFilepath); err != nil {
|
|
||||||
z.longTermFilepath = ""
|
|
||||||
}
|
|
||||||
z.enabledFilepath = filepath.Join(zonePath, "enabled")
|
|
||||||
} else {
|
|
||||||
cclog.ComponentDebugf(m.name, "Power limit readings for %s disabled", zonePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Are all information available?
|
||||||
|
z.ok = foundName && foundEnergy && foundMaxEnergyRange
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,36 +152,14 @@ func (m *RAPLCollector) Init(config json.RawMessage) error {
|
|||||||
|
|
||||||
for _, zonePath := range zonesPath {
|
for _, zonePath := range zonesPath {
|
||||||
zoneID := strings.TrimPrefix(zonePath, zonePrefix)
|
zoneID := strings.TrimPrefix(zonePath, zonePrefix)
|
||||||
|
|
||||||
z := readZoneInfo(zonePath)
|
z := readZoneInfo(zonePath)
|
||||||
if !isIDExcluded[zoneID] &&
|
if z.ok &&
|
||||||
|
!isIDExcluded[zoneID] &&
|
||||||
!isNameExcluded[z.name] {
|
!isNameExcluded[z.name] {
|
||||||
|
|
||||||
si := RAPLZoneInfo{
|
|
||||||
tags: make(map[string]string),
|
|
||||||
energyFilepath: z.energyFilepath,
|
|
||||||
energy: z.energy,
|
|
||||||
energyTimestamp: z.energyTimestamp,
|
|
||||||
maxEnergyRange: z.maxEnergyRange,
|
|
||||||
shortTermFilepath: z.shortTermFilepath,
|
|
||||||
longTermFilepath: z.longTermFilepath,
|
|
||||||
enabledFilepath: z.enabledFilepath,
|
|
||||||
name: z.name,
|
|
||||||
}
|
|
||||||
si.tags["type"] = "node"
|
|
||||||
si.tags["type-id"] = "0"
|
|
||||||
var pid int = 0
|
|
||||||
if strings.HasPrefix(z.name, "package-") {
|
|
||||||
n, err := fmt.Sscanf(z.name, "package-%d", &pid)
|
|
||||||
if err == nil && n == 1 {
|
|
||||||
si.tags["type-id"] = fmt.Sprintf("%d", pid)
|
|
||||||
si.tags["type"] = "socket"
|
|
||||||
}
|
|
||||||
si.name = "pkg"
|
|
||||||
}
|
|
||||||
// Add RAPL monitoring attributes for a zone
|
// Add RAPL monitoring attributes for a zone
|
||||||
m.raplZoneInfo = append(
|
m.RAPLZoneInfo = append(
|
||||||
m.raplZoneInfo,
|
m.RAPLZoneInfo,
|
||||||
RAPLZoneInfo{
|
RAPLZoneInfo{
|
||||||
tags: map[string]string{
|
tags: map[string]string{
|
||||||
"id": zoneID,
|
"id": zoneID,
|
||||||
@@ -258,40 +182,37 @@ func (m *RAPLCollector) Init(config json.RawMessage) error {
|
|||||||
for _, subZonePath := range subZonesPath {
|
for _, subZonePath := range subZonesPath {
|
||||||
subZoneID := strings.TrimPrefix(subZonePath, subZonePrefix)
|
subZoneID := strings.TrimPrefix(subZonePath, subZonePrefix)
|
||||||
sz := readZoneInfo(subZonePath)
|
sz := readZoneInfo(subZonePath)
|
||||||
|
|
||||||
if len(zoneID) > 0 && len(z.name) > 0 &&
|
if len(zoneID) > 0 && len(z.name) > 0 &&
|
||||||
|
sz.ok &&
|
||||||
!isIDExcluded[zoneID+":"+subZoneID] &&
|
!isIDExcluded[zoneID+":"+subZoneID] &&
|
||||||
!isNameExcluded[sz.name] {
|
!isNameExcluded[sz.name] {
|
||||||
m.raplZoneInfo = append(
|
m.RAPLZoneInfo = append(
|
||||||
m.raplZoneInfo,
|
m.RAPLZoneInfo,
|
||||||
RAPLZoneInfo{
|
RAPLZoneInfo{
|
||||||
tags: map[string]string{
|
tags: map[string]string{
|
||||||
"id": zoneID + ":" + subZoneID,
|
"id": zoneID + ":" + subZoneID,
|
||||||
"zone_name": z.name,
|
"zone_name": z.name,
|
||||||
"sub_zone_name": sz.name,
|
"sub_zone_name": sz.name,
|
||||||
},
|
},
|
||||||
energyFilepath: sz.energyFilepath,
|
energyFilepath: sz.energyFilepath,
|
||||||
energy: sz.energy,
|
energy: sz.energy,
|
||||||
energyTimestamp: sz.energyTimestamp,
|
energyTimestamp: sz.energyTimestamp,
|
||||||
maxEnergyRange: sz.maxEnergyRange,
|
maxEnergyRange: sz.maxEnergyRange,
|
||||||
shortTermFilepath: sz.shortTermFilepath,
|
|
||||||
longTermFilepath: sz.longTermFilepath,
|
|
||||||
enabledFilepath: sz.enabledFilepath,
|
|
||||||
name: sz.name,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.raplZoneInfo == nil {
|
if m.RAPLZoneInfo == nil {
|
||||||
return fmt.Errorf("no running average power limit (RAPL) device found in %s", controlTypePath)
|
return fmt.Errorf("no running average power limit (RAPL) device found in %s", controlTypePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialized
|
// Initialized
|
||||||
cclog.ComponentDebug(
|
cclog.ComponentDebug(
|
||||||
m.name,
|
m.name,
|
||||||
"initialized %d zones with running average power limit (RAPL) monitoring attributes",
|
"initialized",
|
||||||
len(m.raplZoneInfo))
|
len(m.RAPLZoneInfo),
|
||||||
|
"zones with running average power limit (RAPL) monitoring attributes")
|
||||||
m.init = true
|
m.init = true
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@@ -300,89 +221,35 @@ func (m *RAPLCollector) Init(config json.RawMessage) error {
|
|||||||
// Read reads running average power limit (RAPL) monitoring attributes for all initialized zones
|
// Read reads running average power limit (RAPL) monitoring attributes for all initialized zones
|
||||||
// See: https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes
|
// See: https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes
|
||||||
func (m *RAPLCollector) Read(interval time.Duration, output chan lp.CCMessage) {
|
func (m *RAPLCollector) Read(interval time.Duration, output chan lp.CCMessage) {
|
||||||
for i := range m.raplZoneInfo {
|
for i := range m.RAPLZoneInfo {
|
||||||
p := &m.raplZoneInfo[i]
|
p := &m.RAPLZoneInfo[i]
|
||||||
|
|
||||||
if !m.config.SkipEnergyReading {
|
// Read current value of the energy counter in micro joules
|
||||||
// Read current value of the energy counter in micro joules
|
if v, err := os.ReadFile(p.energyFilepath); err == nil {
|
||||||
if v, err := os.ReadFile(p.energyFilepath); err == nil {
|
energyTimestamp := time.Now()
|
||||||
energyTimestamp := time.Now()
|
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
energy := i
|
||||||
energy := i
|
|
||||||
|
|
||||||
// Compute average power (Δ energy / Δ time)
|
// Compute average power (Δ energy / Δ time)
|
||||||
energyDiff := energy - p.energy
|
energyDiff := energy - p.energy
|
||||||
if energyDiff < 0 {
|
if energyDiff < 0 {
|
||||||
// Handle overflow:
|
// Handle overflow:
|
||||||
// ( p.maxEnergyRange - p.energy ) + energy
|
// ( p.maxEnergyRange - p.energy ) + energy
|
||||||
// = p.maxEnergyRange + ( energy - p.energy )
|
// = p.maxEnergyRange + ( energy - p.energy )
|
||||||
// = p.maxEnergyRange + diffEnergy
|
// = p.maxEnergyRange + diffEnergy
|
||||||
energyDiff += p.maxEnergyRange
|
energyDiff += p.maxEnergyRange
|
||||||
}
|
|
||||||
timeDiff := energyTimestamp.Sub(p.energyTimestamp)
|
|
||||||
averagePower := float64(energyDiff) / float64(timeDiff.Microseconds())
|
|
||||||
|
|
||||||
y, err := lp.NewMetric(
|
|
||||||
fmt.Sprintf("rapl_%s_average_power", p.name),
|
|
||||||
p.tags,
|
|
||||||
m.meta,
|
|
||||||
averagePower,
|
|
||||||
energyTimestamp)
|
|
||||||
if err == nil {
|
|
||||||
output <- y
|
|
||||||
}
|
|
||||||
|
|
||||||
e, err := lp.NewMetric(
|
|
||||||
fmt.Sprintf("rapl_%s_energy", p.name),
|
|
||||||
p.tags,
|
|
||||||
m.meta,
|
|
||||||
float64(energyDiff)*1e-3,
|
|
||||||
energyTimestamp)
|
|
||||||
if err == nil {
|
|
||||||
e.AddMeta("unit", "Joules")
|
|
||||||
output <- e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current energy counter state
|
|
||||||
p.energy = energy
|
|
||||||
p.energyTimestamp = energyTimestamp
|
|
||||||
}
|
}
|
||||||
}
|
timeDiff := energyTimestamp.Sub(p.energyTimestamp)
|
||||||
}
|
averagePower := float64(energyDiff) / float64(timeDiff.Microseconds())
|
||||||
// https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#constraints
|
|
||||||
if !m.config.SkipLimitsReading {
|
y, err := lp.NewMetric("rapl_average_power", p.tags, m.meta, averagePower, energyTimestamp)
|
||||||
skip := false
|
if err == nil {
|
||||||
if m.config.OnlyEnabledLimits {
|
output <- y
|
||||||
if v, err := os.ReadFile(p.enabledFilepath); err == nil {
|
|
||||||
if strings.TrimSpace(string(v)) == "0" {
|
|
||||||
skip = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !skip {
|
|
||||||
if len(p.shortTermFilepath) > 0 {
|
|
||||||
if v, err := os.ReadFile(p.shortTermFilepath); err == nil {
|
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
|
||||||
name := fmt.Sprintf("rapl_%s_limit_short_term", p.name)
|
|
||||||
y, err := lp.NewMetric(name, p.tags, m.meta, i/1e6, time.Now())
|
|
||||||
if err == nil {
|
|
||||||
output <- y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.longTermFilepath) > 0 {
|
// Save current energy counter state
|
||||||
if v, err := os.ReadFile(p.longTermFilepath); err == nil {
|
p.energy = energy
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
p.energyTimestamp = energyTimestamp
|
||||||
name := fmt.Sprintf("rapl_%s_limit_long_term", p.name)
|
|
||||||
y, err := lp.NewMetric(name, p.tags, m.meta, i/1e6, time.Now())
|
|
||||||
if err == nil {
|
|
||||||
output <- y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,24 +11,16 @@ hugo_path: docs/reference/cc-metric-collector/collectors/rapl.md
|
|||||||
|
|
||||||
## `rapl` collector
|
## `rapl` collector
|
||||||
|
|
||||||
This collector reads running average power limit (RAPL) monitoring attributes to compute average power consumption metrics. See <https://www.kernel.org/doc/html/latest/power/powercap/powercap.html>.
|
This collector reads running average power limit (RAPL) monitoring attributes to compute average power consumption metrics. See <https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes>.
|
||||||
|
|
||||||
|
The Likwid metric collector provides similar functionality.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"rapl": {
|
"rapl": {
|
||||||
"exclude_device_by_id": ["0:1", "0:2"],
|
"exclude_device_by_id": ["0:1", "0:2"],
|
||||||
"exclude_device_by_name": ["psys"],
|
"exclude_device_by_name": ["psys"]
|
||||||
"skip_energy_reading": false,
|
|
||||||
"skip_limits_reading": false,
|
|
||||||
"only_enabled_limits": true
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Metrics:
|
Metrics:
|
||||||
* `rapl_<domain>_average_power`: average power consumption in Watt. The average is computed over the entire runtime from the last measurement to the current measurement
|
* `rapl_average_power`: average power consumption in Watt. The average is computed over the entire runtime from the last measurement to the current measurement
|
||||||
* `rapl_<domain>_energy`: Difference from the last measurement
|
|
||||||
* `rapl_<domain>_limit_short_term`: Short term powercap setting for the domain
|
|
||||||
* `rapl_<domain>_limit_long_term`: Long term powercap setting for the domain
|
|
||||||
|
|
||||||
Only the `rapl_<domain>_average_power` and `rapl_<domain>_energy` metrics require root-permissions. The limits can be read as user. Some domains have limits available but they are not enabled. By default, only enabled domain limits are collected.
|
|
||||||
|
|
||||||
Energy and power measurments can also be done with the Likwid metric collector.
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -45,6 +46,37 @@ type SlurmCgroupCollector struct {
|
|||||||
|
|
||||||
const defaultCgroupBase = "/sys/fs/cgroup/system.slice/slurmstepd.scope"
|
const defaultCgroupBase = "/sys/fs/cgroup/system.slice/slurmstepd.scope"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Slurm cgroup v2 directory layout:
|
||||||
|
// - Slurm <= 25.11: job_<numeric job id>
|
||||||
|
// - Slurm >= 26.05: SLUID, encoded as "s" + 13 Crockford Base32 characters
|
||||||
|
jobIDDirRE = regexp.MustCompile(`^job_[0-9]+$`)
|
||||||
|
sluidDirRE = regexp.MustCompile(`(?i)^s[0-9A-HJKMNP-TV-Z]{13}$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *SlurmCgroupCollector) findSlurmJobDirs() ([]string, error) {
|
||||||
|
entries, err := os.ReadDir(m.cgroupBase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jobDirs := make([]string, 0)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := entry.Name()
|
||||||
|
|
||||||
|
if jobIDDirRE.MatchString(name) || sluidDirRE.MatchString(name) {
|
||||||
|
jobDirs = append(jobDirs, filepath.Join(m.cgroupBase, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobDirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ParseCPUs(cpuset string) ([]int, error) {
|
func ParseCPUs(cpuset string) ([]int, error) {
|
||||||
var result []int
|
var result []int
|
||||||
if cpuset == "" {
|
if cpuset == "" {
|
||||||
@@ -237,10 +269,9 @@ func (m *SlurmCgroupCollector) Read(interval time.Duration, output chan lp.CCMes
|
|||||||
delete(m.cpuUsed, k)
|
delete(m.cpuUsed, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
globPattern := filepath.Join(m.cgroupBase, "job_*")
|
jobDirs, err := m.findSlurmJobDirs()
|
||||||
jobDirs, err := filepath.Glob(globPattern)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError(m.name, "Error globbing job directories:", err.Error())
|
cclog.ComponentError(m.name, "Error reading job directories:", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/ClusterCockpit/cc-lib/v2 v2.12.0
|
github.com/ClusterCockpit/cc-lib/v2 v2.12.0
|
||||||
github.com/ClusterCockpit/go-rocm-smi v0.4.0
|
github.com/ClusterCockpit/go-rocm-smi v0.4.0
|
||||||
github.com/NVIDIA/go-nvml v0.13.0-1
|
github.com/NVIDIA/go-nvml v0.13.2-0
|
||||||
github.com/PaesslerAG/gval v1.2.4
|
github.com/PaesslerAG/gval v1.2.4
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/tklauser/go-sysconf v0.4.0
|
github.com/tklauser/go-sysconf v0.4.0
|
||||||
golang.design/x/thread v0.3.2
|
golang.design/x/runtime v0.3.0
|
||||||
golang.org/x/sys v0.45.0
|
golang.org/x/sys v0.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
|||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||||
github.com/NVIDIA/go-nvml v0.13.0-1 h1:OLX8Jq3dONuPOQPC7rndB6+iDmDakw0XTYgzMxObkEw=
|
|
||||||
github.com/NVIDIA/go-nvml v0.13.0-1/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4hrK3LmPt4=
|
github.com/NVIDIA/go-nvml v0.13.0-1/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4hrK3LmPt4=
|
||||||
|
github.com/NVIDIA/go-nvml v0.13.2-0 h1:7M4cFG62wSUHw8i0XSiNU7ejKODytTS6ZrW/vgB2NSI=
|
||||||
|
github.com/NVIDIA/go-nvml v0.13.2-0/go.mod h1:ahi2psRYoa+wYUBIrZPRO+wJs9lcvMhxSSkjjvsJJNQ=
|
||||||
github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
|
github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
|
||||||
github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||||
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
||||||
@@ -173,8 +174,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
golang.design/x/thread v0.3.2 h1:FmD1glspGrQCe6FuQLmSrT6wz2CSzq7vKVDluyiMnqo=
|
golang.design/x/runtime v0.3.0 h1:8bA+GQfO8A18JXJQlQA56pEc+Sgrbo4cmLzjzmJ58ZI=
|
||||||
golang.design/x/thread v0.3.2/go.mod h1:6+Hi2rMOgMHZdKDWaqNHyWtoFUx1HxZ06LfHPh5Z/hQ=
|
golang.design/x/runtime v0.3.0/go.mod h1:PjVKQjImLdSrvjIw4FmrnCPq9BGs5PMBHb8i6b2BN9I=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 h1:9k5exFQKQglLo+RoP+4zMjOFE14P6+vyR0baDAi0Rcs=
|
golang.org/x/exp v0.0.0-20231005195138-3e424a577f31 h1:9k5exFQKQglLo+RoP+4zMjOFE14P6+vyR0baDAi0Rcs=
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
|||||||
vars["starttime"] = starttime
|
vars["starttime"] = starttime
|
||||||
vars["endtime"] = endtime
|
vars["endtime"] = endtime
|
||||||
for _, f := range c.functions {
|
for _, f := range c.functions {
|
||||||
cclog.ComponentDebug("MetricCache", "COLLECT", f.Name, "COND", f.Condition)
|
cclog.ComponentDebugf("MetricCache", "COLLECT %s COND '%s'", f.Name, f.Condition)
|
||||||
var valuesFloat64 []float64
|
var valuesFloat64 []float64
|
||||||
var valuesFloat32 []float32
|
var valuesFloat32 []float32
|
||||||
var valuesInt []int
|
var valuesInt []int
|
||||||
@@ -140,7 +140,7 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
|||||||
vars["metric"] = m
|
vars["metric"] = m
|
||||||
value, err := f.gvalCond.EvalBool(context.Background(), vars)
|
value, err := f.gvalCond.EvalBool(context.Background(), vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError("MetricCache", "COLLECT", f.Name, "COND", f.Condition, ":", err.Error())
|
cclog.ComponentErrorf("MetricCache", "COLLECT %s COND '%s' : %s", f.Name, f.Condition, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if value {
|
if value {
|
||||||
@@ -160,7 +160,7 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
|||||||
case bool:
|
case bool:
|
||||||
valuesBool = append(valuesBool, x)
|
valuesBool = append(valuesBool, x)
|
||||||
default:
|
default:
|
||||||
cclog.ComponentError("MetricCache", "COLLECT ADD VALUE", v, "FAILED")
|
cclog.ComponentErrorf("MetricCache", "COLLECT ADD VALUE %v FAILED", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matches = append(matches, m)
|
matches = append(matches, m)
|
||||||
@@ -213,13 +213,13 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
|||||||
vars["values"] = valuesBool
|
vars["values"] = valuesBool
|
||||||
len_values = len(valuesBool)
|
len_values = len(valuesBool)
|
||||||
}
|
}
|
||||||
cclog.ComponentDebug("MetricCache", "EVALUATE", f.Name, "METRICS", len_values, "CALC", f.Function)
|
cclog.ComponentDebugf("MetricCache", "EVALUATE %s METRICS %d CALC '%s'", f.Name, len_values, f.Function)
|
||||||
|
|
||||||
vars["metrics"] = matches
|
vars["metrics"] = matches
|
||||||
if len_values > 0 {
|
if len_values > 0 {
|
||||||
value, err := gval.Evaluate(f.Function, vars, c.language)
|
value, err := gval.Evaluate(f.Function, vars, c.language)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError("MetricCache", "EVALUATE", f.Name, "METRICS", len_values, "CALC", f.Function, ":", err.Error())
|
cclog.ComponentErrorf("MetricCache", "EVALUATE %s METRICS %d CALC '%s': %s", f.Name, len_values, f.Function, err.Error())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,12 +273,12 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
|||||||
case string:
|
case string:
|
||||||
m, err = lp.NewMessage(f.Name, tags, meta, map[string]any{"value": t}, starttime)
|
m, err = lp.NewMessage(f.Name, tags, meta, map[string]any{"value": t}, starttime)
|
||||||
default:
|
default:
|
||||||
cclog.ComponentError("MetricCache", "Gval returned invalid type", t, "skipping metric", f.Name)
|
cclog.ComponentErrorf("MetricCache", "Gval returned invalid type %s skipping metric %s", t, f.Name)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError("MetricCache", "Cannot create metric from Gval result", value, ":", err.Error())
|
cclog.ComponentErrorf("MetricCache", "Cannot create metric from Gval result %v: %s", value, err.Error())
|
||||||
}
|
}
|
||||||
cclog.ComponentDebug("MetricCache", "SEND", m)
|
cclog.ComponentDebugf("MetricCache", "SEND %s", m.ToLineProtocol(nil))
|
||||||
select {
|
select {
|
||||||
case c.output <- m:
|
case c.output <- m:
|
||||||
default:
|
default:
|
||||||
@@ -295,12 +295,12 @@ func (c *metricAggregator) AddAggregation(name, function, condition string, tags
|
|||||||
newcond := strings.ReplaceAll(condition, "'", "\"")
|
newcond := strings.ReplaceAll(condition, "'", "\"")
|
||||||
gvalCond, err := gval.Full(metricCacheLanguage).NewEvaluable(newcond)
|
gvalCond, err := gval.Full(metricCacheLanguage).NewEvaluable(newcond)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError("MetricAggregator", "Cannot add aggregation, invalid if condition", newcond, ":", err.Error())
|
cclog.ComponentErrorf("MetricAggregator", "Cannot add aggregation, invalid if condition '%s': %s", newcond, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
gvalFunc, err := gval.Full(metricCacheLanguage).NewEvaluable(newfunc)
|
gvalFunc, err := gval.Full(metricCacheLanguage).NewEvaluable(newfunc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError("MetricAggregator", "Cannot add aggregation, invalid function condition", newfunc, ":", err.Error())
|
cclog.ComponentErrorf("MetricAggregator", "Cannot add aggregation, invalid function condition %s: %s", newfunc, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, agg := range c.functions {
|
for _, agg := range c.functions {
|
||||||
|
|||||||
Reference in New Issue
Block a user