| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,9 +5,10 @@ | |||
| 5 | 5 | process_collector, registry, | |
| 6 | 6 | ) | |
| 7 | 7 | 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, | ||
| 11 | 12 | write_to_textfile, | |
| 12 | 13 | ) | |
| 13 | 14 | from .gc_collector import GC_COLLECTOR, GCCollector | |
@@ -33,6 +34,8 @@ | |||
| 33 | 34 | 'enable_created_metrics', | |
| 34 | 35 | 'disable_created_metrics', | |
| 35 | 36 | 'CONTENT_TYPE_LATEST', | |
| 37 | + 'CONTENT_TYPE_PLAIN_0_0_4', | ||
| 38 | + 'CONTENT_TYPE_PLAIN_1_0_0', | ||
| 36 | 39 | 'generate_latest', | |
| 37 | 40 | 'MetricsHandler', | |
| 38 | 41 | 'make_wsgi_app', | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,6 @@ | |||
| 1 | 1 | import base64 | |
| 2 | 2 | from contextlib import closing | |
| 3 | + from functools import partial | ||
| 3 | 4 | import gzip | |
| 4 | 5 | from http.server import BaseHTTPRequestHandler | |
| 5 | 6 | import os | |
@@ -17,13 +18,16 @@ | |||
| 17 | 18 | ) | |
| 18 | 19 | from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer | |
| 19 | 20 | ||
| 21 | + from packaging.version import Version | ||
| 22 | + | ||
| 20 | 23 | from .openmetrics import exposition as openmetrics | |
| 21 | 24 | from .registry import CollectorRegistry, REGISTRY | |
| 22 | 25 | from .utils import floatToGoString | |
| 23 | - from .validation import _is_valid_legacy_metric_name | ||
| 24 | 26 | ||
| 25 | 27 | __all__ = ( | |
| 26 | 28 | 'CONTENT_TYPE_LATEST', | |
| 29 | + 'CONTENT_TYPE_PLAIN_0_0_4', | ||
| 30 | + 'CONTENT_TYPE_PLAIN_1_0_0', | ||
| 27 | 31 | 'delete_from_gateway', | |
| 28 | 32 | 'generate_latest', | |
| 29 | 33 | 'instance_ip_grouping_key', | |
@@ -37,8 +41,13 @@ | |||
| 37 | 41 | 'write_to_textfile', | |
| 38 | 42 | ) | |
| 39 | 43 | ||
| 40 | - CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8' | ||
| 41 | - """Content type of the latest text format""" | ||
| 44 | + CONTENT_TYPE_PLAIN_0_0_4 = 'text/plain; version=0.0.4; charset=utf-8' | ||
| 45 | + """Content type of the compatibility format""" | ||
| 46 | + | ||
| 47 | + CONTENT_TYPE_PLAIN_1_0_0 = 'text/plain; version=1.0.0; charset=utf-8' | ||
| 48 | + """Content type of the latest format""" | ||
| 49 | + | ||
| 50 | + CONTENT_TYPE_LATEST = CONTENT_TYPE_PLAIN_1_0_0 | ||
| 42 | 51 | ||
| 43 | 52 | ||
| 44 | 53 | class _PrometheusRedirectHandler(HTTPRedirectHandler): | |
@@ -245,29 +254,38 @@ class TmpServer(ThreadingWSGIServer): | |||
| 245 | 254 | start_http_server = start_wsgi_server | |
| 246 | 255 | ||
| 247 | 256 | ||
| 248 | - def generate_latest(registry: CollectorRegistry = REGISTRY) -> bytes: | ||
| 249 | - """Returns the metrics from the registry in latest text format as a string.""" | ||
| 257 | + def generate_latest(registry: CollectorRegistry = REGISTRY, escaping: str = openmetrics.UNDERSCORES) -> bytes: | ||
| 258 | + """ | ||
| 259 | + Generates the exposition format using the basic Prometheus text format. | ||
| 260 | + | ||
| 261 | + Params: | ||
| 262 | + registry: CollectorRegistry to export data from. | ||
| 263 | + escaping: Escaping scheme used for metric and label names. | ||
| 264 | + | ||
| 265 | + Returns: UTF-8 encoded string containing the metrics in text format. | ||
| 266 | + """ | ||
| 250 | 267 | ||
| 251 | 268 | def sample_line(samples): | |
| 252 | 269 | if samples.labels: | |
| 253 | 270 | labelstr = '{0}'.format(','.join( | |
| 271 | + # Label values always support UTF-8 | ||
| 254 | 272 | ['{}="{}"'.format( | |
| 255 | - openmetrics.escape_label_name(k), openmetrics._escape(v)) | ||
| 273 | + openmetrics.escape_label_name(k, escaping), openmetrics._escape(v, openmetrics.ALLOWUTF8, False)) | ||
| 256 | 274 | for k, v in sorted(samples.labels.items())])) | |
| 257 | 275 | else: | |
| 258 | 276 | labelstr = '' | |
| 259 | 277 | timestamp = '' | |
| 260 | 278 | if samples.timestamp is not None: | |
| 261 | 279 | # Convert to milliseconds. | |
| 262 | 280 | timestamp = f' {int(float(samples.timestamp) * 1000):d}' | |
| 263 | - if _is_valid_legacy_metric_name(samples.name): | ||
| 281 | + if escaping != openmetrics.ALLOWUTF8 or openmetrics._is_valid_legacy_metric_name(samples.name): | ||
| 264 | 282 | if labelstr: | |
| 265 | 283 | labelstr = '{{{0}}}'.format(labelstr) | |
| 266 | - return f'{samples.name}{labelstr} {floatToGoString(samples.value)}{timestamp}\n' | ||
| 284 | + return f'{openmetrics.escape_metric_name(samples.name, escaping)}{labelstr} {floatToGoString(samples.value)}{timestamp}\n' | ||
| 267 | 285 | maybe_comma = '' | |
| 268 | 286 | if labelstr: | |
| 269 | 287 | maybe_comma = ',' | |
| 270 | - return f'{{{openmetrics.escape_metric_name(samples.name)}{maybe_comma}{labelstr}}} {floatToGoString(samples.value)}{timestamp}\n' | ||
| 288 | + return f'{{{openmetrics.escape_metric_name(samples.name, escaping)}{maybe_comma}{labelstr}}} {floatToGoString(samples.value)}{timestamp}\n' | ||
| 271 | 289 | ||
| 272 | 290 | output = [] | |
| 273 | 291 | for metric in registry.collect(): | |
@@ -290,8 +308,8 @@ def sample_line(samples): | |||
| 290 | 308 | mtype = 'untyped' | |
| 291 | 309 | ||
| 292 | 310 | 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') | ||
| 311 | + openmetrics.escape_metric_name(mname, escaping), metric.documentation.replace('\\', r'\\').replace('\n', r'\n'))) | ||
| 312 | + output.append(f'# TYPE {openmetrics.escape_metric_name(mname, escaping)} {mtype}\n') | ||
| 295 | 313 | ||
| 296 | 314 | om_samples: Dict[str, List[str]] = {} | |
| 297 | 315 | for s in metric.samples: | |
@@ -307,20 +325,79 @@ def sample_line(samples): | |||
| 307 | 325 | raise | |
| 308 | 326 | ||
| 309 | 327 | for suffix, lines in sorted(om_samples.items()): | |
| 310 | - output.append('# HELP {} {}\n'.format(openmetrics.escape_metric_name(metric.name + suffix), | ||
| 328 | + output.append('# HELP {} {}\n'.format(openmetrics.escape_metric_name(metric.name + suffix, escaping), | ||
| 311 | 329 | metric.documentation.replace('\\', r'\\').replace('\n', r'\n'))) | |
| 312 | - output.append(f'# TYPE {openmetrics.escape_metric_name(metric.name + suffix)} gauge\n') | ||
| 330 | + output.append(f'# TYPE {openmetrics.escape_metric_name(metric.name + suffix, escaping)} gauge\n') | ||
| 313 | 331 | output.extend(lines) | |
| 314 | 332 | return ''.join(output).encode('utf-8') | |
| 315 | 333 | ||
| 316 | 334 | ||
| 317 | 335 | def choose_encoder(accept_header: str) -> Tuple[Callable[[CollectorRegistry], bytes], str]: | |
| 336 | + # Python client library accepts a narrower range of content-types than | ||
| 337 | + # Prometheus does. | ||
| 318 | 338 | accept_header = accept_header or '' | |
| 339 | + escaping = openmetrics.UNDERSCORES | ||
| 319 | 340 | for accepted in accept_header.split(','): | |
| 320 | 341 | 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 | ||
| 342 | + toks = accepted.split(';') | ||
| 343 | + version = _get_version(toks) | ||
| 344 | + escaping = _get_escaping(toks) | ||
| 345 | + # Only return an escaping header if we have a good version and | ||
| 346 | + # mimetype. | ||
| 347 | + if not version: | ||
| 348 | + return (partial(openmetrics.generate_latest, escaping=openmetrics.UNDERSCORES), openmetrics.CONTENT_TYPE_LATEST) | ||
| 349 | + if version and Version(version) >= Version('1.0.0'): | ||
| 350 | + return (partial(openmetrics.generate_latest, escaping=escaping), | ||
| 351 | + openmetrics.CONTENT_TYPE_LATEST + '; escaping=' + str(escaping)) | ||
| 352 | + elif accepted.split(';')[0].strip() == 'text/plain': | ||
| 353 | + toks = accepted.split(';') | ||
| 354 | + version = _get_version(toks) | ||
| 355 | + escaping = _get_escaping(toks) | ||
| 356 | + # Only return an escaping header if we have a good version and | ||
| 357 | + # mimetype. | ||
| 358 | + if version and Version(version) >= Version('1.0.0'): | ||
| 359 | + return (partial(generate_latest, escaping=escaping), | ||
| 360 | + CONTENT_TYPE_LATEST + '; escaping=' + str(escaping)) | ||
| 361 | + return generate_latest, CONTENT_TYPE_PLAIN_0_0_4 | ||
| 362 | + | ||
| 363 | + | ||
| 364 | + def _get_version(accept_header: List[str]) -> str: | ||
| 365 | + """Return the version tag from the Accept header. | ||
| 366 | + | ||
| 367 | + If no version is specified, returns empty string.""" | ||
| 368 | + | ||
| 369 | + for tok in accept_header: | ||
| 370 | + if '=' not in tok: | ||
| 371 | + continue | ||
| 372 | + key, value = tok.strip().split('=', 1) | ||
| 373 | + if key == 'version': | ||
| 374 | + return value | ||
| 375 | + return "" | ||
| 376 | + | ||
| 377 | + | ||
| 378 | + def _get_escaping(accept_header: List[str]) -> str: | ||
| 379 | + """Return the escaping scheme from the Accept header. | ||
| 380 | + | ||
| 381 | + If no escaping scheme is specified or the scheme is not one of the allowed | ||
| 382 | + strings, defaults to UNDERSCORES.""" | ||
| 383 | + | ||
| 384 | + for tok in accept_header: | ||
| 385 | + if '=' not in tok: | ||
| 386 | + continue | ||
| 387 | + key, value = tok.strip().split('=', 1) | ||
| 388 | + if key != 'escaping': | ||
| 389 | + continue | ||
| 390 | + if value == openmetrics.ALLOWUTF8: | ||
| 391 | + return openmetrics.ALLOWUTF8 | ||
| 392 | + elif value == openmetrics.UNDERSCORES: | ||
| 393 | + return openmetrics.UNDERSCORES | ||
| 394 | + elif value == openmetrics.DOTS: | ||
| 395 | + return openmetrics.DOTS | ||
| 396 | + elif value == openmetrics.VALUES: | ||
| 397 | + return openmetrics.VALUES | ||
| 398 | + else: | ||
| 399 | + return openmetrics.UNDERSCORES | ||
| 400 | + return openmetrics.UNDERSCORES | ||
| 324 | 401 | ||
| 325 | 402 | ||
| 326 | 403 | def gzip_accepted(accept_encoding_header: str) -> bool: | |
@@ -369,15 +446,15 @@ def factory(cls, registry: CollectorRegistry) -> type: | |||
| 369 | 446 | return MyMetricsHandler | |
| 370 | 447 | ||
| 371 | 448 | ||
| 372 | - def write_to_textfile(path: str, registry: CollectorRegistry) -> None: | ||
| 449 | + def write_to_textfile(path: str, registry: CollectorRegistry, escaping: str = openmetrics.ALLOWUTF8) -> None: | ||
| 373 | 450 | """Write metrics to the given path. | |
| 374 | 451 | ||
| 375 | 452 | This is intended for use with the Node exporter textfile collector. | |
| 376 | 453 | The path must end in .prom for the textfile collector to process it.""" | |
| 377 | 454 | tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}' | |
| 378 | 455 | try: | |
| 379 | 456 | with open(tmppath, 'wb') as f: | |
| 380 | - f.write(generate_latest(registry)) | ||
| 457 | + f.write(generate_latest(registry, escaping)) | ||
| 381 | 458 | ||
| 382 | 459 | # rename(2) is atomic but fails on Windows if the destination file exists | |
| 383 | 460 | if os.name == 'nt': | |
@@ -645,7 +722,7 @@ def _use_gateway( | |||
| 645 | 722 | ||
| 646 | 723 | handler( | |
| 647 | 724 | url=url, method=method, timeout=timeout, | |
| 648 | - headers=[('Content-Type', CONTENT_TYPE_LATEST)], data=data, | ||
| 725 | + headers=[('Content-Type', CONTENT_TYPE_PLAIN_0_0_4)], data=data, | ||
| 649 | 726 | )() | |
| 650 | 727 | ||
| 651 | 728 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments