1212endpoint serves, and turns it into plain Python data.
1313
1414The two formats are close relatives and this module accepts both. Where they disagree, the
15- `dialect` parameter of `parse()` decides. The grammar implemented here follows the reference
16- parsers (`model/textparse` in prometheus/prometheus and the `prometheus_client` Python package),
17- which are stricter than most hand-written parsers about escaping and looser than the OpenMetrics
18- specification about whitespace.
15+ `dialect` parameter of `parse()` decides. The grammar answers to two authorities: the OpenMetrics
16+ specification and the parser the Prometheus server runs (`model/textparse` in
17+ prometheus/prometheus). The `prometheus_client` Python package is a secondary, informational
18+ reference only, consulted where it shows what real exporters send and called out where it differs
19+ from those two. The result is stricter than most hand-written parsers about escaping and looser
20+ than the OpenMetrics specification about whitespace.
1921"""
2022
2123__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
22- __version__ = '2026071708 '
24+ __version__ = '2026071710 '
2325
2426
2527import math
@@ -276,11 +278,12 @@ def _parse_labels(text, dialect):
276278 i += 1
277279 elif dialect == DIALECT_OPENMETRICS :
278280 # The two grammars disagree about whether the comma is optional. OpenMetrics wants it
279- # and its reference parser rejects a label set that leaves one out, so reading
280- # `a="1" b="2"` as two labels here would invent a structure out of a payload that says
281- # something else. The Prometheus grammar takes the entries apart without it and its
282- # reference parser reads them as two labels, so the loop carries on to the next entry
283- # rather than lose the whole payload over a separator that format does not require.
281+ # and rejects a label set that leaves one out, so reading `a="1" b="2"` as two labels
282+ # here would invent a structure out of a payload that says something else. The
283+ # Prometheus grammar takes the entries apart without it and the Prometheus server parser
284+ # reads them as two labels (the `prometheus_client` package rejects them instead), so
285+ # the loop carries on to the next entry rather than lose the whole payload over a
286+ # separator that format does not require.
284287 return False , f'expected "," between the entries of a label set at "{ text [i :i + 16 ]} "'
285288 return True , (name , labels , duplicate )
286289
@@ -308,9 +311,14 @@ def _find_brace_close(text, i):
308311# The number grammar of both formats, which is narrower than what Python's float() reads. Spelled
309312# out rather than filtered, because float() takes digit separators (`1_2`) and non-ASCII digits
310313# (`١٢٣`), neither of which either format defines. Accepting those would read a number where the
311- # endpoint sent something else entirely.
314+ # endpoint sent something else entirely. A sign is allowed in front of `inf` but not in front of
315+ # `nan`: the OpenMetrics grammar spells the value as bare `nan` with no sign (`number =/ "nan"`),
316+ # and the Prometheus server parser rejects `-nan` and `+nan` too, since Go's float parser reads a
317+ # sign only in front of `inf`. The `prometheus_client` package is looser and reads a signed nan
318+ # through Python's float(); this follows the two authorities and rejects it, as a signed
319+ # not-a-number says nothing a bare `nan` does not.
312320_FLOAT_REGEX = re .compile (
313- r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?|[-+]?(?: inf(?:inity)?|nan) ' ,
321+ r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?|[-+]?inf(?:inity)?|nan' ,
314322 re .ASCII | re .IGNORECASE ,
315323)
316324
@@ -610,9 +618,12 @@ def parse(data, dialect=None):
610618 OpenMetrics, putting every timestamp 1000 times too far in the future. Passing `dialect`
611619 rules both out, so pass it wherever the format is known, for instance from the
612620 `Content-Type` the endpoint answered with.
613- - Whitespace is taken the lenient way the Prometheus reference parser takes it, in both
614- dialects. A payload that a strict OpenMetrics reader would reject over spacing is read
615- here. As a consequence, a help text keeps neither its leading nor its trailing spaces.
621+ - Whitespace is taken more leniently than either reference parser takes it: the leading and
622+ trailing spaces and tabs of every line are stripped before the line is parsed, in both
623+ dialects. A payload that a strict OpenMetrics reader would reject over spacing is read here,
624+ and so is metadata whitespace that the Prometheus reference keeps and then rejects, such as
625+ the trailing space that turns `# TYPE c counter ` into the type `counter ` there. As a
626+ consequence, a help text keeps neither its leading nor its trailing spaces.
616627 - Beyond whitespace, two more line classes are read in the OpenMetrics dialect that its
617628 reference parser rejects: a blank line, and a comment other than `# HELP`, `# TYPE`,
618629 `# UNIT` and `# EOF`. Neither carries a measurement, so neither is worth losing every
@@ -631,8 +642,8 @@ def parse(data, dialect=None):
631642 trace data and say nothing about the value. This also reads them in the Prometheus dialect,
632643 which has no exemplars and whose reference parser rejects them, because an endpoint serving
633644 OpenMetrics under a `text/plain` content type is a thing that happens. Native histograms, an
634- experimental Prometheus extension neither format specifies, are not read at all and fail the
635- payload.
645+ experimental Prometheus extension that this text format cannot carry (it is defined for the
646+ protobuf exposition), are not read at all and fail the payload.
636647 - Neither format forbids a payload from reporting the same name and labels twice. Such
637648 samples are reported as they arrive, rather than deduplicated.
638649 - A sample whose label set names the same label twice is dropped, and dropped silently, while
0 commit comments