Skip to content

Commit 43be8ed

Browse files
validate and test metric name
1 parent 1ce17eb commit 43be8ed

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed

src/service/prometheus/prometheus_publisher.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import hashlib
22
import logging
3+
import re
34
import threading
45
import uuid
56
from typing import Dict, Optional
@@ -10,6 +11,11 @@
1011

1112
logger: logging.Logger = logging.getLogger(__name__)
1213

14+
# Prometheus metric name validation regex
15+
# Must start with letter, underscore, or colon
16+
# Can contain letters, numbers, underscores, and colons
17+
# Lowercase only (shall we allow uppercase?)
18+
PROMETHEUS_METRIC_NAME_REGEX = re.compile(r'^[a-z_:][a-z0-9_:]*$')
1319

1420
class PrometheusPublisher:
1521
def __init__(self, registry: CollectorRegistry = REGISTRY) -> None:
@@ -164,7 +170,22 @@ def gauge(
164170
)
165171

166172
def _get_full_metric_name(self, metric_name: str) -> str:
167-
return f"{PROMETHEUS_METRIC_PREFIX}{metric_name.lower()}"
173+
if not PROMETHEUS_METRIC_PREFIX.strip():
174+
raise ValueError("Prometheus metric prefix cannot be empty")
175+
if not metric_name.strip():
176+
raise ValueError("Metric name cannot be empty")
177+
178+
full_name = f"{PROMETHEUS_METRIC_PREFIX}{metric_name.lower()}"
179+
# Validate the full metric name against the regex
180+
if not PROMETHEUS_METRIC_NAME_REGEX.match(full_name):
181+
raise ValueError(
182+
f"Invalid Prometheus metric name: '{metric_name}'. "
183+
f"Metric names must start with a lowercase letter, "
184+
f"underscore, or colon, and contain only lowercase letters, "
185+
f"numbers, underscores, and colons."
186+
)
187+
188+
return full_name
168189

169190
@staticmethod
170191
def generate_uuid(content: str) -> uuid.UUID:

tests/service/prometheus/test_prometheus_publisher.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import pytest
66

7-
# from unittest.mock import Mock, MagicMock, patch
87
from prometheus_client import CollectorRegistry, Gauge
98
from src.service.constants import PROMETHEUS_METRIC_PREFIX
109
from src.service.payloads.metrics.base_metric_request import BaseMetricRequest
@@ -246,3 +245,57 @@ def test_invalid_gauge_parameters(
246245
match="Either 'request' or 'metric_name' & 'value' must be provided",
247246
):
248247
publisher.gauge(model_name="test_model", id=test_id, value=0.5)
248+
249+
def test_valid_metric_names(self, publisher: PrometheusPublisher):
250+
"""Test validation of valid Prometheus metric names."""
251+
valid_names = [
252+
"simple_metric",
253+
"metric_with_numbers123",
254+
"UPPERCASE_METRIC", # we convert to lowercase
255+
"metric:with:colons",
256+
"_underscore_start",
257+
"a1b2c3",
258+
"metric_name_with_3underscores"
259+
]
260+
261+
for name in valid_names:
262+
full_name = publisher._get_full_metric_name(name)
263+
expected = f"{PROMETHEUS_METRIC_PREFIX}{name.lower()}"
264+
assert full_name == expected
265+
266+
def test_invalid_metric_names(self, publisher: PrometheusPublisher):
267+
"""Test validation of invalid Prometheus metric names."""
268+
invalid_names = [
269+
"metric-with-hyphens",
270+
"metric with spaces",
271+
"metric@with@symbols",
272+
"metric.with.dots",
273+
"metric#with#hash",
274+
"metric%with%percent",
275+
"-starts_with_hyphen",
276+
"metric/with/slashes"
277+
]
278+
279+
for name in invalid_names:
280+
with pytest.raises(ValueError, match="Invalid Prometheus metric name"):
281+
publisher._get_full_metric_name(name)
282+
283+
def test_empty_metric_name_validation(self, publisher: PrometheusPublisher):
284+
with pytest.raises(ValueError, match="Metric name cannot be empty"):
285+
publisher._get_full_metric_name("")
286+
287+
def test_gauge_creation_with_invalid_metric_name(
288+
self, publisher: PrometheusPublisher, mock_request: MockMetricRequest
289+
):
290+
mock_request.metric_name = "invalid-metric-name"
291+
292+
test_id = uuid.uuid4()
293+
test_value = 0.5
294+
295+
with pytest.raises(ValueError, match="Invalid Prometheus metric name"):
296+
publisher.gauge(
297+
model_name="test_model",
298+
id=test_id,
299+
value=test_value,
300+
request=mock_request
301+
)

0 commit comments

Comments
 (0)