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
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
|