Repository navigation
Add an experimental OpenMetrics 2.0 exposition (gauge, info, unknown) #1244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,10 @@ | |
| ESCAPING_HEADER_TAG = 'escaping' | ||
|
|
||
|
|
||
| # TODO: remove once every metric type is supported by _generate_latest_2_0, so | ||
| # that version 2.0.0 is chosen by content negotiation alone. | ||
| _openmetrics_2_enabled = False | ||
|
|
||
| ALLOWUTF8 = 'allow-utf-8' | ||
| UNDERSCORES = 'underscores' | ||
| DOTS = 'dots' | ||
|
|
@@ -57,6 +61,12 @@ def _compose_exemplar_string(metric, sample, exemplar): | |
|
|
||
| def generate_latest(registry, escaping=UNDERSCORES, version="1.0.0"): | ||
| '''Returns the metrics from the registry in latest text format as a string.''' | ||
| if _openmetrics_2_enabled and parse_version(version) == (2, 0, 0): | ||
| return _generate_latest_2_0(registry, escaping) | ||
| return _generate_latest_1_0(registry, escaping, version) | ||
|
|
||
|
|
||
| def _generate_latest_1_0(registry, escaping, version): | ||
| output = [] | ||
| for metric in registry.collect(): | ||
| try: | ||
|
|
@@ -170,6 +180,99 @@ def generate_latest(registry, escaping=UNDERSCORES, version="1.0.0"): | |
| return ''.join(output).encode('utf-8') | ||
|
|
||
|
|
||
| # OpenMetrics 2.0 requires the family name to equal the sample name, while | ||
| # metric.name omits the suffix these types add to their samples. | ||
| _FAMILY_NAME_SUFFIXES_2_0 = {'counter': '_total', 'info': '_info'} | ||
|
|
||
|
|
||
| def _generate_latest_2_0(registry, escaping): | ||
| """EXPERIMENTAL: targets OpenMetrics 2.0 and may change without notice.""" | ||
| formatters = { | ||
| 'counter': _format_counter_2_0, | ||
| 'gauge': _format_simple_samples_2_0, | ||
| 'summary': _format_summary_2_0, | ||
| 'histogram': _format_histogram_2_0, | ||
| 'gaugehistogram': _format_gaugehistogram_2_0, | ||
| 'unknown': _format_simple_samples_2_0, | ||
| 'info': _format_simple_samples_2_0, | ||
| 'stateset': _format_stateset_2_0, | ||
| } | ||
| output = [] | ||
| for metric in registry.collect(): | ||
| try: | ||
| family_name = metric.name + _FAMILY_NAME_SUFFIXES_2_0.get(metric.type, '') | ||
| output.append(_format_metadata_2_0(metric, family_name, escaping)) | ||
| output.append(formatters[metric.type](metric, family_name, escaping)) | ||
| except Exception as exception: | ||
| exception.args = (exception.args or ('',)) + (metric,) | ||
| raise | ||
| output.append('# EOF\n') | ||
| return ''.join(output).encode('utf-8') | ||
|
|
||
|
|
||
| def _format_metadata_2_0(metric, family_name, escaping): | ||
| """Formats the TYPE, UNIT and HELP lines, in the order OpenMetrics 2.0 recommends.""" | ||
| escaped_name = escape_metric_name(family_name, escaping) | ||
| output = [f'# TYPE {escaped_name} {metric.type}\n'] | ||
| if metric.unit: | ||
| output.append(f'# UNIT {escaped_name} {metric.unit}\n') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the OpenMetrics 2.0 ABNF ( |
||
| output.append('# HELP {} {}\n'.format( | ||
| escaped_name, _escape(metric.documentation, ALLOWUTF8, _is_legacy_labelname_rune))) | ||
| return ''.join(output) | ||
|
|
||
|
|
||
| def _format_simple_samples_2_0(metric, family_name, escaping): | ||
| """Formats the samples of a type that has neither start timestamps nor exemplars. | ||
|
|
||
| OpenMetrics 2.0 only allows exemplars on counters and histograms, and says | ||
| to drop invalid exemplars rather than fail the exposition, so any exemplar | ||
| on these samples is left out. | ||
| """ | ||
| return ''.join(_format_sample_2_0(family_name, s, escaping) for s in metric.samples) | ||
|
|
||
|
|
||
| def _format_sample_2_0(family_name, sample, escaping): | ||
| """Formats a sample as ``name{labels} value [timestamp]``, named after its family as OpenMetrics 2.0 requires.""" | ||
| line = f'{_format_name_and_labels_2_0(family_name, sample.labels, escaping)} {floatToGoString(sample.value)}' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a custom collector constructs a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point, here I'm a bit greedy as I force it to the family_name so that its always a OM2 valid output. I'm very hesitant to failing the full scrape for a sample name mismatching. |
||
| if sample.timestamp is not None: | ||
| line += f' {sample.timestamp}' | ||
| return line + '\n' | ||
|
|
||
|
|
||
| def _format_name_and_labels_2_0(name, labels, escaping): | ||
| """Formats ``name{labels}``, or ``{"name",labels}`` when the name has to be quoted.""" | ||
| escaped_name = escape_metric_name(name, escaping) | ||
| items = [ | ||
| '{}="{}"'.format(escape_label_name(k, escaping), _escape(v, ALLOWUTF8, _is_legacy_labelname_rune)) | ||
| for k, v in sorted(labels.items()) | ||
| ] | ||
| if escaped_name.startswith('"'): | ||
| return '{' + ','.join([escaped_name] + items) + '}' | ||
| if items: | ||
| return escaped_name + '{' + ','.join(items) + '}' | ||
| return escaped_name | ||
|
|
||
|
|
||
| def _format_counter_2_0(metric, family_name, escaping): | ||
| raise NotImplementedError('OpenMetrics 2.0 does not support counter metrics yet') | ||
|
|
||
|
|
||
| def _format_summary_2_0(metric, family_name, escaping): | ||
| raise NotImplementedError('OpenMetrics 2.0 does not support summary metrics yet') | ||
|
|
||
|
|
||
| def _format_histogram_2_0(metric, family_name, escaping): | ||
| raise NotImplementedError('OpenMetrics 2.0 does not support histogram metrics yet') | ||
|
|
||
|
|
||
| def _format_gaugehistogram_2_0(metric, family_name, escaping): | ||
| raise NotImplementedError('OpenMetrics 2.0 does not support gaugehistogram metrics yet') | ||
|
|
||
|
|
||
| def _format_stateset_2_0(metric, family_name, escaping): | ||
| raise NotImplementedError('OpenMetrics 2.0 does not support stateset metrics yet') | ||
|
|
||
|
|
||
| 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. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenMetrics 2.0 natively supports UTF-8 names. Should
generate_latest(andchoose_encoderonce 2.0 is enabled) defaultescapingtoALLOWUTF8whenversionis2.0.0and no explicit escaping is passed?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not, this would need to be documented as breaking somehow as without changing the escaping param this could break alerts/dashboards. I do like the idea though, maybe on a separate change ?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This made me think of another similar issue, on OM2. From https://prometheus.io/docs/guides/open_metrics_2_0_migration/#counter-suffix-rules
Currently it gets added automatically and the users have no control. The way it works is that the suffix is stripped before hand and added while serializing the metrics. This would require to be reworked totally to allow for user choice. let me add that to the issue and do that separately from the implem of the actual metrics :)