Skip to content

Commit 6a665c3

Browse files
authored
Merge pull request #21 from faucetsdn/v0.23.1
Upgrade python3-prometheus-client to v0.23.1
2 parents 62f060c + d477abf commit 6a665c3

21 files changed

+1197
-208
lines changed

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
graft tests/certs
2+
graft tests/proc

debian/control

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Build-Depends-Indep: dh-python,
1010
python3-all,
1111
python3-decorator (>= 4.0.10),
1212
python3-pytest,
13+
python3-pytest-benchmark,
1314
python3-setuptools,
1415
Rules-Requires-Root: no
1516
Standards-Version: 4.5.1

debian/patches/0002-Update-pyproject.toml.patch

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Index: python3-prometheus-client/pyproject.toml
33
--- python3-prometheus-client.orig/pyproject.toml
44
+++ python3-prometheus-client/pyproject.toml
55
@@ -7,11 +7,7 @@ name = "prometheus_client"
6-
version = "0.22.1"
6+
version = "0.23.1"
77
description = "Python client for the Prometheus monitoring system."
88
readme = "README.md"
99
-license = "Apache-2.0 AND BSD-2-Clause"

prometheus_client/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
process_collector, registry,
66
)
77
from .exposition import (
8-
CONTENT_TYPE_LATEST, delete_from_gateway, generate_latest,
9-
instance_ip_grouping_key, make_asgi_app, make_wsgi_app, MetricsHandler,
10-
push_to_gateway, pushadd_to_gateway, start_http_server, start_wsgi_server,
8+
CONTENT_TYPE_LATEST, CONTENT_TYPE_PLAIN_0_0_4, CONTENT_TYPE_PLAIN_1_0_0,
9+
delete_from_gateway, generate_latest, instance_ip_grouping_key,
10+
make_asgi_app, make_wsgi_app, MetricsHandler, push_to_gateway,
11+
pushadd_to_gateway, start_http_server, start_wsgi_server,
1112
write_to_textfile,
1213
)
1314
from .gc_collector import GC_COLLECTOR, GCCollector
@@ -33,6 +34,8 @@
3334
'enable_created_metrics',
3435
'disable_created_metrics',
3536
'CONTENT_TYPE_LATEST',
37+
'CONTENT_TYPE_PLAIN_0_0_4',
38+
'CONTENT_TYPE_PLAIN_1_0_0',
3639
'generate_latest',
3740
'MetricsHandler',
3841
'make_wsgi_app',

prometheus_client/asgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def make_asgi_app(registry: CollectorRegistry = REGISTRY, disable_compression: b
1111
async def prometheus_app(scope, receive, send):
1212
assert scope.get("type") == "http"
1313
# Prepare parameters
14-
params = parse_qs(scope.get('query_string', b''))
14+
params = parse_qs(scope.get('query_string', b'').decode("utf8"))
1515
accept_header = ",".join([
1616
value.decode("utf8") for (name, value) in scope.get('headers')
1717
if name.decode("utf8").lower() == 'accept'

prometheus_client/exposition.py

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
from contextlib import closing
3+
from functools import partial
34
import gzip
45
from http.server import BaseHTTPRequestHandler
56
import os
@@ -19,11 +20,12 @@
1920

2021
from .openmetrics import exposition as openmetrics
2122
from .registry import CollectorRegistry, REGISTRY
22-
from .utils import floatToGoString
23-
from .validation import _is_valid_legacy_metric_name
23+
from .utils import floatToGoString, parse_version
2424

2525
__all__ = (
2626
'CONTENT_TYPE_LATEST',
27+
'CONTENT_TYPE_PLAIN_0_0_4',
28+
'CONTENT_TYPE_PLAIN_1_0_0',
2729
'delete_from_gateway',
2830
'generate_latest',
2931
'instance_ip_grouping_key',
@@ -37,8 +39,13 @@
3739
'write_to_textfile',
3840
)
3941

40-
CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
41-
"""Content type of the latest text format"""
42+
CONTENT_TYPE_PLAIN_0_0_4 = 'text/plain; version=0.0.4; charset=utf-8'
43+
"""Content type of the compatibility format"""
44+
45+
CONTENT_TYPE_PLAIN_1_0_0 = 'text/plain; version=1.0.0; charset=utf-8'
46+
"""Content type of the latest format"""
47+
48+
CONTENT_TYPE_LATEST = CONTENT_TYPE_PLAIN_1_0_0
4249

4350

4451
class _PrometheusRedirectHandler(HTTPRedirectHandler):
@@ -245,29 +252,38 @@ class TmpServer(ThreadingWSGIServer):
245252
start_http_server = start_wsgi_server
246253

247254

248-
def generate_latest(registry: CollectorRegistry = REGISTRY) -> bytes:
249-
"""Returns the metrics from the registry in latest text format as a string."""
255+
def generate_latest(registry: CollectorRegistry = REGISTRY, escaping: str = openmetrics.UNDERSCORES) -> bytes:
256+
"""
257+
Generates the exposition format using the basic Prometheus text format.
258+
259+
Params:
260+
registry: CollectorRegistry to export data from.
261+
escaping: Escaping scheme used for metric and label names.
262+
263+
Returns: UTF-8 encoded string containing the metrics in text format.
264+
"""
250265

251266
def sample_line(samples):
252267
if samples.labels:
253268
labelstr = '{0}'.format(','.join(
269+
# Label values always support UTF-8
254270
['{}="{}"'.format(
255-
openmetrics.escape_label_name(k), openmetrics._escape(v))
271+
openmetrics.escape_label_name(k, escaping), openmetrics._escape(v, openmetrics.ALLOWUTF8, False))
256272
for k, v in sorted(samples.labels.items())]))
257273
else:
258274
labelstr = ''
259275
timestamp = ''
260276
if samples.timestamp is not None:
261277
# Convert to milliseconds.
262278
timestamp = f' {int(float(samples.timestamp) * 1000):d}'
263-
if _is_valid_legacy_metric_name(samples.name):
279+
if escaping != openmetrics.ALLOWUTF8 or openmetrics._is_valid_legacy_metric_name(samples.name):
264280
if labelstr:
265281
labelstr = '{{{0}}}'.format(labelstr)
266-
return f'{samples.name}{labelstr} {floatToGoString(samples.value)}{timestamp}\n'
282+
return f'{openmetrics.escape_metric_name(samples.name, escaping)}{labelstr} {floatToGoString(samples.value)}{timestamp}\n'
267283
maybe_comma = ''
268284
if labelstr:
269285
maybe_comma = ','
270-
return f'{{{openmetrics.escape_metric_name(samples.name)}{maybe_comma}{labelstr}}} {floatToGoString(samples.value)}{timestamp}\n'
286+
return f'{{{openmetrics.escape_metric_name(samples.name, escaping)}{maybe_comma}{labelstr}}} {floatToGoString(samples.value)}{timestamp}\n'
271287

272288
output = []
273289
for metric in registry.collect():
@@ -290,8 +306,8 @@ def sample_line(samples):
290306
mtype = 'untyped'
291307

292308
output.append('# HELP {} {}\n'.format(
293-
openmetrics.escape_metric_name(mname), metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
294-
output.append(f'# TYPE {openmetrics.escape_metric_name(mname)} {mtype}\n')
309+
openmetrics.escape_metric_name(mname, escaping), metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
310+
output.append(f'# TYPE {openmetrics.escape_metric_name(mname, escaping)} {mtype}\n')
295311

296312
om_samples: Dict[str, List[str]] = {}
297313
for s in metric.samples:
@@ -307,20 +323,79 @@ def sample_line(samples):
307323
raise
308324

309325
for suffix, lines in sorted(om_samples.items()):
310-
output.append('# HELP {} {}\n'.format(openmetrics.escape_metric_name(metric.name + suffix),
326+
output.append('# HELP {} {}\n'.format(openmetrics.escape_metric_name(metric.name + suffix, escaping),
311327
metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
312-
output.append(f'# TYPE {openmetrics.escape_metric_name(metric.name + suffix)} gauge\n')
328+
output.append(f'# TYPE {openmetrics.escape_metric_name(metric.name + suffix, escaping)} gauge\n')
313329
output.extend(lines)
314330
return ''.join(output).encode('utf-8')
315331

316332

317333
def choose_encoder(accept_header: str) -> Tuple[Callable[[CollectorRegistry], bytes], str]:
334+
# Python client library accepts a narrower range of content-types than
335+
# Prometheus does.
318336
accept_header = accept_header or ''
337+
escaping = openmetrics.UNDERSCORES
319338
for accepted in accept_header.split(','):
320339
if accepted.split(';')[0].strip() == 'application/openmetrics-text':
321-
return (openmetrics.generate_latest,
322-
openmetrics.CONTENT_TYPE_LATEST)
323-
return generate_latest, CONTENT_TYPE_LATEST
340+
toks = accepted.split(';')
341+
version = _get_version(toks)
342+
escaping = _get_escaping(toks)
343+
# Only return an escaping header if we have a good version and
344+
# mimetype.
345+
if not version:
346+
return (partial(openmetrics.generate_latest, escaping=openmetrics.UNDERSCORES, version="1.0.0"), openmetrics.CONTENT_TYPE_LATEST)
347+
if version and parse_version(version) >= (1, 0, 0):
348+
return (partial(openmetrics.generate_latest, escaping=escaping, version=version),
349+
f'application/openmetrics-text; version={version}; charset=utf-8; escaping=' + str(escaping))
350+
elif accepted.split(';')[0].strip() == 'text/plain':
351+
toks = accepted.split(';')
352+
version = _get_version(toks)
353+
escaping = _get_escaping(toks)
354+
# Only return an escaping header if we have a good version and
355+
# mimetype.
356+
if version and parse_version(version) >= (1, 0, 0):
357+
return (partial(generate_latest, escaping=escaping),
358+
CONTENT_TYPE_LATEST + '; escaping=' + str(escaping))
359+
return generate_latest, CONTENT_TYPE_PLAIN_0_0_4
360+
361+
362+
def _get_version(accept_header: List[str]) -> str:
363+
"""Return the version tag from the Accept header.
364+
365+
If no version is specified, returns empty string."""
366+
367+
for tok in accept_header:
368+
if '=' not in tok:
369+
continue
370+
key, value = tok.strip().split('=', 1)
371+
if key == 'version':
372+
return value
373+
return ""
374+
375+
376+
def _get_escaping(accept_header: List[str]) -> str:
377+
"""Return the escaping scheme from the Accept header.
378+
379+
If no escaping scheme is specified or the scheme is not one of the allowed
380+
strings, defaults to UNDERSCORES."""
381+
382+
for tok in accept_header:
383+
if '=' not in tok:
384+
continue
385+
key, value = tok.strip().split('=', 1)
386+
if key != 'escaping':
387+
continue
388+
if value == openmetrics.ALLOWUTF8:
389+
return openmetrics.ALLOWUTF8
390+
elif value == openmetrics.UNDERSCORES:
391+
return openmetrics.UNDERSCORES
392+
elif value == openmetrics.DOTS:
393+
return openmetrics.DOTS
394+
elif value == openmetrics.VALUES:
395+
return openmetrics.VALUES
396+
else:
397+
return openmetrics.UNDERSCORES
398+
return openmetrics.UNDERSCORES
324399

325400

326401
def gzip_accepted(accept_encoding_header: str) -> bool:
@@ -369,15 +444,24 @@ def factory(cls, registry: CollectorRegistry) -> type:
369444
return MyMetricsHandler
370445

371446

372-
def write_to_textfile(path: str, registry: CollectorRegistry) -> None:
447+
def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = openmetrics.ALLOWUTF8, tmpdir: Optional[str] = None) -> None:
373448
"""Write metrics to the given path.
374449
375450
This is intended for use with the Node exporter textfile collector.
376-
The path must end in .prom for the textfile collector to process it."""
377-
tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}'
451+
The path must end in .prom for the textfile collector to process it.
452+
453+
An optional tmpdir parameter can be set to determine where the
454+
metrics will be temporarily written to. If not set, it will be in
455+
the same directory as the .prom file. If provided, the path MUST be
456+
on the same filesystem."""
457+
if tmpdir is not None:
458+
filename = os.path.basename(path)
459+
tmppath = f'{os.path.join(tmpdir, filename)}.{os.getpid()}.{threading.current_thread().ident}'
460+
else:
461+
tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}'
378462
try:
379463
with open(tmppath, 'wb') as f:
380-
f.write(generate_latest(registry))
464+
f.write(generate_latest(registry, escaping))
381465

382466
# rename(2) is atomic but fails on Windows if the destination file exists
383467
if os.name == 'nt':
@@ -645,7 +729,7 @@ def _use_gateway(
645729

646730
handler(
647731
url=url, method=method, timeout=timeout,
648-
headers=[('Content-Type', CONTENT_TYPE_LATEST)], data=data,
732+
headers=[('Content-Type', CONTENT_TYPE_PLAIN_0_0_4)], data=data,
649733
)()
650734

651735

0 commit comments

Comments
 (0)