From 7a83a4ade3d06c039e8b1d24439675ce92269b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Wed, 2 Oct 2024 10:03:21 +0200 Subject: [PATCH 01/10] chore(pkgs): dep on prometheus-client for agent --- CHANGELOG.md | 4 +++- pyproject.toml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a52f4f..0a747423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Convert `[cache]` > `password` agent parameter from string to password type. - Convert `[ldap]` > `bind_password` gateway parameter from string to password type. -- pkgs: Add requirement on RFL.settings and RFL.core >= 1.1.0. +- pkgs: + - Add requirement on RFL.settings and RFL.core >= 1.1.0. + - Add dependency on prometheus-client for the agent. ### Fixed - agent: diff --git a/pyproject.toml b/pyproject.toml index 854e0823..5acbe81b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dev = [ agent = [ "RacksDB[web]", "redis", + "prometheus-client", ] tests = [ "pytest", From 955b7b1c29ec0595f51cb0207bda2357bbc14a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Tue, 22 Oct 2024 18:10:08 +0200 Subject: [PATCH 02/10] feat(conf): agent metrics>enabled parameter This is a boolean parameter to control if metrics feature and integration with Prometheus (or compatible) is enabled on the agent. This is disabled by default. --- CHANGELOG.md | 4 +++- conf/vendor/agent.yml | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a747423..922166cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - frontend: - Request RacksDB with the infrastructure name provided by the gateway (#348). - Display time limit of running jobs in job details page (#352). -- conf: Add `racksdb` > `infrastructure` parameter for the agent. +- conf: + - Add `racksdb` > `infrastructure` parameter for the agent. + - Add `metrics` > `enabled` parameter for the agent. - show-conf: Introduce `slurm-web-show-conf` utility to dump current configuration settings of gateway and agent components with their origin, which can either be configuration definition file or site override (#349). diff --git a/conf/vendor/agent.yml b/conf/vendor/agent.yml index 6220d930..b82dc2c8 100644 --- a/conf/vendor/agent.yml +++ b/conf/vendor/agent.yml @@ -354,3 +354,11 @@ cache: type: int default: 60 doc: Expiration delay in seconds for accounts in cache + +metrics: + enabled: + type: bool + default: false + doc: | + Determine if metrics feature and integration with Prometheus (or + compatible) is enabled. From 93ed2343b38381c5707d0f012c4476a56b08867a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Wed, 2 Oct 2024 10:03:52 +0200 Subject: [PATCH 03/10] feat(agent): /metrics endpoint Add optional /metrics endpoint with various Slurm metrics in OpenMetrics format designed to be scraped by Prometheus or compatible. fix #274 --- CHANGELOG.md | 7 +- slurmweb/apps/agent.py | 12 ++++ slurmweb/metrics.py | 111 ++++++++++++++++++++++++++++++++ slurmweb/slurmrestd/__init__.py | 69 ++++++++++++++++++++ 4 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 slurmweb/metrics.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 922166cc..4ecdf3da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] ### Added -- agent: Return RacksDB infrastructure name in `/info` endpoint in complement of - the cluster name. +- agent: + - Return RacksDB infrastructure name in `/info` endpoint in complement of + the cluster name. + - Add optional `/metrics` endpoint with various Slurm metrics in OpenMetrics + format designed to be scraped by Prometheus or compatible (#274). - gateway: Return RacksDB infrastructure name of every clusters in `/clusters` endpoint. - frontend: diff --git a/slurmweb/apps/agent.py b/slurmweb/apps/agent.py index 859b3da4..82ffcb05 100644 --- a/slurmweb/apps/agent.py +++ b/slurmweb/apps/agent.py @@ -10,6 +10,7 @@ from rfl.web.tokens import RFLTokenizedRBACWebApp from racksdb.errors import RacksDBSchemaError, RacksDBFormatError from racksdb.web.app import RacksDBWebBlueprint +from werkzeug.middleware import dispatcher from . import SlurmwebWebApp from ..version import get_version @@ -104,3 +105,14 @@ def __init__(self, seed): # Default RacksDB infrastructure is the cluster name. if self.settings.racksdb.infrastructure is None: self.settings.racksdb.infrastructure = self.settings.service.cluster + + self.metrics = None + if self.settings.metrics.enabled: + # Lazy load metrics module to avoid failing on missing optional external + # dependency when feature is actually disabled. + from ..metrics import SlurmWebMetricsCollector, make_wsgi_app + + self.metrics = SlurmWebMetricsCollector(self.slurmrestd) + self.wsgi_app = dispatcher.DispatcherMiddleware( + self.wsgi_app, {"/metrics": make_wsgi_app()} + ) diff --git a/slurmweb/metrics.py b/slurmweb/metrics.py new file mode 100644 index 00000000..576d813d --- /dev/null +++ b/slurmweb/metrics.py @@ -0,0 +1,111 @@ +# Copyright (c) 2024 Rackslab +# +# This file is part of Slurm-web. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import logging + +import prometheus_client +import prometheus_client.core + +from .errors import SlurmwebCacheError +from .slurmrestd.errors import ( + SlurmrestdNotFoundError, + SlurmrestdInvalidResponseError, + SlurmrestConnectionError, + SlurmrestdInternalError, +) + +logger = logging.getLogger(__name__) + + +class SlurmWebMetricsCollector(prometheus_client.registry.Collector): + def __init__(self, slurmrestd): + self.slurmrestd = slurmrestd + self.register() + + def describe(self): + """This method is defined to avoid the registry call collect() and request + slurmrestd eventually when the collector is registered to get a description of + all metrics exported by this collector. Just return an empty list to avoid + redundant code with _collect() method.""" + return [] + + def register(self): + prometheus_client.REGISTRY.register(self) + # Unregister all standard built-ins collectors. + for collector in ( + prometheus_client.GC_COLLECTOR, + prometheus_client.PLATFORM_COLLECTOR, + prometheus_client.PROCESS_COLLECTOR, + ): + try: + prometheus_client.REGISTRY.unregister(collector) + except KeyError: + # Ignore if collector has not been found in registry + pass + + def unregister(self): + prometheus_client.REGISTRY.unregister(self) + + def _collect(self): + (nodes_states, cores_states, nodes_total, cores_total) = ( + self.slurmrestd.nodes_cores_states() + ) + c = prometheus_client.core.GaugeMetricFamily( + "slurm_nodes", "Slurm nodes", labels=["state"] + ) + for status, value in nodes_states.items(): + c.add_metric([status], value) + yield c + yield prometheus_client.metrics_core.GaugeMetricFamily( + "slurm_nodes_total", "Slurm total number of nodes", value=nodes_total + ) + c = prometheus_client.core.GaugeMetricFamily( + "slurm_cores", "Slurm cores", labels=["state"] + ) + for status, value in cores_states.items(): + c.add_metric([status], value) + yield c + yield prometheus_client.metrics_core.GaugeMetricFamily( + "slurm_cores_total", "Slurm total number of cores", value=cores_total + ) + + (jobs_states, jobs_total) = self.slurmrestd.jobs_states() + c = prometheus_client.core.GaugeMetricFamily( + "slurm_jobs", "Slurm jobs", labels=["state"] + ) + for status, value in jobs_states.items(): + c.add_metric([status], value) + yield c + yield prometheus_client.core.GaugeMetricFamily( + "slurm_jobs_total", "Slurm total number of jobs", value=jobs_total + ) + + def collect(self): + try: + yield from self._collect() + except SlurmrestdNotFoundError as err: + logger.error( + "Unable to collect metrics due to URL not found on slurmrestd: %s", err + ) + except SlurmrestdInvalidResponseError as err: + logger.error( + "Unable to collect metrics due to slurmrestd invalid response: %s", err + ) + except SlurmrestConnectionError as err: + logger.error( + "Unable to collect metrics due to slurmrestd connection error: %s", err + ) + except SlurmrestdInternalError as err: + logger.error( + "Unable to collect metrics due to slurmrestd internal error: %s (%s)", + err.description, + err.source, + ) + except SlurmwebCacheError as err: + logger.error("Unable to collect metrics due to cache error: %s", err) + +def make_wsgi_app(): + return prometheus_client.make_wsgi_app() diff --git a/slurmweb/slurmrestd/__init__.py b/slurmweb/slurmrestd/__init__.py index 1801149e..d6b37b71 100644 --- a/slurmweb/slurmrestd/__init__.py +++ b/slurmweb/slurmrestd/__init__.py @@ -95,6 +95,32 @@ def version(self, **kwargs): def jobs(self, **kwargs): return self._request(f"/slurm/v{self.api_version}/jobs", "jobs", **kwargs) + def jobs_states(self): + jobs = { + "running": 0, + "completed": 0, + "completing": 0, + "cancelled": 0, + "pending": 0, + "unknown": 0, + } + total = 0 + for job in self.jobs(): + if job["job_state"] == "RUNNING": + jobs["running"] += 1 + elif job["job_state"] == "COMPLETED": + jobs["completed"] += 1 + elif job["job_state"] == "COMPLETING": + jobs["completing"] += 1 + elif job["job_state"] == "CANCELLED": + jobs["cancelled"] += 1 + elif job["job_state"] == "PENDING": + jobs["pending"] += 1 + else: + jobs["unknown"] += 1 + total += 1 + return jobs, total + def _ctldjob(self, job_id: int, **kwargs): return self._request( f"/slurm/v{self.api_version}/job/{job_id}", "jobs", **kwargs @@ -108,6 +134,49 @@ def _acctjob(self, job_id: int, **kwargs): def nodes(self, **kwargs): return self._request(f"/slurm/v{self.api_version}/nodes", "nodes", **kwargs) + def nodes_cores_states(self): + nodes_states = { + "idle": 0, + "mixed": 0, + "allocated": 0, + "down": 0, + "drain": 0, + "unknown": 0, + } + cores_states = { + "idle": 0, + "mixed": 0, + "allocated": 0, + "down": 0, + "drain": 0, + "unknown": 0, + } + nodes_total = 0 + cores_total = 0 + for node in self.nodes(): + cores = node["cpus"] + if "MIXED" in node["state"]: + nodes_states["mixed"] += 1 + cores_states["mixed"] += cores + elif "ALLOCATED" in node["state"]: + nodes_states["allocated"] += 1 + cores_states["allocated"] += cores + elif "DOWN" in node["state"]: + nodes_states["down"] += 1 + cores_states["down"] += cores + elif "DRAIN" in node["state"]: + nodes_states["drain"] += 1 + cores_states["drain"] += cores + elif "IDLE" in node["state"]: + nodes_states["idle"] += 1 + cores_states["idle"] += cores + else: + nodes_states["unknown"] += 1 + cores_states["unknown"] += cores + nodes_total += 1 + cores_total += cores + return nodes_states, cores_states, nodes_total, cores_total + def node(self, node_name: str, **kwargs): try: return self._request( From 1bd57c48930163f08f1aaeca0c0db0e95eb6c0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Tue, 22 Oct 2024 18:12:43 +0200 Subject: [PATCH 04/10] chore(dev): enable metrics feature --- dev/conf/agent.ini.j2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/conf/agent.ini.j2 b/dev/conf/agent.ini.j2 index ce63dc82..0d6fa775 100644 --- a/dev/conf/agent.ini.j2 +++ b/dev/conf/agent.ini.j2 @@ -26,3 +26,6 @@ infrastructure={{ infrastructure }} enabled={{ cache_enabled }} port={{ redis_port }} password={{ redis_password }} + +[metrics] +enabled=yes From d84cc91bf06d7b3f87ab6ae608559bfd7c78fe2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Wed, 23 Oct 2024 13:36:46 +0200 Subject: [PATCH 05/10] feat(conf): metrics>restrict agent parameter --- CHANGELOG.md | 1 + conf/vendor/agent.yml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ecdf3da..75cd1a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - conf: - Add `racksdb` > `infrastructure` parameter for the agent. - Add `metrics` > `enabled` parameter for the agent. + - Add `metrics` > `restrict` parameter for the agent. - show-conf: Introduce `slurm-web-show-conf` utility to dump current configuration settings of gateway and agent components with their origin, which can either be configuration definition file or site override (#349). diff --git a/conf/vendor/agent.yml b/conf/vendor/agent.yml index b82dc2c8..611a5898 100644 --- a/conf/vendor/agent.yml +++ b/conf/vendor/agent.yml @@ -362,3 +362,10 @@ metrics: doc: | Determine if metrics feature and integration with Prometheus (or compatible) is enabled. + restrict: + type: list + content: network + default: + - 127.0.0.0/24 + doc: | + Restricted list of IP networks permitted to request metrics. From 3493432dabf630e493397168acbea8ff9c7134b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Wed, 23 Oct 2024 13:37:20 +0200 Subject: [PATCH 06/10] feat(agent): client ip filtering on metrics Add IP address of client filter mechanism in metrics endpoint. --- slurmweb/apps/agent.py | 2 +- slurmweb/metrics.py | 48 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/slurmweb/apps/agent.py b/slurmweb/apps/agent.py index 82ffcb05..7642aff7 100644 --- a/slurmweb/apps/agent.py +++ b/slurmweb/apps/agent.py @@ -114,5 +114,5 @@ def __init__(self, seed): self.metrics = SlurmWebMetricsCollector(self.slurmrestd) self.wsgi_app = dispatcher.DispatcherMiddleware( - self.wsgi_app, {"/metrics": make_wsgi_app()} + self.wsgi_app, {"/metrics": make_wsgi_app(self.settings.metrics)} ) diff --git a/slurmweb/metrics.py b/slurmweb/metrics.py index 576d813d..1fd1a856 100644 --- a/slurmweb/metrics.py +++ b/slurmweb/metrics.py @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import typing as t +import ipaddress import logging import prometheus_client @@ -17,6 +19,9 @@ SlurmrestdInternalError, ) +if t.TYPE_CHECKING: + from rfl.settings import RuntimeSettings + logger = logging.getLogger(__name__) @@ -107,5 +112,44 @@ def collect(self): except SlurmwebCacheError as err: logger.error("Unable to collect metrics due to cache error: %s", err) -def make_wsgi_app(): - return prometheus_client.make_wsgi_app() + +def get_client_ipaddress(environ): + """Return IP address of the client as found in request environment.""" + # To properly handle setup in which agent is behind a reverse proxy, first try to + # use X-Forwarded-For header if defined. In this header, the original client IP + # address the leftmost address in a comma (and optionally whitespaces) separated + # list of addresses, followed by the addresses of the intermediate proxies. If + # X-Forwarded-For is not defined, use REMOTE_ADDR environment key as fallback. + try: + ip = environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip() + except KeyError: + ip = environ["REMOTE_ADDR"] + return ipaddress.ip_address(ip) + + +def make_wsgi_app(settings: "RuntimeSettings"): + prometheus_app = prometheus_client.make_wsgi_app() + + def slurmweb_metrics_app(environ, start_response): + + # Check if client IP address is member of restricted networks list. If + # not, send response with HTTP/403 status code. + ip = get_client_ipaddress(environ) + permitted = False + for restricted_network in settings.restrict: + if ip in restricted_network: + permitted = True + break + if not permitted: + status = "403 Forbidden" + headers = [("", "")] + output = f"IP address {ip} not authorized to request metrics" + logger.warning(output) + start_response(status, headers) + return [(output + "\n").encode()] + + # Client IP address is authorized, return metrics. + logger.debug("IP address %s authorized to request metrics", ip) + return prometheus_app(environ, start_response) + + return slurmweb_metrics_app From d449c2c730964735db48a9e7e1b0cc6d09ba5a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Wed, 23 Oct 2024 15:05:26 +0200 Subject: [PATCH 07/10] chore(pkg): bump RFL.settings requirement v1.1.1 This new version has a fix required to load ip and network default values. --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75cd1a3f..5b7aff48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Convert `[ldap]` > `bind_password` gateway parameter from string to password type. - pkgs: - - Add requirement on RFL.settings and RFL.core >= 1.1.0. + - Add requirement on RFL.core >= 1.1.0. + - Add requirement on RFL.settings >= 1.1.1. - Add dependency on prometheus-client for the agent. ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 5acbe81b..6dec4d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "RFL.authentication >= 1.0.3", "RFL.core >= 1.1.0", "RFL.log", - "RFL.settings >= 1.1.0", + "RFL.settings >= 1.1.1", "RFL.web", "setuptools" ] From f81c9480b37c0bfa2f3d8b578ce4832f22a52354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Wed, 23 Oct 2024 17:16:32 +0200 Subject: [PATCH 08/10] tests(agent): cover metrics module --- slurmweb/tests/test_agent.py | 204 +++++++++++++++++++++++++++++- slurmweb/tests/test_slurmrestd.py | 46 +++++++ 2 files changed, 244 insertions(+), 6 deletions(-) diff --git a/slurmweb/tests/test_agent.py b/slurmweb/tests/test_agent.py index 69e79a60..572cbeec 100644 --- a/slurmweb/tests/test_agent.py +++ b/slurmweb/tests/test_agent.py @@ -9,17 +9,23 @@ from unittest import mock import tempfile import os +import textwrap +import ipaddress from flask import Blueprint from rfl.authentication.user import AuthenticatedUser +from prometheus_client.parser import text_string_to_metric_families from slurmweb.version import get_version from slurmweb.apps import SlurmwebConfSeed from slurmweb.apps.agent import SlurmwebAppAgent from slurmweb.slurmrestd.errors import ( SlurmrestConnectionError, + SlurmrestdNotFoundError, SlurmrestdInvalidResponseError, + SlurmrestdInternalError, ) +from slurmweb.errors import SlurmwebCacheError from .utils import ( all_slurm_versions, @@ -48,8 +54,9 @@ def __init__(self, **kwargs): super().__init__("Fake RacksDB web blueprint", __name__) -class TestAgent(unittest.TestCase): - def setUp(self): +class TestAgentBase(unittest.TestCase): + + def setup_client(self, additional_conf=None): # Generate JWT signing key key = tempfile.NamedTemporaryFile(mode="w+") key.write("hey") @@ -67,7 +74,12 @@ def setUp(self): # Generate configuration file conf = tempfile.NamedTemporaryFile(mode="w+") - conf.write(CONF.format(key=key.name, policy_defs=policy_defs, policy=policy)) + conf_content = CONF + if additional_conf is not None: + conf_content += additional_conf + conf.write( + conf_content.format(key=key.name, policy_defs=policy_defs, policy=policy) + ) conf.seek(0) # Configuration definition path @@ -104,13 +116,19 @@ def setUp(self): self.client = self.app.test_client() self.client.environ_base["HTTP_AUTHORIZATION"] = "Bearer " + token + def mock_slurmrestd_responses(self, slurm_version, assets): + return mock_slurmrestd_responses(self.app.slurmrestd, slurm_version, assets) + + +class TestAgent(TestAgentBase): + + def setUp(self): + self.setup_client() + # # Generic routes (without slurmrestd requests) # - def mock_slurmrestd_responses(self, slurm_version, assets): - return mock_slurmrestd_responses(self.app.slurmrestd, slurm_version, assets) - def test_version(self): response = self.client.get("/version") self.assertEqual(response.status_code, 200) @@ -566,3 +584,177 @@ def test_request_accounts(self, slurm_version): self.assertEqual(len(response.json), len(accounts_asset)) for idx in range(len(response.json)): self.assertEqual(response.json[idx]["name"], accounts_asset[idx]["name"]) + + def test_request_metrics(self): + # Metrics feature is disabled in this test case, check that the corresponding + # endpoint returns HTTP/404 (not found). + response = self.client.get("/metrics") + self.assertEqual(response.status_code, 404) + + +class TestAgentMetrics(TestAgentBase): + + def setUp(self): + self.setup_client( + additional_conf=textwrap.dedent( + """ + + [metrics] + enabled=yes + """ + ) + ) + + def tearDown(self): + self.app.metrics.unregister() + + @all_slurm_versions + def test_request_metrics(self, slurm_version): + try: + [nodes_asset, jobs_asset] = self.mock_slurmrestd_responses( + slurm_version, + [("slurm-nodes", "nodes"), ("slurm-jobs", "jobs")], + ) + except SlurmwebAssetUnavailable: + return + response = self.client.get("/metrics") + self.assertEqual(response.status_code, 200) + families = list(text_string_to_metric_families(response.text)) + # Check expected metrics are present + metrics_names = [family.name for family in families] + self.assertCountEqual( + [ + "slurm_nodes", + "slurm_nodes_total", + "slurm_cores", + "slurm_cores_total", + "slurm_jobs", + "slurm_jobs_total", + ], + metrics_names, + ) + # Check some values against assets + for family in families: + if family.name == "slurm_nodes_total": + self.assertEqual(family.samples[0].value, len(nodes_asset)) + if family.name == "slurm_jobs_total": + self.assertEqual(family.samples[0].value, len(jobs_asset)) + + def test_request_metrics_forbidden(self): + # Change restricted list of network allowed to request metrics + self.app.settings.metrics.restrict = [ipaddress.ip_network("192.168.1.0/24")] + with self.assertLogs("slurmweb", level="WARNING") as cm: + response = self.client.get("/metrics") + + # Check HTTP/403 is returned with text message. Also check warning message is + # emitted in logs. + self.assertEqual(response.status_code, 403) + self.assertEqual( + response.text, "IP address 127.0.0.1 not authorized to request metrics\n" + ) + self.assertEqual( + cm.output, + [ + "WARNING:slurmweb.metrics:IP address 127.0.0.1 not authorized to " + "request metrics" + ], + ) + + def test_request_metrics_slurmrest_connection_error(self): + self.app.slurmrestd._request = mock.Mock( + side_effect=SlurmrestConnectionError("connection error") + ) + with self.assertLogs("slurmweb", level="ERROR") as cm: + response = self.client.get("/metrics") + # In case of connection error with slurmrestd, metrics WSGI application returns + # HTTP/200 empty response. Check error message is emitted in logs. + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "") + self.assertEqual( + cm.output, + [ + "ERROR:slurmweb.metrics:Unable to collect metrics due to slurmrestd " + "connection error: connection error" + ], + ) + + def test_request_metrics_slurmrestd_invalid_type(self): + self.app.slurmrestd._request = mock.Mock( + side_effect=SlurmrestdInvalidResponseError("invalid type") + ) + with self.assertLogs("slurmweb", level="ERROR") as cm: + response = self.client.get("/metrics") + # In case of invalid response from slurmrestd, metrics WSGI application returns + # HTTP/200 empty response. Check error message is emitted in logs. + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "") + self.assertEqual( + cm.output, + [ + "ERROR:slurmweb.metrics:Unable to collect metrics due to slurmrestd " + "invalid response: invalid type" + ], + ) + + def test_request_metrics_slurmrestd_internal_error(self): + self.app.slurmrestd._request = mock.Mock( + side_effect=SlurmrestdInternalError( + "slurmrestd fake error", + -1, + "fake error description", + "fake error source", + ) + ) + with self.assertLogs("slurmweb", level="ERROR") as cm: + response = self.client.get("/metrics") + # In case of slurmrestd internal error, metrics WSGI application returns + # HTTP/200 empty response. Check error message is emitted in logs. + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "") + self.assertEqual( + cm.output, + [ + "ERROR:slurmweb.metrics:Unable to collect metrics due to slurmrestd " + "internal error: fake error description (fake error source)" + ], + ) + + @all_slurm_versions + def test_request_metrics_slurmrestd_not_found(self, slurm_version): + self.app.slurmrestd._request = mock.Mock( + side_effect=SlurmrestdNotFoundError("/unfound") + ) + with self.assertLogs("slurmweb", level="ERROR") as cm: + response = self.client.get("/metrics") + # In case of slurmrestd not found error, metrics WSGI application returns + # HTTP/200 empty response. Check error message is emitted in logs. + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "") + self.assertEqual( + cm.output, + [ + "ERROR:slurmweb.metrics:Unable to collect metrics due to URL not found " + "on slurmrestd: /unfound" + ], + ) + + @all_slurm_versions + def test_request_metrics_cache_error(self, slurm_version): + # Collector first calls slurmrestd.nodes() then trigger SlurmwebCacheError on + # this method call. + self.app.slurmrestd.nodes = mock.Mock( + side_effect=SlurmwebCacheError("fake error") + ) + with self.assertLogs("slurmweb", level="ERROR") as cm: + response = self.client.get("/metrics") + # In case of cache error, metrics WSGI application returns HTTP/200 empty + # response. Check error message is emitted in logs. + self.assertEqual(response.status_code, 200) + self.assertEqual(response.text, "") + self.assertEqual( + cm.output, + [ + "ERROR:slurmweb.metrics:Unable to collect metrics due to cache error: " + "fake error" + ], + ) diff --git a/slurmweb/tests/test_slurmrestd.py b/slurmweb/tests/test_slurmrestd.py index 5d8ee616..5fd132a0 100644 --- a/slurmweb/tests/test_slurmrestd.py +++ b/slurmweb/tests/test_slurmrestd.py @@ -137,6 +137,25 @@ def test_jobs(self, slurm_version): jobs = self.slurmrestd.jobs() self.assertCountEqual(jobs, asset) + @all_slurm_versions + def test_jobs_states(self, slurm_version): + try: + [asset] = self.mock_slurmrestd_responses( + slurm_version, [("slurm-jobs", "jobs")] + ) + except SlurmwebAssetUnavailable: + return + + jobs, total = self.slurmrestd.jobs_states() + # Check total value matches the number of jobs in asset + self.assertEqual(total, len(asset)) + + # Check sum of jobs states matches the total number of jobs + jobs_sum = 0 + for value in jobs.values(): + jobs_sum += value + self.assertEqual(total, jobs_sum) + @all_slurm_versions def test_nodes(self, slurm_version): try: @@ -149,6 +168,33 @@ def test_nodes(self, slurm_version): nodes = self.slurmrestd.nodes() self.assertCountEqual(nodes, asset) + @all_slurm_versions + def test_nodes_cores_states(self, slurm_version): + try: + [asset] = self.mock_slurmrestd_responses( + slurm_version, [("slurm-nodes", "nodes")] + ) + except SlurmwebAssetUnavailable: + return + + nodes_states, cores_states, nodes_total, cores_total = ( + self.slurmrestd.nodes_cores_states() + ) + # Check total number of nodes matches the number of nodes in asset + self.assertEqual(nodes_total, len(asset)) + + # Check sum of nodes states matches the total number of nodes + nodes_sum = 0 + for value in nodes_states.values(): + nodes_sum += value + self.assertEqual(nodes_total, nodes_sum) + + # Check sum of cores states matches the total number of cores + cores_sum = 0 + for value in cores_states.values(): + cores_sum += value + self.assertEqual(cores_total, cores_sum) + @all_slurm_versions def test_node(self, slurm_version): # We can use slurm-node-allocated asset for this test. From e14edbdb06db97e327ed2cb1003b697dbdcba892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Thu, 24 Oct 2024 10:59:42 +0200 Subject: [PATCH 09/10] docs: update conf references --- docs/modules/conf/examples/agent.ini | 13 +++++++ docs/modules/conf/partials/conf-agent.adoc | 41 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/docs/modules/conf/examples/agent.ini b/docs/modules/conf/examples/agent.ini index 21bb5715..314677a7 100644 --- a/docs/modules/conf/examples/agent.ini +++ b/docs/modules/conf/examples/agent.ini @@ -457,3 +457,16 @@ reservations=60 # # Default value: 60 accounts=60 + +[metrics] + +# Determine if metrics feature and integration with Prometheus (or +# compatible) is enabled. +enabled=no + +# Restricted list of IP networks permitted to request metrics. +# +# Default value: +# - 127.0.0.0/24 +restrict= + 127.0.0.0/24 diff --git a/docs/modules/conf/partials/conf-agent.adoc b/docs/modules/conf/partials/conf-agent.adoc index bfbaecc1..c3c6ab73 100644 --- a/docs/modules/conf/partials/conf-agent.adoc +++ b/docs/modules/conf/partials/conf-agent.adoc @@ -839,3 +839,44 @@ _No default value_ |=== + +== `metrics` + +[cols="2l,1,5a,^1"] +|=== +|Parameter|Type|Description|Required + + +|enabled +|bool +|Determine if metrics feature and integration with Prometheus (or +compatible) is enabled. + + + + + +*Default:* `False` + +|- + +|restrict +|list[network] +|Restricted list of IP networks permitted to request metrics. + + + + + +*Default:* + + +* `127.0.0.0/24` + + +|- + + +|=== + + From ba21b34f3aa24cda3cbd292f5acad92a3f99012f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Palancher?= Date: Thu, 24 Oct 2024 14:48:01 +0200 Subject: [PATCH 10/10] docs: cover metrics export feature - Add metrics export configuration documentation. - Mention metrics export optional feature in quickstart guide. - Mention metrics export feature in overview page. - Mention possible Prometheus integration in architecture page. --- CHANGELOG.md | 7 +- docs/modules/conf/nav.adoc | 1 + docs/modules/conf/pages/metrics.adoc | 121 +++ docs/modules/install/pages/quickstart.adoc | 22 + .../images/arch/slurm-web_integration.png | Bin 31274 -> 34982 bytes .../images/arch/slurm-web_integration.svg | 737 +++++++----------- .../overview/images/slurm-web_metrics.png | Bin 0 -> 8901 bytes .../overview/images/slurm-web_metrics.svg | 175 +++++ docs/modules/overview/pages/architecture.adoc | 8 +- docs/modules/overview/pages/overview.adoc | 17 + docs/utils/build.yaml | 1 + 11 files changed, 623 insertions(+), 466 deletions(-) create mode 100644 docs/modules/conf/pages/metrics.adoc create mode 100644 docs/modules/overview/images/slurm-web_metrics.png create mode 100644 docs/modules/overview/images/slurm-web_metrics.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7aff48..71dc8be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - show-conf: Introduce `slurm-web-show-conf` utility to dump current configuration settings of gateway and agent components with their origin, which can either be configuration definition file or site override (#349). -- docs: Add manpage for `slurm-web-show-conf` command. +- docs: + - Add manpage for `slurm-web-show-conf` command. + - Add metrics export configuration documentation. + - Mention metrics export optional feature in quickstart guide. + - Mention metrics export feature in overview page. + - Mention possible Prometheus integration in architecture page. ### Changed - docs: Update configuration reference documentation. diff --git a/docs/modules/conf/nav.adoc b/docs/modules/conf/nav.adoc index 98a6965d..84b9087c 100644 --- a/docs/modules/conf/nav.adoc +++ b/docs/modules/conf/nav.adoc @@ -6,3 +6,4 @@ ** xref:conf/gateway.adoc[Gateway] ** xref:conf/agent.adoc[Agent] * xref:policy.adoc[] +* xref:metrics.adoc[] diff --git a/docs/modules/conf/pages/metrics.adoc b/docs/modules/conf/pages/metrics.adoc new file mode 100644 index 00000000..5300c917 --- /dev/null +++ b/docs/modules/conf/pages/metrics.adoc @@ -0,0 +1,121 @@ += Metrics Export + +Slurm-web agent can export metrics in standard OpenMetrics format on `/metrics` +endpoint. This is designed to be scraped by Prometheus (or compatible) in order +to store metrics in timeseries databases and draw diagrams of historical data. + +This page explains how to enable and secure this feature by +<> to specific hosts and +<> to scrap these metrics. It also provides a +<>. + +== Configuration + +The metrics export feature is disabled by default. It can be enabled with the +following lines in [.path]#`/etc/slurm-web/agent.ini`#: + +[source,ini] +---- +[metrics] +enabled=yes +---- + +.More details +**** +* xref:conf/agent.adoc#_metrics[Agent configuration metrics section reference documentation]. +**** + +[#restrict] +== Host Restriction + +For security reasons, Slurm-web agent restrict access to `/metrics` endpoint to +localhost only. When Prometheus is running on external hosts, you must define +`restrict` parameter in [.path]#`/etc/slurm-web/agent.ini`# to allow other +networks explicitely. For example: + +[source,ini] +---- +[metrics] +enabled=yes +restrict= + 192.168.1.0/24 + 10.0.0.251/32 +---- + +In this example, all IP addresses in range `192.168.1.[0-254]` and `10.0.0.251` +are permitted to request metrics. + +.More details +**** +* xref:conf/agent.adoc#_metrics[Agent configuration reference documentation for metrics section]. +**** + +[#prometheus] +== Prometheus Integration + +Prometheus must be configured to request `/metrics` endpoint of Slurm-web agent. +Edit [.path]#`/etc/prometheus/prometheus.yml`# to add one of the following +configuration snippets depending of your setup: + +* Slurm-web agent running as native service (ie. with +`slurm-web-agent.service`): + +[source,yaml] +---- +scrape_configs: + - job_name: slurm + scrape_interval: 30s + static_configs: + - targets: ['localhost:5012'] +---- + +* Slurm-web agent running on xref:wsgi/index.adoc[production HTTP server]: + +[source,yaml] +---- +scrape_configs: + - job_name: slurm + scrape_interval: 30s + metrics_path: /agent/metrics + static_configs: + - targets: ['localhost:80'] +---- + +NOTE: You may need to adjust the target hostname, typically if Prometheus is +running on a remote host, and destination port (for example 443 for HTTPS). + +.Reference +**** +* https://prometheus.io/docs/prometheus/latest/configuration/configuration/[Prometheus Official Configuration Documentation]. +**** + +[#reference] +== Available Metrics + +This table describes all metrics exported by Slurm-web: + +[cols="1l,3a"] +|=== +|Metric|Description + +|slurm_nodes[state] +|Number of compute nodes in a given state. Supported states are: _idle_, +_mixed_, _allocated_, _down_, _drain_ and _unknown_. + +|slurm_nodes_total +|Total number of compute nodes managed by Slurm. + +|slurm_cores[state] +|Number of cores of compute nodes in a given state. Supported states are: +_idle_, _mixed_, _allocated_, _down_, _drain_ and _unknown_. + +|slurm_cores_total +|Total number of cores on compute nodes managed by Slurm. + +|slurm_jobs[state] +|Number of jobs in a given state in Slurm controller queue. Supported states +are: _running_, _completed_, _completing_, _cancelled_, _pending_ and _unknown_. + +|slurm_jobs_total +|Total number of jobs in Slurm controller queue. +|=== diff --git a/docs/modules/install/pages/quickstart.adoc b/docs/modules/install/pages/quickstart.adoc index 887db4d9..62dd8011 100644 --- a/docs/modules/install/pages/quickstart.adoc +++ b/docs/modules/install/pages/quickstart.adoc @@ -637,6 +637,28 @@ xref:misc:troubleshooting.adoc#wsgi[troubleshooting guide] for help. * xref:conf:wsgi/index.adoc[Production HTTP server setup guide]. **** +== Metrics (optional) + +Slurm-web offers the possibility to +xref:overview:overview.adoc#metrics[export Slurm metrics] in +https://openmetrics.io/[OpenMetrics] format and integrate with +https://prometheus.io/[Prometheus]. This feature can be used to store metrics in +timeseries databases and draw diagrams of historical data. + +This feature is disabled by default. It can be enabled with the following lines +in [.path]#`/etc/slurm-web/agent.ini`#: + +[source,ini] +---- +[metrics] +enabled=yes +---- + +.More details +**** +* xref:conf:metrics.adoc[Metrics export configuration documentation]. +**** + == Multi-clusters Slurm-web is designed to support diff --git a/docs/modules/overview/images/arch/slurm-web_integration.png b/docs/modules/overview/images/arch/slurm-web_integration.png index 45296b19784b798e03c8000f63b7439139b4f4de..98bc43c847555c19e4d0b9bf1b10ae860a7a3563 100644 GIT binary patch literal 34982 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 E1EW`nkpKVy literal 31274 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - slurmrestd - slurmrestd - slurmdbd - slurmdbd - frontend - frontend - - - - - - - - - gateway - gateway - - Slurm-web - Slurm-webSlurm - Slurm - - - - - - - - - - - - - LDAP directory - LDAP directory - - - - - - - - - - - - - Redis cache - Redis cache - Policy - Policy - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - RacksDB - RacksDB - - - - - - agent - - - - - - - - - - - - - - - - + id="path1035" />Prometheustimeseriesagent diff --git a/docs/modules/overview/images/slurm-web_metrics.png b/docs/modules/overview/images/slurm-web_metrics.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c2e42aca48e33d114f4894afc0da49d036bc0a GIT binary patch literal 8901 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 + + +Slurm-webSlurm diff --git a/docs/modules/overview/pages/architecture.adoc b/docs/modules/overview/pages/architecture.adoc index b093f4cd..d3e4c949 100644 --- a/docs/modules/overview/pages/architecture.adoc +++ b/docs/modules/overview/pages/architecture.adoc @@ -47,8 +47,12 @@ permissions associated to roles and LDAP groups. The component also extracts cluster racking topology from xref:racksdb:overview:start.adoc[RacksDB] database to generate xref:overview.adoc#nodes-status[graphical representation of nodes status] in the -racks. Finally, it connects to https://redis.io/[Redis] in-memory key/value -database to save cached data from Slurm. +racks. + +Optionally, the *agent* can connect to https://redis.io/[Redis] in-memory +key/value database to save cached data from Slurm. It can also +xref:overview.adoc#metrics[export metrics] to https://prometheus.io/[Prometheus] +(or compatible) in order to store values in timeseries databases. [#protocols] == Protocols diff --git a/docs/modules/overview/pages/overview.adoc b/docs/modules/overview/pages/overview.adoc index 4129591d..f716f4d3 100644 --- a/docs/modules/overview/pages/overview.adoc +++ b/docs/modules/overview/pages/overview.adoc @@ -125,3 +125,20 @@ image::slurm-web_transparent_cache.png[] Users are able to track jobs list in near real-time very efficiently. Finally drop the load generated by infinite loops of `squeue`! + +[#metrics] +== Metrics + +image::slurm-web_metrics.png[] + +Slurm-web can export many metrics of the clusters statuses and the jobs. These +metrics are exported in standard https://openmetrics.io/[OpenMetrics] format, +designed to be scraped by https://prometheus.io/[Prometheus] (or any compatible +solution) to store in timeseries databases. Diagrams of these metrics provide +historical views of your production. + +[sidebar] +-- +.More links +* xref:conf:metrics.adoc[Metrics export configuration documentation] +-- diff --git a/docs/utils/build.yaml b/docs/utils/build.yaml index 5561ec79..a10b67b2 100644 --- a/docs/utils/build.yaml +++ b/docs/utils/build.yaml @@ -3,6 +3,7 @@ # generated. diagrams: modules/overview/images/slurm-web_transparent_cache.svg: medium + modules/overview/images/slurm-web_metrics.svg: large modules/overview/images/arch/slurm-web_architecture.svg: medium modules/overview/images/arch/slurm-web_distribution.svg: medium modules/overview/images/arch/slurm-web_integration.svg: medium