You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
11 KiB
Python

#!/usr/bin/env python
from io import StringIO
from sys import maxunicode
from typing import Callable
from ..utils import floatToGoString, parse_version
from ..validation import (
_is_valid_legacy_labelname, _is_valid_legacy_metric_name,
)
CONTENT_TYPE_LATEST = 'application/openmetrics-text; version=1.0.0; charset=utf-8'
"""Content type of the latest OpenMetrics 1.0 text format"""
CONTENT_TYPE_LATEST_2_0 = 'application/openmetrics-text; version=2.0.0; charset=utf-8'
"""Content type of the OpenMetrics 2.0 text format"""
ESCAPING_HEADER_TAG = 'escaping'
ALLOWUTF8 = 'allow-utf-8'
UNDERSCORES = 'underscores'
DOTS = 'dots'
VALUES = 'values'
def _is_valid_exemplar_metric(metric, sample):
if metric.type == 'counter' and sample.name.endswith('_total'):
return True
if metric.type in ('gaugehistogram') and sample.name.endswith('_bucket'):
return True
if metric.type in ('histogram') and sample.name.endswith('_bucket') or sample.name == metric.name:
return True
return False
def _compose_exemplar_string(metric, sample, exemplar):
"""Constructs an exemplar string."""
if not _is_valid_exemplar_metric(metric, sample):
raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter")
labels = '{{{0}}}'.format(','.join(
['{}="{}"'.format(
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
for k, v in sorted(exemplar.labels.items())]))
if exemplar.timestamp is not None:
exemplarstr = ' # {} {} {}'.format(
labels,
floatToGoString(exemplar.value),
exemplar.timestamp,
)
else:
exemplarstr = ' # {} {}'.format(
labels,
floatToGoString(exemplar.value),
)
return exemplarstr
def generate_latest(registry, escaping=UNDERSCORES, version="1.0.0"):
'''Returns the metrics from the registry in latest text format as a string.'''
output = []
for metric in registry.collect():
try:
mname = metric.name
output.append('# HELP {} {}\n'.format(
escape_metric_name(mname, escaping), _escape(metric.documentation, ALLOWUTF8, _is_legacy_labelname_rune)))
output.append(f'# TYPE {escape_metric_name(mname, escaping)} {metric.type}\n')
if metric.unit:
output.append(f'# UNIT {escape_metric_name(mname, escaping)} {metric.unit}\n')
for s in metric.samples:
if escaping == ALLOWUTF8 and not _is_valid_legacy_metric_name(s.name):
labelstr = escape_metric_name(s.name, escaping)
if s.labels:
labelstr += ','
else:
labelstr = ''
if s.labels:
items = sorted(s.labels.items())
# Label values always support UTF-8
labelstr += ','.join(
['{}="{}"'.format(
escape_label_name(k, escaping), _escape(v, ALLOWUTF8, _is_legacy_labelname_rune))
for k, v in items])
if labelstr:
labelstr = "{" + labelstr + "}"
if s.exemplar:
exemplarstr = _compose_exemplar_string(metric, s, s.exemplar)
else:
exemplarstr = ''
timestamp = ''
if s.timestamp is not None:
timestamp = f' {s.timestamp}'
# Skip native histogram samples entirely if version < 2.0.0
if s.native_histogram and parse_version(version) < (2, 0, 0):
continue
native_histogram = ''
negative_spans = ''
negative_deltas = ''
positive_spans = ''
positive_deltas = ''
if s.native_histogram:
# Initialize basic nh template
nh_sample_template = '{{count:{},sum:{},schema:{},zero_threshold:{},zero_count:{}'
args = [
s.native_histogram.count_value,
s.native_histogram.sum_value,
s.native_histogram.schema,
s.native_histogram.zero_threshold,
s.native_histogram.zero_count,
]
# If there are neg spans, append them and the neg deltas to the template and args
if s.native_histogram.neg_spans:
negative_spans = ','.join([f'{ns[0]}:{ns[1]}' for ns in s.native_histogram.neg_spans])
negative_deltas = ','.join(str(nd) for nd in s.native_histogram.neg_deltas)
nh_sample_template += ',negative_spans:[{}]'
args.append(negative_spans)
nh_sample_template += ',negative_deltas:[{}]'
args.append(negative_deltas)
# If there are pos spans, append them and the pos spans to the template and args
if s.native_histogram.pos_spans:
positive_spans = ','.join([f'{ps[0]}:{ps[1]}' for ps in s.native_histogram.pos_spans])
positive_deltas = ','.join(f'{pd}' for pd in s.native_histogram.pos_deltas)
nh_sample_template += ',positive_spans:[{}]'
args.append(positive_spans)
nh_sample_template += ',positive_deltas:[{}]'
args.append(positive_deltas)
# Add closing brace
nh_sample_template += '}}'
# Format the template with the args
native_histogram = nh_sample_template.format(*args)
if s.native_histogram.nh_exemplars:
for nh_ex in s.native_histogram.nh_exemplars:
nh_exemplarstr = _compose_exemplar_string(metric, s, nh_ex)
exemplarstr += nh_exemplarstr
value = ''
if s.native_histogram:
value = native_histogram
elif s.value is not None:
value = floatToGoString(s.value)
if (escaping != ALLOWUTF8) or _is_valid_legacy_metric_name(s.name):
output.append('{}{} {}{}{}\n'.format(
_escape(s.name, escaping, _is_legacy_labelname_rune),
labelstr,
value,
timestamp,
exemplarstr
))
else:
output.append('{} {}{}{}\n'.format(
labelstr,
value,
timestamp,
exemplarstr
))
except Exception as exception:
exception.args = (exception.args or ('',)) + (metric,)
raise
output.append('# EOF\n')
return ''.join(output).encode('utf-8')
def escape_metric_name(s: str, escaping: str = UNDERSCORES) -> str:
"""Escapes the metric name and puts it in quotes iff the name does not
conform to the legacy Prometheus character set.
"""
if len(s) == 0:
return s
if escaping == ALLOWUTF8:
if not _is_valid_legacy_metric_name(s):
return '"{}"'.format(_escape(s, escaping, _is_legacy_metric_rune))
return _escape(s, escaping, _is_legacy_metric_rune)
elif escaping == UNDERSCORES:
if _is_valid_legacy_metric_name(s):
return s
return _escape(s, escaping, _is_legacy_metric_rune)
elif escaping == DOTS:
return _escape(s, escaping, _is_legacy_metric_rune)
elif escaping == VALUES:
if _is_valid_legacy_metric_name(s):
return s
return _escape(s, escaping, _is_legacy_metric_rune)
return s
def escape_label_name(s: str, escaping: str = UNDERSCORES) -> str:
"""Escapes the label name and puts it in quotes iff the name does not
conform to the legacy Prometheus character set.
"""
if len(s) == 0:
return s
if escaping == ALLOWUTF8:
if not _is_valid_legacy_labelname(s):
return '"{}"'.format(_escape(s, escaping, _is_legacy_labelname_rune))
return _escape(s, escaping, _is_legacy_labelname_rune)
elif escaping == UNDERSCORES:
if _is_valid_legacy_labelname(s):
return s
return _escape(s, escaping, _is_legacy_labelname_rune)
elif escaping == DOTS:
return _escape(s, escaping, _is_legacy_labelname_rune)
elif escaping == VALUES:
if _is_valid_legacy_labelname(s):
return s
return _escape(s, escaping, _is_legacy_labelname_rune)
return s
def _escape(s: str, escaping: str, valid_rune_fn: Callable[[str, int], bool]) -> str:
"""Performs backslash escaping on backslash, newline, and double-quote characters.
valid_rune_fn takes the input character and its index in the containing string."""
if escaping == ALLOWUTF8:
return s.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
elif escaping == UNDERSCORES:
escaped = StringIO()
for i, b in enumerate(s):
if valid_rune_fn(b, i):
escaped.write(b)
else:
escaped.write('_')
return escaped.getvalue()
elif escaping == DOTS:
escaped = StringIO()
for i, b in enumerate(s):
if b == '_':
escaped.write('__')
elif b == '.':
escaped.write('_dot_')
elif valid_rune_fn(b, i):
escaped.write(b)
else:
escaped.write('__')
return escaped.getvalue()
elif escaping == VALUES:
escaped = StringIO()
escaped.write("U__")
for i, b in enumerate(s):
if b == '_':
escaped.write("__")
elif valid_rune_fn(b, i):
escaped.write(b)
elif not _is_valid_utf8(b):
escaped.write("_FFFD_")
else:
escaped.write('_')
escaped.write(format(ord(b), 'x'))
escaped.write('_')
return escaped.getvalue()
return s
def _is_legacy_metric_rune(b: str, i: int) -> bool:
return _is_legacy_labelname_rune(b, i) or b == ':'
def _is_legacy_labelname_rune(b: str, i: int) -> bool:
if len(b) != 1:
raise ValueError("Input 'b' must be a single character.")
return (
('a' <= b <= 'z')
or ('A' <= b <= 'Z')
or (b == '_')
or ('0' <= b <= '9' and i > 0)
)
_SURROGATE_MIN = 0xD800
_SURROGATE_MAX = 0xDFFF
def _is_valid_utf8(s: str) -> bool:
if 0 <= ord(s) < _SURROGATE_MIN:
return True
if _SURROGATE_MAX < ord(s) <= maxunicode:
return True
return False