From 7da3eee2771a95b0158d38687591efbb57f5e454 Mon Sep 17 00:00:00 2001 From: Aaditya Dhruv Date: Tue, 24 Jun 2025 09:56:37 +0530 Subject: [PATCH 1/2] Add support to write_to_textfile for custom tmpdir While the try/except block does prevent most of the temp files from persisting, if there is a non catchable exception, those temp files continue to pollute the directory. Optionally set the temp directory would let us write to something like /tmp, so the target directory isn't polluted Signed-off-by: Aaditya Dhruv --- prometheus_client/exposition.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 8c84ffb5..5746f2a7 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -4,6 +4,7 @@ import gzip from http.server import BaseHTTPRequestHandler import os +import shutil import socket from socketserver import ThreadingMixIn import ssl @@ -446,21 +447,28 @@ def factory(cls, registry: CollectorRegistry) -> type: return MyMetricsHandler -def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = openmetrics.ALLOWUTF8) -> None: +def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = openmetrics.ALLOWUTF8, tmpdir: Optional[str] = None,) -> None: """Write metrics to the given path. This is intended for use with the Node exporter textfile collector. The path must end in .prom for the textfile collector to process it.""" - tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}' + if tmpdir is not None: + filename = os.path.basename(path) + tmppath = f'{os.path.join(tmpdir, filename)}.{os.getpid()}.{threading.current_thread().ident}' + else: + tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}' try: with open(tmppath, 'wb') as f: f.write(generate_latest(registry, escaping)) # rename(2) is atomic but fails on Windows if the destination file exists - if os.name == 'nt': - os.replace(tmppath, path) + if tmpdir is not None: + shutil.move(tmppath, path) else: - os.rename(tmppath, path) + if os.name == 'nt': + os.replace(tmppath, path) + else: + os.rename(tmppath, path) except Exception: if os.path.exists(tmppath): os.remove(tmppath) From 4804d357736ba7038bba5a3c0c9984a5a4a91eba Mon Sep 17 00:00:00 2001 From: Aaditya Dhruv Date: Wed, 9 Jul 2025 16:55:18 -0500 Subject: [PATCH 2/2] Modify write_to_textfile to ensure tmpdir is on same filesystem The tmpdir must be on the same filesystem to ensure an atomic operation takes place. If this is not enforced, there could be partial writes which can lead to partial/incorrect metrics being exported Signed-off-by: Aaditya Dhruv --- prometheus_client/exposition.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 5746f2a7..100e8e2b 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -4,7 +4,6 @@ import gzip from http.server import BaseHTTPRequestHandler import os -import shutil import socket from socketserver import ThreadingMixIn import ssl @@ -447,11 +446,16 @@ def factory(cls, registry: CollectorRegistry) -> type: return MyMetricsHandler -def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = openmetrics.ALLOWUTF8, tmpdir: Optional[str] = None,) -> None: +def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = openmetrics.ALLOWUTF8, tmpdir: Optional[str] = None) -> None: """Write metrics to the given path. This is intended for use with the Node exporter textfile collector. - The path must end in .prom for the textfile collector to process it.""" + The path must end in .prom for the textfile collector to process it. + + An optional tmpdir parameter can be set to determine where the + metrics will be temporarily written to. If not set, it will be in + the same directory as the .prom file. If provided, the path MUST be + on the same filesystem.""" if tmpdir is not None: filename = os.path.basename(path) tmppath = f'{os.path.join(tmpdir, filename)}.{os.getpid()}.{threading.current_thread().ident}' @@ -462,13 +466,10 @@ def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = op f.write(generate_latest(registry, escaping)) # rename(2) is atomic but fails on Windows if the destination file exists - if tmpdir is not None: - shutil.move(tmppath, path) + if os.name == 'nt': + os.replace(tmppath, path) else: - if os.name == 'nt': - os.replace(tmppath, path) - else: - os.rename(tmppath, path) + os.rename(tmppath, path) except Exception: if os.path.exists(tmppath): os.remove(tmppath)