mirror of
https://github.com/ClusterCockpit/cc-metric-collector.git
synced 2026-07-18 15:40:38 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53fdda341e |
@@ -21,37 +21,35 @@ 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,9 +116,6 @@ 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)
|
||||||
@@ -182,9 +179,6 @@ 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: %s", m.name, err.Error())
|
return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err)
|
||||||
}
|
}
|
||||||
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: %s", m.name, err.Error())
|
return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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': %s", m.name, CPUSTATFILE, err.Error())
|
return fmt.Errorf("%s Init(): Failed to open file '%s': %w", m.name, CPUSTATFILE, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-generate tags for all CPUs
|
// Pre-generate tags for all CPUs
|
||||||
@@ -117,13 +117,10 @@ 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': %s", m.name, CPUSTATFILE, err.Error())
|
return fmt.Errorf("%s Init(): Failed to close file '%s': %w", m.name, CPUSTATFILE, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.lastTimestamp = time.Now()
|
m.lastTimestamp = time.Now()
|
||||||
@@ -176,13 +173,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.Error()))
|
fmt.Sprintf("Read(): Failed to open file '%s': %v", CPUSTATFILE, err))
|
||||||
}
|
}
|
||||||
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.Error()))
|
fmt.Sprintf("Read(): Failed to close file '%s': %v", string(CPUSTATFILE), err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -197,9 +194,6 @@ 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,11 +147,6 @@ 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: %s", m.name, err.Error())
|
return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err)
|
||||||
}
|
}
|
||||||
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: %s", m.name, err.Error())
|
return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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\": %s", m.name, IOSTATFILE, err.Error())
|
return fmt.Errorf("%s Init(): Failed to open file \"%s\": %w", m.name, IOSTATFILE, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
@@ -130,11 +130,8 @@ 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\": %s", m.name, IOSTATFILE, err.Error())
|
return fmt.Errorf("%s Init(): Failed to close file \"%s\": %w", m.name, IOSTATFILE, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.init = true
|
m.init = true
|
||||||
@@ -150,14 +147,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': %s", IOSTATFILE, err.Error()))
|
fmt.Sprintf("Read(): Failed to open file '%s': %v", IOSTATFILE, err))
|
||||||
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': %s", IOSTATFILE, err.Error()))
|
fmt.Sprintf("Read(): Failed to close file '%s': %v", IOSTATFILE, err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -201,11 +198,6 @@ 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,15 +28,13 @@ 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 {
|
||||||
@@ -66,15 +64,6 @@ 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
|
||||||
|
|
||||||
@@ -156,11 +145,6 @@ 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":
|
||||||
@@ -181,9 +165,6 @@ 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 {
|
||||||
@@ -228,11 +209,6 @@ 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,8 +15,7 @@ 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" : []
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -37,5 +36,3 @@ 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.
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<!--
|
|
||||||
---
|
|
||||||
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/runtime/thread"
|
"golang.design/x/thread"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
// 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())
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<!--
|
|
||||||
---
|
|
||||||
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
|
|
||||||
```
|
|
||||||
+210
-77
@@ -22,15 +22,24 @@ 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)
|
||||||
tags map[string]string
|
// type=socket for dram, core, uncore, package-* and type=node for psys
|
||||||
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
// type-id=socket id
|
||||||
energy int64 // current reading of the energy counter in micro joules
|
tags map[string]string
|
||||||
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 {
|
||||||
@@ -42,12 +51,40 @@ 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
|
||||||
@@ -67,6 +104,9 @@ 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()
|
||||||
@@ -93,49 +133,63 @@ 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
|
||||||
ok bool // Are all information available?
|
shortTermFilepath string
|
||||||
|
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 := os.ReadFile(
|
if v, err :=
|
||||||
filepath.Join(zonePath, "name")); err == nil {
|
os.ReadFile(
|
||||||
foundName = true
|
filepath.Join(zonePath, "name")); err == nil {
|
||||||
z.name = strings.TrimSpace(string(v))
|
z.name = strings.TrimSpace(string(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// path to a file containing the zones current energy counter in micro joules
|
if !m.config.SkipEnergyReading {
|
||||||
z.energyFilepath = filepath.Join(zonePath, "energy_uj")
|
// path to a file containing the zones current energy counter in micro joules
|
||||||
|
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
|
||||||
foundEnergy := false
|
if v, err := os.ReadFile(z.energyFilepath); err == nil {
|
||||||
if v, err := os.ReadFile(z.energyFilepath); err == nil {
|
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||||
// timestamp when energy counter was read
|
z.energy = i
|
||||||
z.energyTimestamp = time.Now()
|
// timestamp when energy counter was read
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
z.energyTimestamp = time.Now()
|
||||||
foundEnergy = true
|
}
|
||||||
z.energy = i
|
} else {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range of the above energy counter in micro-joules
|
if !m.config.SkipLimitsReading {
|
||||||
foundMaxEnergyRange := false
|
z.shortTermFilepath = ZoneLimitFile(zonePath, "short_term")
|
||||||
if v, err := os.ReadFile(
|
if _, err := os.Stat(z.shortTermFilepath); err != nil {
|
||||||
filepath.Join(zonePath, "max_energy_range_uj")); err == nil {
|
z.shortTermFilepath = ""
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +206,36 @@ 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 z.ok &&
|
if !isIDExcluded[zoneID] &&
|
||||||
!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,
|
||||||
@@ -182,37 +258,40 @@ 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",
|
"initialized %d zones with running average power limit (RAPL) monitoring attributes",
|
||||||
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
|
||||||
@@ -221,35 +300,89 @@ 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]
|
||||||
|
|
||||||
// Read current value of the energy counter in micro joules
|
if !m.config.SkipEnergyReading {
|
||||||
if v, err := os.ReadFile(p.energyFilepath); err == nil {
|
// Read current value of the energy counter in micro joules
|
||||||
energyTimestamp := time.Now()
|
if v, err := os.ReadFile(p.energyFilepath); err == nil {
|
||||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
energyTimestamp := time.Now()
|
||||||
energy := i
|
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||||
|
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
|
||||||
y, err := lp.NewMetric("rapl_average_power", p.tags, m.meta, averagePower, energyTimestamp)
|
if !m.config.SkipLimitsReading {
|
||||||
if err == nil {
|
skip := false
|
||||||
output <- y
|
if m.config.OnlyEnabledLimits {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current energy counter state
|
if len(p.longTermFilepath) > 0 {
|
||||||
p.energy = energy
|
if v, err := os.ReadFile(p.longTermFilepath); err == nil {
|
||||||
p.energyTimestamp = energyTimestamp
|
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||||
|
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,16 +11,24 @@ 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#monitoring-attributes>.
|
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>.
|
||||||
|
|
||||||
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_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>_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,7 +7,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -46,37 +45,6 @@ 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 == "" {
|
||||||
@@ -269,9 +237,10 @@ func (m *SlurmCgroupCollector) Read(interval time.Duration, output chan lp.CCMes
|
|||||||
delete(m.cpuUsed, k)
|
delete(m.cpuUsed, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
jobDirs, err := m.findSlurmJobDirs()
|
globPattern := filepath.Join(m.cgroupBase, "job_*")
|
||||||
|
jobDirs, err := filepath.Glob(globPattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentError(m.name, "Error reading job directories:", err.Error())
|
cclog.ComponentError(m.name, "Error globbing 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.2-0
|
github.com/NVIDIA/go-nvml v0.13.0-1
|
||||||
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/runtime v0.3.0
|
golang.design/x/thread v0.3.2
|
||||||
golang.org/x/sys v0.45.0
|
golang.org/x/sys v0.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ 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=
|
||||||
@@ -174,8 +173,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/runtime v0.3.0 h1:8bA+GQfO8A18JXJQlQA56pEc+Sgrbo4cmLzjzmJ58ZI=
|
golang.design/x/thread v0.3.2 h1:FmD1glspGrQCe6FuQLmSrT6wz2CSzq7vKVDluyiMnqo=
|
||||||
golang.design/x/runtime v0.3.0/go.mod h1:PjVKQjImLdSrvjIw4FmrnCPq9BGs5PMBHb8i6b2BN9I=
|
golang.design/x/thread v0.3.2/go.mod h1:6+Hi2rMOgMHZdKDWaqNHyWtoFUx1HxZ06LfHPh5Z/hQ=
|
||||||
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.ComponentDebugf("MetricCache", "COLLECT %s COND '%s'", f.Name, f.Condition)
|
cclog.ComponentDebug("MetricCache", "COLLECT", f.Name, "COND", 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.ComponentErrorf("MetricCache", "COLLECT %s COND '%s' : %s", f.Name, f.Condition, err.Error())
|
cclog.ComponentError("MetricCache", "COLLECT", f.Name, "COND", 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.ComponentErrorf("MetricCache", "COLLECT ADD VALUE %v FAILED", v)
|
cclog.ComponentError("MetricCache", "COLLECT ADD VALUE", v, "FAILED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.ComponentDebugf("MetricCache", "EVALUATE %s METRICS %d CALC '%s'", f.Name, len_values, f.Function)
|
cclog.ComponentDebug("MetricCache", "EVALUATE", f.Name, "METRICS", len_values, "CALC", 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.ComponentErrorf("MetricCache", "EVALUATE %s METRICS %d CALC '%s': %s", f.Name, len_values, f.Function, err.Error())
|
cclog.ComponentError("MetricCache", "EVALUATE", f.Name, "METRICS", len_values, "CALC", 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.ComponentErrorf("MetricCache", "Gval returned invalid type %s skipping metric %s", t, f.Name)
|
cclog.ComponentError("MetricCache", "Gval returned invalid type", t, "skipping metric", f.Name)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.ComponentErrorf("MetricCache", "Cannot create metric from Gval result %v: %s", value, err.Error())
|
cclog.ComponentError("MetricCache", "Cannot create metric from Gval result", value, ":", err.Error())
|
||||||
}
|
}
|
||||||
cclog.ComponentDebugf("MetricCache", "SEND %s", m.ToLineProtocol(nil))
|
cclog.ComponentDebug("MetricCache", "SEND", m)
|
||||||
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.ComponentErrorf("MetricAggregator", "Cannot add aggregation, invalid if condition '%s': %s", newcond, err.Error())
|
cclog.ComponentError("MetricAggregator", "Cannot add aggregation, invalid if condition", 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.ComponentErrorf("MetricAggregator", "Cannot add aggregation, invalid function condition %s: %s", newfunc, err.Error())
|
cclog.ComponentError("MetricAggregator", "Cannot add aggregation, invalid function condition", newfunc, ":", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, agg := range c.functions {
|
for _, agg := range c.functions {
|
||||||
|
|||||||
Reference in New Issue
Block a user