prometheus-dcache-exporter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. prometheus_dcache_exporter-0.1.0/LICENCE +13 -0
  2. prometheus_dcache_exporter-0.1.0/MANIFEST.in +2 -0
  3. prometheus_dcache_exporter-0.1.0/PKG-INFO +32 -0
  4. prometheus_dcache_exporter-0.1.0/pyproject.toml +99 -0
  5. prometheus_dcache_exporter-0.1.0/setup.cfg +4 -0
  6. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/__init__.py +13 -0
  7. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/__main__.py +437 -0
  8. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/_collector.py +638 -0
  9. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/_config.py +92 -0
  10. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/_dcache/__init__.py +38 -0
  11. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/_dcache/admin.py +199 -0
  12. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/_dcache/frontend.py +187 -0
  13. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter/_util.py +56 -0
  14. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter.egg-info/PKG-INFO +32 -0
  15. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter.egg-info/SOURCES.txt +17 -0
  16. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter.egg-info/dependency_links.txt +1 -0
  17. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter.egg-info/entry_points.txt +2 -0
  18. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter.egg-info/requires.txt +13 -0
  19. prometheus_dcache_exporter-0.1.0/src/prometheus_dcache_exporter.egg-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ Copyright © 2026 Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2
+
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU General Public License as published by the Free Software
6
+ Foundation, either version 3 of the License, or (at your option) any later
7
+ version.
8
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY
9
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10
+ PARTICULAR PURPOSE.
11
+ See the GNU General Public License for more details.
12
+ You should have received a copy of the GNU General Public License along with
13
+ this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,2 @@
1
+ exclude COPYING
2
+ prune debian
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: prometheus-dcache-exporter
3
+ Version: 0.1.0
4
+ Summary: A Prometheus exporter for dCache.
5
+ Author-email: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
6
+ Maintainer-email: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
7
+ Project-URL: website, https://gitlab.com/calestyo/prometheus-dcache-exporter
8
+ Keywords: monitoring,prometheus,prometheus exporter,dcache
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Environment :: No Input/Output (Daemon)
12
+ Classifier: Environment :: Plugins
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: System :: Monitoring
18
+ Classifier: Topic :: System :: Networking :: Monitoring
19
+ Requires-Python: >=3.13
20
+ License-File: LICENCE
21
+ Requires-Dist: certifi
22
+ Requires-Dist: httpx
23
+ Requires-Dist: humanfriendly
24
+ Requires-Dist: paramiko
25
+ Requires-Dist: prometheus-client
26
+ Requires-Dist: python-jsonpath
27
+ Requires-Dist: python-module-utils
28
+ Provides-Extra: extended-logging-formatter
29
+ Requires-Dist: python-logging-extras; extra == "extended-logging-formatter"
30
+ Provides-Extra: rich-text
31
+ Requires-Dist: rich; extra == "rich-text"
32
+ Dynamic: license-file
@@ -0,0 +1,99 @@
1
+ [project]
2
+ name = "prometheus-dcache-exporter"
3
+
4
+ description = "A Prometheus exporter for dCache."
5
+ keywords = [
6
+ "monitoring", "prometheus", "prometheus exporter",
7
+ "dcache"
8
+ ]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Environment :: No Input/Output (Daemon)",
13
+ "Environment :: Plugins",
14
+ "Intended Audience :: Information Technology",
15
+ "Intended Audience :: System Administrators",
16
+ "Natural Language :: English",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: System :: Monitoring",
19
+ "Topic :: System :: Networking :: Monitoring"
20
+ ]
21
+
22
+ authors = [
23
+ {name = "Christoph Anton Mitterer", email = "mail@christoph.anton.mitterer.name"}
24
+ ]
25
+ maintainers = [
26
+ {name = "Christoph Anton Mitterer", email = "mail@christoph.anton.mitterer.name"}
27
+ ]
28
+
29
+ license-files = ["LICENCE"]
30
+
31
+ requires-python = ">=3.13"
32
+ dependencies = [
33
+ "certifi",
34
+ "httpx",
35
+ "humanfriendly",
36
+ "paramiko",
37
+ "prometheus-client",
38
+ "python-jsonpath",
39
+ "python-module-utils"
40
+ ]
41
+
42
+ dynamic = ["version"]
43
+
44
+
45
+ [project.urls]
46
+ website = "https://gitlab.com/calestyo/prometheus-dcache-exporter"
47
+
48
+
49
+ [project.optional-dependencies]
50
+ extended-logging-formatter = ["python-logging-extras"]
51
+ rich-text = ["rich"]
52
+
53
+
54
+ [project.scripts]
55
+ prometheus-dcache-exporter = "prometheus_dcache_exporter.__main__:main"
56
+
57
+
58
+
59
+
60
+ [build-system]
61
+ requires = ["setuptools>=77", "setuptools-scm>=8"]
62
+ build-backend = "setuptools.build_meta"
63
+
64
+
65
+
66
+
67
+ [tool.setuptools]
68
+
69
+
70
+ [tool.setuptools_scm]
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+ #Copyright © 2026 Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
88
+ #
89
+ #
90
+ #This program is free software: you can redistribute it and/or modify it under
91
+ #the terms of the GNU General Public License as published by the Free Software
92
+ #Foundation, either version 3 of the License, or (at your option) any later
93
+ #version.
94
+ #This program is distributed in the hope that it will be useful, but WITHOUT ANY
95
+ #WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
96
+ #PARTICULAR PURPOSE.
97
+ #See the GNU General Public License for more details.
98
+ #You should have received a copy of the GNU General Public License along with
99
+ #this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ #Copyright © 2026 Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2
+ #
3
+ #
4
+ #This program is free software: you can redistribute it and/or modify it under
5
+ #the terms of the GNU General Public License as published by the Free Software
6
+ #Foundation, either version 3 of the License, or (at your option) any later
7
+ #version.
8
+ #This program is distributed in the hope that it will be useful, but WITHOUT ANY
9
+ #WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
10
+ #PARTICULAR PURPOSE.
11
+ #See the GNU General Public License for more details.
12
+ #You should have received a copy of the GNU General Public License along with
13
+ #this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,437 @@
1
+ from module_utils import optional_import, optional_from_import
2
+
3
+
4
+ import sys
5
+ import os
6
+ import argparse
7
+ import enum
8
+ import signal
9
+ import locale
10
+ import logging
11
+ import contextlib
12
+ from warnings import warn
13
+
14
+ import prometheus_client
15
+ (rich, HAVE_RICH_CONSOLE) = optional_import("rich.console", get_top_level_modules=True)
16
+ (rich.logging, HAVE_RICH_LOGGING) = optional_import("rich.logging")
17
+ (ExtendedLoggingFormatter, HAVE_EXTENDEDLOGGINGFORMATTER) = optional_from_import("logging_extras.formatters", "ExtendedLoggingFormatter")
18
+
19
+
20
+ from ._config import *
21
+ from ._util import sequence_to_separated_str
22
+ from . import _collector as collector
23
+ from . import _dcache as dcache
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+ #*******************************************************************************
33
+ #* Enums *
34
+ #*******************************************************************************
35
+ class ExportMode(enum.StrEnum):
36
+ http = enum.auto()
37
+ stdout = enum.auto()
38
+ file = enum.auto()
39
+
40
+
41
+
42
+
43
+ #*******************************************************************************
44
+ #* Global Variables *
45
+ #*******************************************************************************
46
+ log = logging.getLogger(__name__)
47
+
48
+
49
+
50
+
51
+ #*******************************************************************************
52
+ #* Signal Handlers *
53
+ #*******************************************************************************
54
+ def exiting_signal_handler(signalnum, frame):
55
+ #prevent exiting while already exiting
56
+ for s in EXITING_SIGNALS:
57
+ signal.signal(s, signal.SIG_IGN)
58
+
59
+ sys.exit(128 + signalnum)
60
+
61
+
62
+
63
+
64
+ #*******************************************************************************
65
+ #* Miscellaneous *
66
+ #*******************************************************************************
67
+ class PositiveInt(int):
68
+ def __new__(cls, number):
69
+ integer = int(number)
70
+
71
+ if isinstance(number, str):
72
+ if str(integer) != number:
73
+ raise ValueError(f"`{number}` contains invalid characters (which includes signs, whitespace, redundant leading zeros and non-Latin digits).")
74
+ else:
75
+ if integer != number:
76
+ raise ValueError(f"`{number}` is not an integer.")
77
+ if integer <= 0:
78
+ raise ValueError(f"The integer `{number}` is not positive.")
79
+
80
+ return integer
81
+
82
+
83
+
84
+
85
+ class IntOrStr:
86
+ def __new__(cls, obj):
87
+ string = str(obj)
88
+
89
+ try:
90
+ integer = int(obj)
91
+ except ValueError:
92
+ return string
93
+
94
+ if str(integer) == string:
95
+ return integer
96
+ else:
97
+ return string
98
+
99
+
100
+
101
+
102
+ class FromResourceOrBytesOrStr:
103
+ def __new__(cls, obj):
104
+ if isinstance(obj, str):
105
+ match obj[:1]:
106
+ case "=":
107
+ return obj[1:]
108
+ case "$":
109
+ return os.environb[ obj[1:].encode() ]
110
+ case "<":
111
+ with open(obj[1:], mode="rb") as f:
112
+ return f.read()
113
+ case _:
114
+ return obj
115
+ elif isinstance(obj, bytes):
116
+ return obj
117
+ else:
118
+ return str(obj)
119
+
120
+
121
+
122
+
123
+ class AddMetricsExtraLabel(argparse.Action):
124
+ def __call__(self, parser, namespace, values, option_string=None):
125
+ metric_name_regular_expression = values[0]
126
+ extra_label_name = values[1]
127
+
128
+
129
+ #if necessary, initialise the attribute specified via `argparse.ArgumentParser.add_argument`’s `dest`-parameter
130
+ if not hasattr(namespace, self.dest) or getattr(namespace, self.dest) is None:
131
+ setattr(namespace, self.dest, {})
132
+
133
+ #get the attribute specified via `argparse.ArgumentParser.add_argument`’s `dest`-parameter
134
+ metrics_extra_labels = getattr(namespace, self.dest)
135
+
136
+ #if necessary, initialise the set for the extra label names for the given metric name regular expression
137
+ if metric_name_regular_expression not in metrics_extra_labels:
138
+ metrics_extra_labels[metric_name_regular_expression] = set()
139
+
140
+
141
+ #add the given extra label name for the given metric name regular expression
142
+ metrics_extra_labels[metric_name_regular_expression].add(extra_label_name)
143
+
144
+
145
+
146
+
147
+ def warn_about_unmatched_metric_name_regular_expressions(*, unmatched_metric_name_regular_expressions):
148
+ if len(unmatched_metric_name_regular_expressions) == 1:
149
+ if len(cfg.metrics_extra_labels[ unmatched_metric_name_regular_expressions[0] ]) == 1:
150
+ log.warning(f"The `--metrics.extra-label`-option with the `REGULAR-EXPRESSION`-argument having the value `{unmatched_metric_name_regular_expressions[0]}` is ignored.")
151
+ else:
152
+ log.warning(f"The `--metrics.extra-label`-options with the `REGULAR-EXPRESSION`-argument having the value `{unmatched_metric_name_regular_expressions[0]}` is ignored.")
153
+ else:
154
+ log.warning(f"The `--metrics.extra-label`-options with the `REGULAR-EXPRESSION`-argument having any of the values {sequence_to_separated_str(unmatched_metric_name_regular_expressions, ", ", " and ", element_delimiters="`")} is ignored.")
155
+
156
+
157
+
158
+
159
+ #*******************************************************************************
160
+ #* Main *
161
+ #*******************************************************************************
162
+ def main():
163
+ global cfg
164
+
165
+
166
+ #set locale
167
+ locale.setlocale(locale.LC_ALL, "")
168
+
169
+
170
+ #set up logging
171
+ logging_root_logger = logging.getLogger()
172
+
173
+ if os.isatty(sys.stderr.fileno()) and HAVE_RICH_LOGGING and HAVE_RICH_CONSOLE:
174
+ logging_handler = rich.logging.RichHandler(console=rich.console.Console(stderr=True), rich_tracebacks=True, tracebacks_code_width=None, keywords=())
175
+ logging.basicConfig(handlers=(logging_handler, ), format="{name}: {message}", datefmt="%c", style="{")
176
+ else:
177
+ if HAVE_EXTENDEDLOGGINGFORMATTER:
178
+ # Beware that this `logging` handler must not be associated with more than a
179
+ # single `logging` logger (which is `logging_root_logger`).
180
+ # See the `dedicated_handler`-parameter of the
181
+ # `ExtendedLoggingFormatter.callbacks.add_logging_handler_to_hook_state,`-
182
+ # callback-function.
183
+ logging_handler = logging.StreamHandler()
184
+
185
+ # Beware that this `logging` formatter must not be associated with more than a
186
+ # single `logging` handler (which is `logging_handler`).
187
+ # See the `dedicated_handler`-parameter of the
188
+ # `ExtendedLoggingFormatter.callbacks.add_logging_handler_to_hook_state,`-
189
+ # callback-function.
190
+ logging_formatter = ExtendedLoggingFormatter(fmt="{asctime}: {levelname}: {name}: {message_lineno}{message}",
191
+ exc_fmt="{asctime}: DEBUG: {name}: {exc_lineno}{exc}",
192
+ stack_fmt="{asctime}: DEBUG: {name}: {stack_lineno}{stack}",
193
+ datefmt="%c", style="{")
194
+ logging_formatter.registerCallback(ExtendedLoggingFormatter.Hook.format_begin, ExtendedLoggingFormatter.callbacks.add_logging_logger_to_hook_state)
195
+ logging_formatter.registerCallback(ExtendedLoggingFormatter.Hook.format_begin, ExtendedLoggingFormatter.callbacks.add_logging_handler_to_hook_state, dedicated_handler=logging_handler)
196
+ logging_formatter.registerCallback(ExtendedLoggingFormatter.Hook.format_begin, ExtendedLoggingFormatter.callbacks.clear_record_parts_by_extra_level, exc_level=logging.DEBUG, stack_level=logging.DEBUG)
197
+
198
+ logging_handler.setFormatter(logging_formatter)
199
+ logging_root_logger.addHandler(logging_handler)
200
+ else:
201
+ logging.basicConfig(format="{asctime}: {levelname}: {name}: {message}", datefmt="%c", style="{")
202
+
203
+ logging_root_logger.setLevel(logging.NOTSET)
204
+
205
+
206
+ #parse command arguments
207
+ parser = argparse.ArgumentParser(allow_abbrev=False)
208
+ parser.add_argument("--dcache-frontend-rest.base-url", type=str, default=DEFAULT_DCACHE_FRONTEND_REST_BASE_URL, dest="dcache_frontend_rest_base_url", help=f"The base URL under which the REST interface of dCache’s `frontend` service can be reached. Defaults to `{DEFAULT_DCACHE_FRONTEND_REST_BASE_URL}` if this option isn’t given.", metavar="URL")
209
+ parser.add_argument("--dcache-frontend-rest.http-auth-type", type=dcache.frontend.HTTPAuthType, choices=[e.value for e in dcache.frontend.HTTPAuthType], default=dcache.frontend.HTTPAuthType.none, dest="dcache_frontend_rest_http_auth_type", help=f"The type of HTTP authentication to be performed. `{dcache.frontend.HTTPAuthType.none}` (which is the default if this option isn’t given) causes no HTTP authentication to be performed. `{dcache.frontend.HTTPAuthType.basic_auth}` causes basic authentication to be used and requires the `--dcache-frontend-rest.http-auth-user`- and `--dcache-frontend-rest.http-auth-passphrase`-options to be given.")
210
+ parser.add_argument("--dcache-frontend-rest.http-auth-user", type=str, dest="dcache_frontend_rest_http_auth_user", help="The user used for HTTP authentication when connecting to the REST interface of dCache’s `frontend` service.", metavar="USERNAME")
211
+ parser.add_argument("--dcache-frontend-rest.http-auth-passphrase", type=FromResourceOrBytesOrStr, dest="dcache_frontend_rest_http_auth_passphrase", help="The passphrase used for HTTP authentication when connecting to the REST interface of dCache’s `frontend` service. If STRING starts with `$`, the passphrase is binarily read from an environment variable with the remainder of STRING as the name. If it starts with `<`, the passphrase is binarily read from a file with the remainder of it as the pathname. If it starts with `=`, the passphrase is the remainder of it.", metavar="STRING")
212
+ parser.add_argument("--dcache-frontend-rest.allow-http-auth-without-tls", action="store_true", dest="dcache_frontend_rest_allow_http_auth_without_tls", help="Allow HTTP authentication when connecting to the REST interface of dCache’s `frontend` service without TLS.")
213
+ parser.add_argument("--dcache-frontend-rest.tls-ca-certificates", type=str, dest="dcache_frontend_rest_tls_ca_certificates", help="The CA certificates that are trusted when connecting to the REST interface of dCache’s `frontend` service via TLS. If this option isn’t given, the system’s default CA certificates are used. If a regular file (or symbolic link to such), it must consist of concatenated CA certificates in PEM format; if a directory (or symbolic link to such), it must follow the well-known layout by OpenSSL. CRLs are ignored and OCSP isn’t performed.", metavar="PATHNAME")
214
+ parser.add_argument("--dcache-frontend-rest.tls-client-auth-certificate", type=str, dest="dcache_frontend_rest_tls_client_auth_certificate", help="The certificate and, unless the `--dcache-frontend-rest.tls-client-auth-private-key`-option is given, private key used for client authentication when connecting to the REST interface of dCache’s `frontend` service via TLS. It must be a regular file (or symbolic link to such) that consists of the certificate, depending on the aforementioned, concatenated with the private key in PEM format.", metavar="PATHNAME")
215
+ parser.add_argument("--dcache-frontend-rest.tls-client-auth-private-key", type=str, dest="dcache_frontend_rest_tls_client_auth_private_key", help="The private key used for client authentication when connecting to the REST interface of dCache’s `frontend` service via TLS. It must be a regular file (or symbolic link to such) that consists of the private key in PEM format.", metavar="PATHNAME")
216
+ parser.add_argument("--dcache-frontend-rest.tls-client-auth-private-key-passphrase", type=FromResourceOrBytesOrStr, dest="dcache_frontend_rest_tls_client_auth_private_key_passphrase", help="The passphrase used for decrypting the private key specified via the `--dcache-frontend-rest.tls-client-auth-certificate`- or `--dcache-frontend-rest.tls-client-auth-private-key`-options. If STRING starts with `$`, the passphrase is binarily read from an environment variable with the remainder of STRING as the name. If it starts with `<`, the passphrase is binarily read from a file with the remainder of it as the pathname. If it starts with `=`, the passphrase is the remainder of it.", metavar="STRING")
217
+ parser.add_argument("--dcache-frontend-rest.connect-timeout", type=float, default=DEFAULT_DCACHE_FRONTEND_REST_CONNECT_TIMEOUT, dest="dcache_frontend_rest_connect_timeout", help=f"The timeout in seconds for establishing the connection to the REST interface of dCache’s `frontend` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_FRONTEND_REST_CONNECT_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
218
+ parser.add_argument("--dcache-frontend-rest.read-timeout", type=float, default=DEFAULT_DCACHE_FRONTEND_REST_READ_TIMEOUT, dest="dcache_frontend_rest_read_timeout", help=f"The timeout in seconds for reading a chunk of data from the REST interface of dCache’s `frontend` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_FRONTEND_REST_READ_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
219
+ parser.add_argument("--dcache-frontend-rest.pool-timeout", type=float, default=DEFAULT_DCACHE_FRONTEND_REST_POOL_TIMEOUT, dest="dcache_frontend_rest_pool_timeout", help=f"The timeout in seconds for getting a connection from the pool of connections to the REST interface of dCache’s `frontend` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_FRONTEND_REST_POOL_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
220
+ parser.add_argument("--dcache-frontend-rest.connection-pool-max-size", type=float, dest="dcache_frontend_rest_connection_pool_max_size", help="The maximum number of connections to the REST interface of dCache’s `frontend` service in the connection pool, with non-positive decimals meaning unlimited. Defaults to the value of the `--dcache-frontend-rest.max-concurrent-requests`-option if this option isn’t given. The maximum number of requests that are actually made concurrently is specified via the `--dcache-frontend-rest.max-concurrent-requests`-option.", metavar="INTEGER")
221
+ parser.add_argument("--dcache-frontend-rest.connection-pool-max-idle-size", type=float, dest="dcache_frontend_rest_connection_pool_max_idle_size", help="The maximum number of idle connections to the REST interface of dCache’s `frontend` service in the connection pool, with negative decimals meaning unlimited and zero meaning none. Defaults to the value of the `--dcache-frontend-rest.connection-pool-max-size`-option if this option isn’t given.", metavar="INTEGER")
222
+ parser.add_argument("--dcache-frontend-rest.connection-pool-max-idle-time", type=float, default=DEFAULT_DCACHE_FRONTEND_REST_CONNECTION_POOL_MAX_IDLE_TIME, dest="dcache_frontend_rest_connection_pool_max_idle_time", help=f"The maximum time in seconds for which an idle connection to the REST interface of dCache’s `frontend` service may be kept open in the connection pool, with negative decimals meaning unlimited and zero meaning instant closure. Defaults to `{DEFAULT_DCACHE_FRONTEND_REST_CONNECTION_POOL_MAX_IDLE_TIME}` if this option isn’t given.", metavar="DECIMAL")
223
+ parser.add_argument("--dcache-frontend-rest.max-concurrent-requests", type=PositiveInt, default=DEFAULT_DCACHE_FRONTEND_REST_MAX_CONCURRENT_REQUESTS, dest="dcache_frontend_rest_max_concurrent_requests", help=f"The maximum number of requests that are concurrently made to the REST interface of dCache’s `frontend` service. Defaults to `{DEFAULT_DCACHE_FRONTEND_REST_MAX_CONCURRENT_REQUESTS}` if this option isn’t given.", metavar="POSITIVE-INTEGER")
224
+ parser.add_argument("--dcache-admin-ssh.host", type=str, default=DEFAULT_DCACHE_ADMIN_SSH_HOST, dest="dcache_admin_ssh_host", help=f"The host under which the SSH interface of dCache’s `admin` service can be reached. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_HOST}` if this option isn’t given.", metavar="HOSTNAME-OR-ADDRESS")
225
+ parser.add_argument("--dcache-admin-ssh.port", type=int, default=DEFAULT_DCACHE_ADMIN_SSH_PORT, dest="dcache_admin_ssh_port", help=f"The port under which the SSH interface of dCache’s `admin` service can be reached. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_PORT}` if this option isn’t given.", metavar="PORTNUMBER")
226
+ parser.add_argument("--dcache-admin-ssh.user", type=str, dest="dcache_admin_ssh_user", help="The user used for connecting to the SSH interface of dCache’s `admin` service. Defaults to the name of the user that executes the program.", metavar="USERNAME")
227
+ parser.add_argument("--dcache-admin-ssh.known-hosts", type=str, action="append", default=[], dest="dcache_admin_ssh_known_hosts", help=f"Adds the known hosts file (using OpenSSH’s `ssh_known_hosts` format) to the list of those from which trusted SSH host keys are read. The list is processed after that of any existing system known hosts files ({sequence_to_separated_str(OPENSSH_GLOBAL_KNOWN_HOSTS_FILES + OPENSSH_USER_KNOWN_HOSTS_FILES, ", ", " and ", element_delimiters="`")}) and in the order as given, with all but the first entry for a given being ignored.", metavar="PATHNAME")
228
+ parser.add_argument("--dcache-admin-ssh.private-key", type=str, dest="dcache_admin_ssh_private_key", help="The file with the private key used for connecting to the SSH interface of dCache’s `admin` service via the authentication method `publickey`.", metavar="PATHNAME")
229
+ parser.add_argument("--dcache-admin-ssh.private-key-passphrase", type=FromResourceOrBytesOrStr, dest="dcache_admin_ssh_private_key_passphrase", help="The passphrase used for decrypting the private key specified via the `--dcache-admin-ssh.private-key`-option. If STRING starts with `$`, the passphrase is binarily read from an environment variable with the remainder of STRING as the name. If it starts with `<`, the passphrase is binarily read from a file with the remainder of it as the pathname. If it starts with `=`, the passphrase is the remainder of it.", metavar="STRING")
230
+ parser.add_argument("--dcache-admin-ssh.passphrase", type=FromResourceOrBytesOrStr, dest="dcache_admin_ssh_passphrase", help="The passphrase used for connecting to the SSH interface of dCache’s `admin` service via the authentication methods `password` and `keyboard-interactive`. If STRING starts with `$`, the passphrase is binarily read from an environment variable with the remainder of STRING as the name. If it starts with `<`, the passphrase is binarily read from a file with the remainder of it as the pathname. If it starts with `=`, the passphrase is the remainder of it.", metavar="STRING")
231
+ parser.add_argument("--dcache-admin-ssh.no-system-known-hosts", action="store_true", dest="dcache_admin_ssh_no_system_known_hosts", help=f"Don’t add any system known hosts files ({sequence_to_separated_str(OPENSSH_GLOBAL_KNOWN_HOSTS_FILES + OPENSSH_USER_KNOWN_HOSTS_FILES, ", ", " and ", element_delimiters="`")}) to the list of those from which trusted SSH host keys are read.")
232
+ parser.add_argument("--dcache-admin-ssh.no-default-private-key-locations", action="store_true", dest="dcache_admin_ssh_no_default_private_key_locations", help="Don’t use any private keys from default locations (which are the default of OpenSSH’s `IdentityFile`-option).")
233
+ parser.add_argument("--dcache-admin-ssh.no-agent", action="store_true", dest="dcache_admin_ssh_no_agent", help="Don’t use any SSH agent.")
234
+ parser.add_argument("--dcache-admin-ssh.tcp-timeout", type=float, default=DEFAULT_DCACHE_ADMIN_SSH_TCP_TIMEOUT, dest="dcache_admin_ssh_tcp_timeout", help=f"The timeout in seconds for establishing the TCP connection to the SSH interface of dCache’s `admin` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_TCP_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
235
+ parser.add_argument("--dcache-admin-ssh.banner-timeout", type=float, default=DEFAULT_DCACHE_ADMIN_SSH_BANNER_TIMEOUT, dest="dcache_admin_ssh_banner_timeout", help=f"The timeout in seconds for receiving any banner from the SSH interface of dCache’s `admin` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_BANNER_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
236
+ parser.add_argument("--dcache-admin-ssh.authentication-timeout", type=float, default=DEFAULT_DCACHE_ADMIN_SSH_AUTHENTICATION_TIMEOUT, dest="dcache_admin_ssh_authentication_timeout", help=f"The timeout in seconds for authenticating to the SSH interface of dCache’s `admin` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_AUTHENTICATION_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
237
+ parser.add_argument("--dcache-admin-ssh.channel-open-timeout", type=float, default=DEFAULT_DCACHE_ADMIN_SSH_CHANNEL_OPEN_TIMEOUT, dest="dcache_admin_ssh_channel_open_timeout", help=f"The timeout in seconds for opening a (SSH) channel to the SSH interface of dCache’s `admin` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_CHANNEL_OPEN_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
238
+ parser.add_argument("--dcache-admin-ssh.command-channel-timeout", type=float, default=DEFAULT_DCACHE_ADMIN_SSH_COMMAND_CHANNEL_TIMEOUT, dest="dcache_admin_ssh_command_channel_timeout", help=f"The timeout in seconds for read/write operations on the (SSH) channel of an executed command in the SSH interface of dCache’s `admin` service, with non-positive decimals disabling it. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_COMMAND_CHANNEL_TIMEOUT}` if this option isn’t given.", metavar="DECIMAL")
239
+ parser.add_argument("--dcache-admin-ssh.keepalive-interval", type=int, default=DEFAULT_DCACHE_ADMIN_SSH_KEEPALIVE_INTERVAL, dest="dcache_admin_ssh_keepalive_interval", help=f"The interval in seconds at which (client-initiated) SSH keepalive packets are sent to the SSH interface of dCache’s `admin` service, with non-positive integers disabling it. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_KEEPALIVE_INTERVAL}` if this option isn’t given.", metavar="INTEGER")
240
+ parser.add_argument("--dcache-admin-ssh.max-concurrent-commands", type=PositiveInt, default=DEFAULT_DCACHE_ADMIN_SSH_MAX_CONCURRENT_COMMANDS, dest="dcache_admin_ssh_max_concurrent_commands", help=f"The maximum number of commands that are concurrently executed via the SSH interface of dCache’s `admin` service. Defaults to `{DEFAULT_DCACHE_ADMIN_SSH_MAX_CONCURRENT_COMMANDS}` if this option isn’t given.", metavar="POSITIVE-INTEGER")
241
+ parser.add_argument("--metrics.name-prefix", type=str, default=DEFAULT_METRICS_NAME_PREFIX, dest="metrics_name_prefix", help="The prefix for all metric names. Defaults to `{DEFAULT_METRICS_NAME_PREFIX}` if this option isn’t given.", metavar="STRING")
242
+ parser.add_argument("--metrics.extra-label", type=str, nargs=2, default={}, action=AddMetricsExtraLabel, dest="metrics_extra_labels", help="Adds the extra label `EXTRA-LABEL-NAME` (of each matching metric respectively) to metrics whose name (without the metrics prefix specified via the `--metrics.name-prefix`-option and without any `_`-separator after that) matches the (implicitly fully anchored) Python `re` regular expression `REGULAR-EXPRESSION`.", metavar=("REGULAR-EXPRESSION", "EXTRA-LABEL-NAME"))
243
+ parser.add_argument("--export-mode", type=ExportMode, choices=[e.value for e in ExportMode], default=ExportMode.http, help=f"The mode in which the metrics are exported. `{ExportMode.http.value}` (which is the default if this option isn’t given) provides them via a HTTP server (that runs until the program is terminated by a signal or an error), with metrics being (freshly) collected on every scrape. `{ExportMode.stdout.value}` writes them to standard output (once, after which the program terminates). `{ExportMode.file.value}` writes them atomically (via moving a newly created temporary file) to the file specified via the `--output-file`-option (once, after which the program terminates), with the output file being overwritten if it exists already.")
244
+ parser.add_argument("--http-port", type=int, help=f"The port on which the HTTP server listens when the export mode is `http` (ignored otherwise). Defaults to `{DEFAULT_HTTP_PORT}` if this option isn’t given.", metavar="PORTNUMBER")
245
+ parser.add_argument("--output-file", type=str, help="The file to which the metrics are written when the export mode is `file` (ignored otherwise).", metavar="PATHNAME")
246
+ parser.add_argument("--log.level", type=IntOrStr, choices=[ level for name, number in logging.getLevelNamesMapping().items() if name != "NOTSET" for level in (name, number) ], default=DEFAULT_LOG_LEVELNAME, dest="log_level", help=f"The minimum level (name or number) of log messages to be logged. Defaults to `{DEFAULT_LOG_LEVELNAME}` (`{logging.getLevelName(DEFAULT_LOG_LEVELNAME)}`) if this option isn’t given.")
247
+
248
+ cfg = parser.parse_args(namespace=cfg)
249
+
250
+
251
+ #configure logging
252
+ logging_root_logger.setLevel(cfg.log_level)
253
+
254
+
255
+ #check whether options that are required depending on other options are given
256
+ match cfg.dcache_frontend_rest_http_auth_type:
257
+ case dcache.frontend.HTTPAuthType.none:
258
+ pass
259
+ case dcache.frontend.HTTPAuthType.basic_auth:
260
+ if cfg.dcache_frontend_rest_http_auth_user is None and cfg.dcache_frontend_rest_http_auth_passphrase is None:
261
+ parser.error("the following arguments are required: --dcache-frontend-rest.http-auth-user, --dcache-frontend-rest.http-auth-passphrase")
262
+ elif cfg.dcache_frontend_rest_http_auth_user is None:
263
+ parser.error("the following arguments are required: --dcache-frontend-rest.http-auth-user")
264
+ elif cfg.dcache_frontend_rest_http_auth_passphrase is None:
265
+ parser.error("the following arguments are required: --dcache-frontend-rest.http-auth-passphrase")
266
+ case _:
267
+ raise NotImplementedError(f"Unknown HTTP authentication type `{cfg.dcache_frontend_rest_http_auth_type.name}`.")
268
+ if cfg.dcache_frontend_rest_tls_client_auth_private_key is not None and cfg.dcache_frontend_rest_tls_client_auth_certificate is None:
269
+ parser.error("the following arguments are required: --dcache-frontend-rest.tls-client-auth-certificate")
270
+ if cfg.export_mode == ExportMode.file and cfg.output_file is None:
271
+ parser.error("the following arguments are required: --output-file")
272
+
273
+
274
+ #warn about ignored options
275
+ match cfg.dcache_frontend_rest_http_auth_type:
276
+ case dcache.frontend.HTTPAuthType.none:
277
+ if cfg.dcache_frontend_rest_http_auth_user is not None:
278
+ log.warning("The `--dcache-frontend-rest.http-auth-user`-option is ignored.")
279
+ if cfg.dcache_frontend_rest_http_auth_passphrase is not None:
280
+ log.warning("The `--dcache-frontend-rest.http-auth-passphrase`-option is ignored.")
281
+ case dcache.frontend.HTTPAuthType.basic_auth:
282
+ pass
283
+ case _:
284
+ raise NotImplementedError(f"Unknown HTTP authentication type `{cfg.dcache_frontend_rest_http_auth_type.name}`.")
285
+ if cfg.dcache_frontend_rest_tls_client_auth_private_key_passphrase is not None and (cfg.dcache_frontend_rest_tls_client_auth_certificate is None and cfg.dcache_frontend_rest_tls_client_auth_private_key is None):
286
+ log.warning("The `--dcache-frontend-rest.tls-client-auth-private-key-passphrase`-option is ignored.")
287
+ if cfg.http_port is not None and cfg.export_mode in (ExportMode.stdout, ExportMode.file):
288
+ log.warning("The `--http-port`-option is ignored.")
289
+ if cfg.output_file is not None and ( cfg.export_mode == ExportMode.http or (cfg.export_mode == ExportMode.stdout and cfg.output_file != "-") ):
290
+ log.warning("The `--output-file`-option is ignored.")
291
+
292
+
293
+ #set option default values (late)
294
+ if cfg.dcache_frontend_rest_connection_pool_max_size is None:
295
+ cfg.dcache_frontend_rest_connection_pool_max_size = cfg.dcache_frontend_rest_max_concurrent_requests
296
+ if cfg.dcache_frontend_rest_connection_pool_max_idle_size is None:
297
+ cfg.dcache_frontend_rest_connection_pool_max_idle_size = cfg.dcache_frontend_rest_connection_pool_max_size
298
+ if cfg.http_port is None:
299
+ cfg.http_port = DEFAULT_HTTP_PORT
300
+
301
+
302
+ #if the export mode is `file` and the `--output-file`-option has the special value `-`, change the former to `stdout`
303
+ if cfg.export_mode == ExportMode.file and cfg.output_file == "-":
304
+ cfg.export_mode = ExportMode.stdout
305
+
306
+ #disable intervals/limits that are set to a non-positive value
307
+ for a in ("dcache_frontend_rest_connect_timeout", "dcache_frontend_rest_read_timeout", "dcache_frontend_rest_pool_timeout", "dcache_frontend_rest_connection_pool_max_size", "dcache_admin_ssh_tcp_timeout", "dcache_admin_ssh_banner_timeout", "dcache_admin_ssh_authentication_timeout", "dcache_admin_ssh_channel_open_timeout", "dcache_admin_ssh_command_channel_timeout", "dcache_admin_ssh_keepalive_interval"):
308
+ if getattr(cfg, a) <= 0:
309
+ setattr(cfg, a, None)
310
+
311
+ #disable limits that are set to a negative value
312
+ for a in ("dcache_frontend_rest_connection_pool_max_idle_size", "dcache_frontend_rest_connection_pool_max_idle_time"):
313
+ if getattr(cfg, a) < 0:
314
+ setattr(cfg, a, None)
315
+
316
+
317
+ #warn about suboptimal option values
318
+ if cfg.dcache_frontend_rest_connection_pool_max_size is not None and cfg.dcache_frontend_rest_connection_pool_max_size < cfg.dcache_frontend_rest_max_concurrent_requests:
319
+ log.warning("The value of the `--dcache-frontend-rest.connection-pool-max-size`-option is less than that of the `--dcache-frontend-rest.max-concurrent-requests`-option, which means that some of the concurrent requests will block until others have finished.")
320
+ if cfg.dcache_frontend_rest_connection_pool_max_idle_size is not None:
321
+ if cfg.dcache_frontend_rest_connection_pool_max_size is None:
322
+ log.warning("The value of the `--dcache-frontend-rest.connection-pool-max-idle-size`-option causes a limit while that of the `--dcache-frontend-rest.connection-pool-max-size`-option does not, which means that some connections might not be re-used.")
323
+ elif cfg.dcache_frontend_rest_connection_pool_max_idle_size < cfg.dcache_frontend_rest_connection_pool_max_size:
324
+ log.warning("The value of the `--dcache-frontend-rest.connection-pool-max-idle-size`-option is less than that of the `--dcache-frontend-rest.connection-pool-max-size`-option, which means that some connections might not be re-used.")
325
+
326
+
327
+ #manually apply option values
328
+ collector.MetricName.prefix = cfg.metrics_name_prefix
329
+
330
+
331
+ #exit regularly on various exiting signals
332
+ for s in EXITING_SIGNALS:
333
+ signal.signal(s, exiting_signal_handler)
334
+
335
+
336
+ #determine the system SSH known hosts files
337
+ if not cfg.dcache_admin_ssh_no_system_known_hosts:
338
+ cfg.dcache_admin_ssh_system_known_hosts = list(OPENSSH_GLOBAL_KNOWN_HOSTS_FILES)
339
+ for p in OPENSSH_USER_KNOWN_HOSTS_FILES:
340
+ cfg.dcache_admin_ssh_system_known_hosts.append(os.path.expanduser(p))
341
+
342
+
343
+ #unregister `prometheus_client`’s default collectors
344
+ # TODO: Keep this aligned with `prometheus_client`’s default collectors.
345
+ prometheus_client.REGISTRY.unregister(prometheus_client.GC_COLLECTOR)
346
+ prometheus_client.REGISTRY.unregister(prometheus_client.PLATFORM_COLLECTOR)
347
+ prometheus_client.REGISTRY.unregister(prometheus_client.PROCESS_COLLECTOR)
348
+
349
+ #create collector registry
350
+ registry = prometheus_client.CollectorRegistry()
351
+
352
+
353
+ with contextlib.ExitStack() as resource_manager:
354
+ #create collector
355
+ try:
356
+ resource_manager.enter_context( collector.DcacheCollector(registry=registry, unmatched_metric_name_regular_expressions_callback=warn_about_unmatched_metric_name_regular_expressions) )
357
+ except collector.DcacheCollector.exceptions.ConfigurationError as e1:
358
+ e2 = e1.__cause__
359
+ match type(e2):
360
+ case collector.DcacheCollector.exceptions.InvalidMetricNameRegularExpressionError:
361
+ parser.error(f"argument --metrics.extra-label: invalid REGULAR-EXPRESSION value: '{e2.__cause__.pattern}'")
362
+ case collector.DcacheCollector.exceptions.ExtraLabelValueCallablesNotFoundError:
363
+ if len(e2.extra_labels) == 1:
364
+ parser.error(f"argument --metrics.extra-label: invalid EXTRA-LABEL-NAME value in effect for the '{e2.metric_name}'-metric: '{e2.extra_labels[0]}'")
365
+ else:
366
+ parser.error(f"argument --metrics.extra-label: invalid EXTRA-LABEL-NAME values in effect for the '{e2.metric_name}'-metric: {sequence_to_separated_str(e2.extra_labels, ", ", " and ", element_delimiters="'")}")
367
+ case _:
368
+ warn(f"Unexpected type `{e2.__class__.__module__}.{e2.__class__.__qualname__}` of the `__cause__`-attribute of a `{e1.__class__.__module__}.{e1.__class__.__qualname__}`-exception.")
369
+ raise
370
+
371
+
372
+ #export metrics
373
+ match cfg.export_mode:
374
+ case ExportMode.http:
375
+ tmp = prometheus_client.start_http_server(cfg.http_port, registry=registry)
376
+
377
+ #wait for the program to be terminated by a signal or an error
378
+ # TODO: Remove this handling (including the `tmp`-variable) for
379
+ # `prometheus_client` versions < 0.20.0 once newer versions are
380
+ # sufficiently widespread.
381
+ if tmp is None:
382
+ #`prometheus_client` versions < 0.20.0:
383
+
384
+ import time
385
+ while True:
386
+ time.sleep(2**16)
387
+ else:
388
+ #`prometheus_client` versions ≧ 0.20.0:
389
+
390
+ thread = tmp[1]
391
+ thread.join()
392
+ case ExportMode.stdout:
393
+ # TODO: Keep the encoding aligned to that used by `prometheus_client`’s
394
+ # `generate_latest`-function.
395
+ print( prometheus_client.generate_latest(registry=registry).decode(encoding="utf-8"), end="" )
396
+ case ExportMode.file:
397
+ prometheus_client.write_to_textfile(cfg.output_file, registry=registry)
398
+ case _:
399
+ raise NotImplementedError(f"Unknown export mode `{cfg.export_mode.name}`.")
400
+
401
+
402
+ return os.EX_OK
403
+
404
+
405
+
406
+
407
+ if __name__ == "__main__":
408
+ sys.exit( main() )
409
+
410
+
411
+
412
+
413
+
414
+
415
+
416
+
417
+
418
+
419
+
420
+
421
+
422
+
423
+
424
+
425
+ #Copyright © 2026 Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
426
+ #
427
+ #
428
+ #This program is free software: you can redistribute it and/or modify it under
429
+ #the terms of the GNU General Public License as published by the Free Software
430
+ #Foundation, either version 3 of the License, or (at your option) any later
431
+ #version.
432
+ #This program is distributed in the hope that it will be useful, but WITHOUT ANY
433
+ #WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
434
+ #PARTICULAR PURPOSE.
435
+ #See the GNU General Public License for more details.
436
+ #You should have received a copy of the GNU General Public License along with
437
+ #this program. If not, see <https://www.gnu.org/licenses/>.