Skip to content

Commit 21f121b

Browse files
authored
Resolve SLURM scontrol namespace PIDs (DataDog#24384)
* Resolve SLURM scontrol namespace PIDs * Add SLURM changelog entry * Track multiple SLURM host PID matches * Tag SLURM host and namespace PIDs * Refine SLURM PID match handling * Sync SLURM generated config example * Document SLURM PID match limitation * Fix SLURM namespace PID collision handling
1 parent 6e9d2c5 commit 21f121b

7 files changed

Lines changed: 264 additions & 15 deletions

File tree

slurm/assets/configuration/spec.yaml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,20 @@ files:
4949
example: True
5050
- name: collect_scontrol_stats
5151
description: |
52-
Whether or not to collect statistics from the scontrol command. This is mainly used in the worker
52+
Whether or not to collect statistics from the scontrol command. This is mainly used in the worker
5353
node to collect the list of running jobs along with their PIDs.
5454
fleet_configurable: true
5555
value:
5656
type: boolean
5757
example: False
58+
- name: resolve_scontrol_host_pids
59+
description: |
60+
Whether or not to resolve PIDs reported by scontrol through the host proc filesystem before collecting
61+
process tags. Enable this when scontrol reports namespace PIDs instead of host PIDs.
62+
fleet_configurable: true
63+
value:
64+
type: boolean
65+
example: False
5866
- name: collect_gpu_stats
5967
description: Whether or not to collect GPU statistics when Slurm is configured to use GPUs.
6068
fleet_configurable: true
@@ -70,8 +78,8 @@ files:
7078
- name: sinfo_collection_level
7179
description: |
7280
The level of detail to collect from the sinfo command. The default is 'basic'. Available options are 1, 2 and
73-
3. Level 1 collects data only for partitions. Level 2 collects data from individual nodes. Level 3
74-
collects data from from individual nodes as well but is more verbose and includes data such as CPU and
81+
3. Level 1 collects data only for partitions. Level 2 collects data from individual nodes. Level 3
82+
collects data from from individual nodes as well but is more verbose and includes data such as CPU and
7583
memory usage as reported from the OS, as well as additional tags.
7684
fleet_configurable: true
7785
value:
@@ -187,7 +195,7 @@ files:
187195
https://docs.datadoghq.com/developers/write_agent_check/#collection-interval
188196
189197
Most Slurm metrics are collected from calling the different binaries. Depending on the size of the Slurm cluster,
190-
this can be a very expensive operation. It is recommended to set this to a higher value than the default 15
198+
this can be a very expensive operation. It is recommended to set this to a higher value than the default 15
191199
seconds, but this can be adjusted based on the size of the cluster and the desired granularity of the metrics.
192200
min_collection_interval.value.display_default: 60
193201
min_collection_interval.value.default: 60
@@ -196,7 +204,7 @@ files:
196204
- template: logs
197205
example:
198206
- type: file
199-
path: /var/log/slurm/slurmd.log
207+
path: /var/log/slurm/slurmd.log
200208
source: slurm
201209
service: slurm
202210
- type: file

slurm/changelog.d/24384.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix process tag enrichment when SLURM scontrol reports namespace PIDs.

slurm/datadog_checks/slurm/check.py

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import subprocess
77
import time
8+
from dataclasses import dataclass
89
from datetime import timedelta
910

1011
from datadog_checks.base import AgentCheck, is_affirmative
@@ -51,6 +52,12 @@ def parse_duration(time_str):
5152
return None
5253

5354

55+
@dataclass
56+
class ProcessPidMatch:
57+
host_pid: str
58+
namespace_pids: list[str]
59+
60+
5461
class SlurmCheck(AgentCheck, ConfigMixin):
5562
# This will be the prefix of every metric and service check the integration sends
5663
__NAMESPACE__ = 'slurm'
@@ -70,6 +77,7 @@ def __init__(self, name, init_config, instances):
7077
self.collect_sacct_stats = is_affirmative(self.instance.get('collect_sacct_stats', True))
7178
self.collect_scontrol_stats = is_affirmative(self.instance.get('collect_scontrol_stats', False))
7279
self.collect_seff_stats = is_affirmative(self.instance.get('collect_seff_stats', False))
80+
self.resolve_scontrol_host_pids = is_affirmative(self.instance.get('resolve_scontrol_host_pids', False))
7381

7482
# Additional configurations
7583
self.gpu_stats = is_affirmative(self.instance.get('collect_gpu_stats', False))
@@ -589,6 +597,9 @@ def process_scontrol(self, output):
589597

590598
# Cache for job details to avoid duplicate calls
591599
job_details_cache = {}
600+
host_pid_matches_by_namespace_pid: dict[str, list[ProcessPidMatch]] = (
601+
self._get_host_pid_matches_by_namespace_pid() if self.resolve_scontrol_host_pids else {}
602+
)
592603

593604
for line in lines[1:]:
594605
tags = [f"slurm_node_name:{slurm_node.strip()}"]
@@ -597,14 +608,17 @@ def process_scontrol(self, output):
597608

598609
for header, value in zip(headers, fields):
599610
new_header = SCONTROL_TAG_MAPPING.get(header, f"slurm_{header.lower()}")
600-
tags.append(f"{new_header}:{value}")
601611

602612
if new_header == "pid":
603-
# Example gpu tags being returned:
604-
# ['gpu_vendor:nvidia', 'gpu_device:tesla_v100', 'gpu_uuid:gpu_xxxx...']
605-
pidtags = tagger.tag(f"process://{value}", tagger.ORCHESTRATOR)
606-
if pidtags: # Guard against tagger.tag returning None
607-
tags.extend(pidtags)
613+
host_pid_match = self._resolve_scontrol_host_pid(value, host_pid_matches_by_namespace_pid)
614+
tags.extend(self._get_process_tags(host_pid_match.host_pid))
615+
for namespace_pid in host_pid_match.namespace_pids:
616+
if namespace_pid == host_pid_match.host_pid:
617+
continue
618+
tags.append(f"nspid:{namespace_pid}")
619+
value = host_pid_match.host_pid
620+
621+
tags.append(f"{new_header}:{value}")
608622

609623
if header == "JOBID" and value.isdigit():
610624
job_id = value
@@ -617,6 +631,78 @@ def process_scontrol(self, output):
617631

618632
self.gauge("scontrol.jobs.info", 1, tags=tags + self.tags)
619633

634+
def _resolve_scontrol_host_pid(
635+
self, namespace_pid: str, host_pid_matches_by_namespace_pid: dict[str, list[ProcessPidMatch]]
636+
) -> ProcessPidMatch:
637+
host_pid_matches = host_pid_matches_by_namespace_pid.get(namespace_pid)
638+
if not host_pid_matches:
639+
return ProcessPidMatch(host_pid=namespace_pid, namespace_pids=[])
640+
641+
if len(host_pid_matches) > 1:
642+
namespace_match = host_pid_matches[0]
643+
for host_pid_match in host_pid_matches:
644+
# Prefer a translated namespace PID, but do not disambiguate by container yet.
645+
if namespace_pid != host_pid_match.host_pid and namespace_pid in host_pid_match.namespace_pids:
646+
namespace_match = host_pid_match
647+
break
648+
649+
matches = [
650+
{"host_pid": match.host_pid, "namespace_pids": match.namespace_pids} for match in host_pid_matches
651+
]
652+
self.log.debug(
653+
"Found multiple host PID matches for scontrol namespace PID %s: %s. Using host PID %s.",
654+
namespace_pid,
655+
matches,
656+
namespace_match.host_pid,
657+
)
658+
return namespace_match
659+
660+
return host_pid_matches[0]
661+
662+
def _get_process_tags(self, pid):
663+
# Example gpu tags being returned:
664+
# ['gpu_vendor:nvidia', 'gpu_device:tesla_v100', 'gpu_uuid:gpu_xxxx...']
665+
pidtags = tagger.tag(f"process://{pid}", tagger.ORCHESTRATOR)
666+
if pidtags: # Guard against tagger.tag returning None
667+
return pidtags
668+
669+
return []
670+
671+
def _get_host_pid_matches_by_namespace_pid(self):
672+
host_proc = os.environ.get("HOST_PROC", "/host/proc")
673+
host_pid_matches_by_namespace_pid = {}
674+
675+
try:
676+
proc_entries = os.scandir(host_proc)
677+
except OSError as e:
678+
self.log.debug("Unable to scan host proc path '%s': %s", host_proc, e)
679+
return host_pid_matches_by_namespace_pid
680+
681+
with proc_entries:
682+
for entry in proc_entries:
683+
if not entry.name.isdigit():
684+
continue
685+
686+
pid_match = ProcessPidMatch(
687+
host_pid=entry.name,
688+
namespace_pids=self._read_namespace_pids(entry.path, entry.name),
689+
)
690+
for namespace_pid in pid_match.namespace_pids:
691+
host_pid_matches_by_namespace_pid.setdefault(namespace_pid, []).append(pid_match)
692+
693+
return host_pid_matches_by_namespace_pid
694+
695+
def _read_namespace_pids(self, proc_path, host_pid):
696+
try:
697+
with open(os.path.join(proc_path, "status")) as status_file:
698+
for line in status_file:
699+
if line.startswith("NSpid:"):
700+
return line.split()[1:]
701+
except OSError as e:
702+
self.log.debug("Unable to read process status for PID %s: %s", host_pid, e)
703+
704+
return [host_pid]
705+
620706
def _enrich_scontrol_tags(self, job_id):
621707
# Tries to enrich the scontrol job with additional details from squeue.
622708
try:

slurm/datadog_checks/slurm/config_models/defaults.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ def instance_min_collection_interval():
8484
return 60
8585

8686

87+
def instance_resolve_scontrol_host_pids():
88+
return False
89+
90+
8791
def instance_sacct_path():
8892
return '/usr/bin/sacct'
8993

slurm/datadog_checks/slurm/config_models/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class InstanceConfig(BaseModel):
5858
enable_legacy_tags_normalization: Optional[bool] = None
5959
metric_patterns: Optional[MetricPatterns] = None
6060
min_collection_interval: Optional[float] = None
61+
resolve_scontrol_host_pids: Optional[bool] = None
6162
sacct_path: Optional[str] = None
6263
scontrol_path: Optional[str] = None
6364
sdiag_path: Optional[str] = None

slurm/datadog_checks/slurm/data/conf.yaml.example

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,17 @@ instances:
4646
# collect_sshare_stats: true
4747

4848
## @param collect_scontrol_stats - boolean - optional - default: false
49-
## Whether or not to collect statistics from the scontrol command. This is mainly used in the worker
49+
## Whether or not to collect statistics from the scontrol command. This is mainly used in the worker
5050
## node to collect the list of running jobs along with their PIDs.
5151
#
5252
# collect_scontrol_stats: false
5353

54+
## @param resolve_scontrol_host_pids - boolean - optional - default: false
55+
## Whether or not to resolve PIDs reported by scontrol through the host proc filesystem before collecting
56+
## process tags. Enable this when scontrol reports namespace PIDs instead of host PIDs.
57+
#
58+
# resolve_scontrol_host_pids: false
59+
5460
## @param collect_gpu_stats - boolean - optional - default: false
5561
## Whether or not to collect GPU statistics when Slurm is configured to use GPUs.
5662
#
@@ -63,8 +69,8 @@ instances:
6369

6470
## @param sinfo_collection_level - integer - optional - default: 1
6571
## The level of detail to collect from the sinfo command. The default is 'basic'. Available options are 1, 2 and
66-
## 3. Level 1 collects data only for partitions. Level 2 collects data from individual nodes. Level 3
67-
## collects data from from individual nodes as well but is more verbose and includes data such as CPU and
72+
## 3. Level 1 collects data only for partitions. Level 2 collects data from individual nodes. Level 3
73+
## collects data from from individual nodes as well but is more verbose and includes data such as CPU and
6874
## memory usage as reported from the OS, as well as additional tags.
6975
#
7076
# sinfo_collection_level: 1
@@ -125,7 +131,7 @@ instances:
125131
## https://docs.datadoghq.com/developers/write_agent_check/#collection-interval
126132
##
127133
## Most Slurm metrics are collected from calling the different binaries. Depending on the size of the Slurm cluster,
128-
## this can be a very expensive operation. It is recommended to set this to a higher value than the default 15
134+
## this can be a very expensive operation. It is recommended to set this to a higher value than the default 15
129135
## seconds, but this can be adjusted based on the size of the cluster and the desired granularity of the metrics.
130136
#
131137
min_collection_interval: 60

0 commit comments

Comments
 (0)