From 1d76bea81cbf9ae926f9f89743248f74c65678d6 Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Fri, 25 Oct 2024 13:37:47 -0400 Subject: [PATCH 1/5] hooray got a simple thing to work --- prometheus_client/metrics.py | 34 ++++++++++++++++++------- prometheus_client/metrics_core.py | 42 ++++++++++++++++++++++++++++++- prometheus_client/registry.py | 2 ++ tests/test_core.py | 25 ++++++++++++++++++ 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index cceaafda..a8d0ff99 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -11,8 +11,8 @@ from . import values # retain this import style for testability from .context_managers import ExceptionCounter, InprogressTracker, Timer from .metrics_core import ( - Metric, METRIC_LABEL_NAME_RE, METRIC_NAME_RE, - RESERVED_METRIC_LABEL_NAME_RE, + get_legacy_validation, Metric, METRIC_LABEL_NAME_RE, METRIC_NAME_RE, + RESERVED_METRIC_LABEL_NAME_RE ) from .registry import Collector, CollectorRegistry, REGISTRY from .samples import Exemplar, Sample @@ -39,10 +39,19 @@ def _build_full_name(metric_type, name, namespace, subsystem, unit): def _validate_labelname(l): - if not METRIC_LABEL_NAME_RE.match(l): - raise ValueError('Invalid label metric name: ' + l) - if RESERVED_METRIC_LABEL_NAME_RE.match(l): - raise ValueError('Reserved label metric name: ' + l) + print("status???? ", get_legacy_validation()) + if get_legacy_validation(): + if not METRIC_LABEL_NAME_RE.match(l): + raise ValueError('Invalid label metric name: ' + l) + if RESERVED_METRIC_LABEL_NAME_RE.match(l): + raise ValueError('Reserved label metric name: ' + l) + else: + try: + l.encode('utf-8') + except UnicodeDecodeError: + raise ValueError('Invalid label metric name: ' + l) + if RESERVED_METRIC_LABEL_NAME_RE.match(l): + raise ValueError('Reserved label metric name: ' + l) def _validate_labelnames(cls, labelnames): @@ -83,6 +92,7 @@ def enable_created_metrics(): _use_created = True + class MetricWrapperBase(Collector): _type: Optional[str] = None _reserved_labelnames: Sequence[str] = () @@ -139,8 +149,14 @@ def __init__(self: T, self._documentation = documentation self._unit = unit - if not METRIC_NAME_RE.match(self._name): - raise ValueError('Invalid metric name: ' + self._name) + if get_legacy_validation(): + if not METRIC_NAME_RE.match(self._name): + raise ValueError('Invalid metric name2: ' + self._name) + else: + try: + self._name.encode('utf-8') + except UnicodeDecodeError: + raise ValueError('Invalid metric name3: ' + self._name) if self._is_parent(): # Prepare the fields needed for child metrics. @@ -292,7 +308,7 @@ def f(): # Count only one type of exception with c.count_exceptions(ValueError): pass - + You can also reset the counter to zero in case your logical "process" restarts without restarting the actual python process. diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py index b09cea04..9bbf51aa 100644 --- a/prometheus_client/metrics_core.py +++ b/prometheus_client/metrics_core.py @@ -1,3 +1,4 @@ +import os import re from typing import Dict, List, Optional, Sequence, Tuple, Union @@ -12,6 +13,45 @@ RESERVED_METRIC_LABEL_NAME_RE = re.compile(r'^__.*$') +def _init_legacy_validation() -> bool: + print("getting value!", os.environ.get("PROMETHEUS_LEGACY_NAME_VALIDATION", 'False').lower() in ('true', '1', 't')) + return os.environ.get("PROMETHEUS_LEGACY_NAME_VALIDATION", 'False').lower() in ('true', '1', 't') + + +_legacy_validation = _init_legacy_validation() + + +def get_legacy_validation() -> bool: + global _legacy_validation + return _legacy_validation + + +def disable_legacy_validation(): + """Disable legacy name validation, instead allowing all UTF8 characters.""" + global _legacy_validation + _legacy_validation = False + + +def enable_legacy_validation(): + """Enable legacy name validation instead of allowing all UTF8 characters.""" + global _legacy_validation + _legacy_validation = True + + +def valid_metric_name(name: str) -> bool: + global _legacy_validation + if _legacy_validation: + return METRIC_NAME_RE.match(name) + else: + if not name: + return False + try: + name.encode('utf-8') + return True + except UnicodeDecodeError: + return False + + class Metric: """A single metric family and its samples. @@ -24,7 +64,7 @@ class Metric: def __init__(self, name: str, documentation: str, typ: str, unit: str = ''): if unit and not name.endswith("_" + unit): name += "_" + unit - if not METRIC_NAME_RE.match(name): + if not valid_metric_name(name): raise ValueError('Invalid metric name: ' + name) self.name: str = name self.documentation: str = documentation diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py index 4326b39a..bdbeea1b 100644 --- a/prometheus_client/registry.py +++ b/prometheus_client/registry.py @@ -46,6 +46,7 @@ def register(self, collector: Collector) -> None: for name in names: self._names_to_collectors[name] = collector self._collector_to_names[collector] = names + print("ok2?", self._names_to_collectors, self._collector_to_names) def unregister(self, collector: Collector) -> None: """Remove a collector from the registry.""" @@ -137,6 +138,7 @@ def get_sample_value(self, name: str, labels: Optional[Dict[str, str]] = None) - labels = {} for metric in self.collect(): for s in metric.samples: + print("ok?", s) if s.name == name and s.labels == labels: return s.value return None diff --git a/tests/test_core.py b/tests/test_core.py index 056d8e58..361b8247 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,6 +14,9 @@ ) from prometheus_client.decorator import getargspec from prometheus_client.metrics import _get_use_created +from prometheus_client.metrics_core import ( + disable_legacy_validation, enable_legacy_validation +) def is_locked(lock): @@ -114,8 +117,12 @@ def test_inc_not_observable(self): assert_not_observable(counter.inc) def test_exemplar_invalid_label_name(self): + enable_legacy_validation() self.assertRaises(ValueError, self.counter.inc, exemplar={':o)': 'smile'}) self.assertRaises(ValueError, self.counter.inc, exemplar={'1': 'number'}) + disable_legacy_validation() + self.counter.inc(exemplar={':o)': 'smile'}) + self.counter.inc(exemplar={'1': 'number'}) def test_exemplar_unicode(self): # 128 characters should not raise, even using characters larger than 1 byte. @@ -511,8 +518,12 @@ def test_block_decorator_with_label(self): self.assertEqual(1, value('hl_bucket', {'le': '+Inf', 'l': 'a'})) def test_exemplar_invalid_label_name(self): + enable_legacy_validation() self.assertRaises(ValueError, self.histogram.observe, 3.0, exemplar={':o)': 'smile'}) self.assertRaises(ValueError, self.histogram.observe, 3.0, exemplar={'1': 'number'}) + disable_legacy_validation() + self.histogram.observe(3.0, exemplar={':o)': 'smile'}) + self.histogram.observe(3.0, exemplar={'1': 'number'}) def test_exemplar_too_long(self): # 129 characters in total should fail. @@ -655,6 +666,7 @@ def test_labels_by_kwarg(self): self.assertRaises(ValueError, self.two_labels.labels, {'a': 'x'}, b='y') def test_invalid_names_raise(self): + enable_legacy_validation() self.assertRaises(ValueError, Counter, '', 'help') self.assertRaises(ValueError, Counter, '^', 'help') self.assertRaises(ValueError, Counter, '', 'help', namespace='&') @@ -663,6 +675,15 @@ def test_invalid_names_raise(self): self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['a:b']) self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved']) self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile']) + disable_legacy_validation() + self.assertRaises(ValueError, Counter, '', 'help') + # self.assertRaises(ValueError, Counter, '^', 'help') + # self.assertRaises(ValueError, Counter, '', 'help', namespace='&') + # self.assertRaises(ValueError, Counter, '', 'help', subsystem='(') + # self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['^']) + # self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['a:b']) + self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved']) + self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile']) def test_empty_labels_list(self): Histogram('h', 'help', [], registry=self.registry) @@ -713,6 +734,10 @@ def test_untyped_unit(self): def test_counter(self): self.custom_collector(CounterMetricFamily('c_total', 'help', value=1)) self.assertEqual(1, self.registry.get_sample_value('c_total', {})) + + def test_counter_utf8(self): + self.custom_collector(CounterMetricFamily('my.metric', 'help', value=1)) + self.assertEqual(1, self.registry.get_sample_value('my.metric_total', {})) def test_counter_total(self): self.custom_collector(CounterMetricFamily('c_total', 'help', value=1)) From aac1c664284e55ee55cf49e74f9eb671cbb31cd6 Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Fri, 25 Oct 2024 13:47:38 -0400 Subject: [PATCH 2/5] better! --- prometheus_client/metrics.py | 1 - prometheus_client/metrics_core.py | 43 ++++++++++++++++++++----- prometheus_client/openmetrics/parser.py | 8 +++-- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index a8d0ff99..af42a5b8 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -39,7 +39,6 @@ def _build_full_name(metric_type, name, namespace, subsystem, unit): def _validate_labelname(l): - print("status???? ", get_legacy_validation()) if get_legacy_validation(): if not METRIC_LABEL_NAME_RE.match(l): raise ValueError('Invalid label metric name: ' + l) diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py index 9bbf51aa..05058945 100644 --- a/prometheus_client/metrics_core.py +++ b/prometheus_client/metrics_core.py @@ -42,14 +42,41 @@ def valid_metric_name(name: str) -> bool: global _legacy_validation if _legacy_validation: return METRIC_NAME_RE.match(name) - else: - if not name: - return False - try: - name.encode('utf-8') - return True - except UnicodeDecodeError: - return False + if not name: + return False + try: + name.encode('utf-8') + return True + except UnicodeDecodeError: + return False + + +def valid_metric_name_token(tok: str) -> bool: + global _legacy_validation + quoted = tok[0] == '"' and tok[-1] == '"' + if not quoted or _legacy_validation: + return METRIC_NAME_RE.match(tok) + if not tok: + return False + try: + tok.encode('utf-8') + return True + except UnicodeDecodeError: + return False + + +def valid_metric_label_name_token(tok: str) -> bool: + global _legacy_validation + quoted = tok[0] == '"' and tok[-1] == '"' + if not quoted or _legacy_validation: + return METRIC_LABEL_NAME_RE.match(tok) + if not tok: + return False + try: + tok.encode('utf-8') + return True + except UnicodeDecodeError: + return False class Metric: diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index 39a44dc2..49815490 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -5,7 +5,7 @@ import math import re -from ..metrics_core import Metric, METRIC_LABEL_NAME_RE +from ..metrics_core import Metric, valid_metric_name_token, valid_metric_label_name_token from ..samples import BucketSpan, Exemplar, NativeHistogram, Sample, Timestamp from ..utils import floatToGoString @@ -143,7 +143,7 @@ def _parse_labels_with_state_machine(text): state = 'labelvalueslash' elif char == '"': ln = ''.join(labelname) - if not METRIC_LABEL_NAME_RE.match(ln): + if not valid_metric_label_name_token(ln): raise ValueError("Invalid line, bad label name: " + text) if ln in labels: raise ValueError("Invalid line, duplicate label name: " + text) @@ -223,7 +223,7 @@ def _parse_labels(text): # Replace escaping if needed if "\\" in label_value: label_value = _replace_escaping(label_value) - if not METRIC_LABEL_NAME_RE.match(label_name): + if not valid_metric_label_name_token(label_name): raise ValueError("invalid line, bad label name: " + text) if label_name in labels: raise ValueError("invalid line, duplicate label name: " + text) @@ -576,6 +576,8 @@ def build_metric(name, documentation, typ, unit, samples): raise ValueError("Units not allowed for this metric type: " + name) if typ in ['histogram', 'gaugehistogram']: _check_histogram(samples, name) + if not valid_metric_name_token(name): + raise ValueError("Invalid metric name: " + name) metric = Metric(name, documentation, typ, unit) # TODO: check labelvalues are valid utf8 metric.samples = samples From 2bffd2c6eb1cd2389d3e032b130a5c42f80456ad Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Mon, 28 Oct 2024 14:54:39 -0400 Subject: [PATCH 3/5] refactor --- prometheus_client/metrics.py | 60 +++---------- prometheus_client/metrics_core.py | 74 +--------------- prometheus_client/openmetrics/parser.py | 2 +- prometheus_client/validation.py | 108 ++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 123 deletions(-) create mode 100644 prometheus_client/validation.py diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index af42a5b8..c8ff2689 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -10,13 +10,13 @@ from . import values # retain this import style for testability from .context_managers import ExceptionCounter, InprogressTracker, Timer -from .metrics_core import ( - get_legacy_validation, Metric, METRIC_LABEL_NAME_RE, METRIC_NAME_RE, - RESERVED_METRIC_LABEL_NAME_RE -) +from .metrics_core import Metric from .registry import Collector, CollectorRegistry, REGISTRY from .samples import Exemplar, Sample from .utils import floatToGoString, INF +from .validation import ( + validate_metric_name, validate_exemplar, validate_labelnames +) T = TypeVar('T', bound='MetricWrapperBase') F = TypeVar("F", bound=Callable[..., Any]) @@ -38,39 +38,6 @@ def _build_full_name(metric_type, name, namespace, subsystem, unit): return full_name -def _validate_labelname(l): - if get_legacy_validation(): - if not METRIC_LABEL_NAME_RE.match(l): - raise ValueError('Invalid label metric name: ' + l) - if RESERVED_METRIC_LABEL_NAME_RE.match(l): - raise ValueError('Reserved label metric name: ' + l) - else: - try: - l.encode('utf-8') - except UnicodeDecodeError: - raise ValueError('Invalid label metric name: ' + l) - if RESERVED_METRIC_LABEL_NAME_RE.match(l): - raise ValueError('Reserved label metric name: ' + l) - - -def _validate_labelnames(cls, labelnames): - labelnames = tuple(labelnames) - for l in labelnames: - _validate_labelname(l) - if l in cls._reserved_labelnames: - raise ValueError('Reserved label metric name: ' + l) - return labelnames - - -def _validate_exemplar(exemplar): - runes = 0 - for k, v in exemplar.items(): - _validate_labelname(k) - runes += len(k) - runes += len(v) - if runes > 128: - raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding the limit of 128') - def _get_use_created() -> bool: return os.environ.get("PROMETHEUS_DISABLE_CREATED_SERIES", 'False').lower() not in ('true', '1', 't') @@ -91,7 +58,6 @@ def enable_created_metrics(): _use_created = True - class MetricWrapperBase(Collector): _type: Optional[str] = None _reserved_labelnames: Sequence[str] = () @@ -142,20 +108,14 @@ def __init__(self: T, _labelvalues: Optional[Sequence[str]] = None, ) -> None: self._name = _build_full_name(self._type, name, namespace, subsystem, unit) - self._labelnames = _validate_labelnames(self, labelnames) + self._labelnames = validate_labelnames(self, labelnames) self._labelvalues = tuple(_labelvalues or ()) self._kwargs: Dict[str, Any] = {} self._documentation = documentation self._unit = unit - if get_legacy_validation(): - if not METRIC_NAME_RE.match(self._name): - raise ValueError('Invalid metric name2: ' + self._name) - else: - try: - self._name.encode('utf-8') - except UnicodeDecodeError: - raise ValueError('Invalid metric name3: ' + self._name) + if not validate_metric_name(self._name): + raise ValueError('Invalid metric name: ' + self._name) if self._is_parent(): # Prepare the fields needed for child metrics. @@ -307,7 +267,7 @@ def f(): # Count only one type of exception with c.count_exceptions(ValueError): pass - + You can also reset the counter to zero in case your logical "process" restarts without restarting the actual python process. @@ -328,7 +288,7 @@ def inc(self, amount: float = 1, exemplar: Optional[Dict[str, str]] = None) -> N raise ValueError('Counters can only be incremented by non-negative amounts.') self._value.inc(amount) if exemplar: - _validate_exemplar(exemplar) + validate_exemplar(exemplar) self._value.set_exemplar(Exemplar(exemplar, amount, time.time())) def reset(self) -> None: @@ -667,7 +627,7 @@ def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> N if amount <= bound: self._buckets[i].inc(1) if exemplar: - _validate_exemplar(exemplar) + validate_exemplar(exemplar) self._buckets[i].set_exemplar(Exemplar(exemplar, amount, time.time())) break diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py index 05058945..3d00a865 100644 --- a/prometheus_client/metrics_core.py +++ b/prometheus_client/metrics_core.py @@ -1,82 +1,12 @@ -import os -import re from typing import Dict, List, Optional, Sequence, Tuple, Union from .samples import Exemplar, NativeHistogram, Sample, Timestamp +from .validation import validate_metric_name METRIC_TYPES = ( 'counter', 'gauge', 'summary', 'histogram', 'gaugehistogram', 'unknown', 'info', 'stateset', ) -METRIC_NAME_RE = re.compile(r'^[a-zA-Z_:][a-zA-Z0-9_:]*$') -METRIC_LABEL_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') -RESERVED_METRIC_LABEL_NAME_RE = re.compile(r'^__.*$') - - -def _init_legacy_validation() -> bool: - print("getting value!", os.environ.get("PROMETHEUS_LEGACY_NAME_VALIDATION", 'False').lower() in ('true', '1', 't')) - return os.environ.get("PROMETHEUS_LEGACY_NAME_VALIDATION", 'False').lower() in ('true', '1', 't') - - -_legacy_validation = _init_legacy_validation() - - -def get_legacy_validation() -> bool: - global _legacy_validation - return _legacy_validation - - -def disable_legacy_validation(): - """Disable legacy name validation, instead allowing all UTF8 characters.""" - global _legacy_validation - _legacy_validation = False - - -def enable_legacy_validation(): - """Enable legacy name validation instead of allowing all UTF8 characters.""" - global _legacy_validation - _legacy_validation = True - - -def valid_metric_name(name: str) -> bool: - global _legacy_validation - if _legacy_validation: - return METRIC_NAME_RE.match(name) - if not name: - return False - try: - name.encode('utf-8') - return True - except UnicodeDecodeError: - return False - - -def valid_metric_name_token(tok: str) -> bool: - global _legacy_validation - quoted = tok[0] == '"' and tok[-1] == '"' - if not quoted or _legacy_validation: - return METRIC_NAME_RE.match(tok) - if not tok: - return False - try: - tok.encode('utf-8') - return True - except UnicodeDecodeError: - return False - - -def valid_metric_label_name_token(tok: str) -> bool: - global _legacy_validation - quoted = tok[0] == '"' and tok[-1] == '"' - if not quoted or _legacy_validation: - return METRIC_LABEL_NAME_RE.match(tok) - if not tok: - return False - try: - tok.encode('utf-8') - return True - except UnicodeDecodeError: - return False class Metric: @@ -91,7 +21,7 @@ class Metric: def __init__(self, name: str, documentation: str, typ: str, unit: str = ''): if unit and not name.endswith("_" + unit): name += "_" + unit - if not valid_metric_name(name): + if not validate_metric_name(name): raise ValueError('Invalid metric name: ' + name) self.name: str = name self.documentation: str = documentation diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index 49815490..ecc4143e 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -5,7 +5,7 @@ import math import re -from ..metrics_core import Metric, valid_metric_name_token, valid_metric_label_name_token +from ..metrics_core import Metric, valid_metric_label_name_token, valid_metric_name_token from ..samples import BucketSpan, Exemplar, NativeHistogram, Sample, Timestamp from ..utils import floatToGoString diff --git a/prometheus_client/validation.py b/prometheus_client/validation.py new file mode 100644 index 00000000..295bfa27 --- /dev/null +++ b/prometheus_client/validation.py @@ -0,0 +1,108 @@ +import os +import re + +METRIC_NAME_RE = re.compile(r'^[a-zA-Z_:][a-zA-Z0-9_:]*$') +METRIC_LABEL_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') +RESERVED_METRIC_LABEL_NAME_RE = re.compile(r'^__.*$') + + +def _init_legacy_validation() -> bool: + """Retrieve name validation setting from environment.""" + return os.environ.get("PROMETHEUS_LEGACY_NAME_VALIDATION", 'False').lower() in ('true', '1', 't') + + +_legacy_validation = _init_legacy_validation() + + +def get_legacy_validation() -> bool: + global _legacy_validation + return _legacy_validation + + +def disable_legacy_validation(): + """Disable legacy name validation, instead allowing all UTF8 characters.""" + global _legacy_validation + _legacy_validation = False + + +def enable_legacy_validation(): + """Enable legacy name validation instead of allowing all UTF8 characters.""" + global _legacy_validation + _legacy_validation = True + + +def validate_metric_name(name: str) -> bool: + if not name: + return False + global _legacy_validation + if _legacy_validation: + return METRIC_NAME_RE.match(name) + try: + name.encode('utf-8') + return True + except UnicodeDecodeError: + return False + + +def validate_metric_name_token(tok: str) -> bool: + """Check validity of a parsed metric name token. UTF-8 names must be quoted.""" + if not tok: + return False + global _legacy_validation + quoted = tok[0] == '"' and tok[-1] == '"' + if not quoted or _legacy_validation: + return METRIC_NAME_RE.match(tok) + try: + tok.encode('utf-8') + return True + except UnicodeDecodeError: + return False + + +def validate_metric_label_name_token(tok: str) -> bool: + """Check validity of a parsed label name token. UTF-8 names must be quoted.""" + if not tok: + return False + global _legacy_validation + quoted = tok[0] == '"' and tok[-1] == '"' + if not quoted or _legacy_validation: + return METRIC_LABEL_NAME_RE.match(tok) + try: + tok.encode('utf-8') + return True + except UnicodeDecodeError: + return False + + +def validate_labelname(l): + if get_legacy_validation(): + if not METRIC_LABEL_NAME_RE.match(l): + raise ValueError('Invalid label metric name: ' + l) + if RESERVED_METRIC_LABEL_NAME_RE.match(l): + raise ValueError('Reserved label metric name: ' + l) + else: + try: + l.encode('utf-8') + except UnicodeDecodeError: + raise ValueError('Invalid label metric name: ' + l) + if RESERVED_METRIC_LABEL_NAME_RE.match(l): + raise ValueError('Reserved label metric name: ' + l) + + +def validate_labelnames(cls, labelnames): + labelnames = tuple(labelnames) + for l in labelnames: + validate_labelname(l) + if l in cls._reserved_labelnames: + raise ValueError('Reserved label metric name: ' + l) + return labelnames + + +def validate_exemplar(exemplar): + runes = 0 + for k, v in exemplar.items(): + validate_labelname(k) + runes += len(k) + runes += len(v) + if runes > 128: + raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding the limit of 128') From 4f6f227d56efe568cc27a40516cc69fbab4abf10 Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Mon, 28 Oct 2024 14:56:56 -0400 Subject: [PATCH 4/5] nit --- prometheus_client/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index c8ff2689..4e1688ea 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -15,7 +15,7 @@ from .samples import Exemplar, Sample from .utils import floatToGoString, INF from .validation import ( - validate_metric_name, validate_exemplar, validate_labelnames + validate_exemplar, validate_labelnames, validate_metric_name ) T = TypeVar('T', bound='MetricWrapperBase') From b3bec303418c0bb2442153860bb8555c35725394 Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Mon, 28 Oct 2024 15:06:53 -0400 Subject: [PATCH 5/5] tests pass other than lint --- prometheus_client/openmetrics/parser.py | 9 +++++---- tests/test_core.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py index ecc4143e..0e3e4bb5 100644 --- a/prometheus_client/openmetrics/parser.py +++ b/prometheus_client/openmetrics/parser.py @@ -5,9 +5,10 @@ import math import re -from ..metrics_core import Metric, valid_metric_label_name_token, valid_metric_name_token +from ..metrics_core import Metric from ..samples import BucketSpan, Exemplar, NativeHistogram, Sample, Timestamp from ..utils import floatToGoString +from ..validation import validate_metric_name_token, validate_metric_label_name_token def text_string_to_metric_families(text): @@ -143,7 +144,7 @@ def _parse_labels_with_state_machine(text): state = 'labelvalueslash' elif char == '"': ln = ''.join(labelname) - if not valid_metric_label_name_token(ln): + if not validate_metric_label_name_token(ln): raise ValueError("Invalid line, bad label name: " + text) if ln in labels: raise ValueError("Invalid line, duplicate label name: " + text) @@ -223,7 +224,7 @@ def _parse_labels(text): # Replace escaping if needed if "\\" in label_value: label_value = _replace_escaping(label_value) - if not valid_metric_label_name_token(label_name): + if not validate_metric_label_name_token(label_name): raise ValueError("invalid line, bad label name: " + text) if label_name in labels: raise ValueError("invalid line, duplicate label name: " + text) @@ -576,7 +577,7 @@ def build_metric(name, documentation, typ, unit, samples): raise ValueError("Units not allowed for this metric type: " + name) if typ in ['histogram', 'gaugehistogram']: _check_histogram(samples, name) - if not valid_metric_name_token(name): + if not validate_metric_name_token(name): raise ValueError("Invalid metric name: " + name) metric = Metric(name, documentation, typ, unit) # TODO: check labelvalues are valid utf8 diff --git a/tests/test_core.py b/tests/test_core.py index 361b8247..55e8f215 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,7 +14,7 @@ ) from prometheus_client.decorator import getargspec from prometheus_client.metrics import _get_use_created -from prometheus_client.metrics_core import ( +from prometheus_client.validation import ( disable_legacy_validation, enable_legacy_validation )