Skip to content

Commit 60c0ead

Browse files
committed
format and handle quotes
1 parent 83e1e81 commit 60c0ead

3 files changed

Lines changed: 73 additions & 30 deletions

File tree

datadog_checks_base/datadog_checks/base/checks/openmetrics/parser_optimizations.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,52 @@
1818
import prometheus_client.parser as _prom_parser
1919

2020

21+
def _is_char_escaped(text, pos):
22+
"""Return True if the character at pos is preceded by an odd number of backslashes."""
23+
num_bslashes = 0
24+
while pos > num_bslashes and text[pos - 1 - num_bslashes] == '\\':
25+
num_bslashes += 1
26+
return num_bslashes % 2 == 1
27+
28+
2129
def _next_unquoted_char(text, chs, startidx=0):
22-
"""Find the next occurrence of any character in chs."""
30+
"""Find the next unquoted occurrence of any character in chs.
31+
32+
Uses str.find() to jump to candidate characters, skipping over quoted regions.
33+
"""
2334
if chs is None:
2435
chs = string.whitespace
2536

26-
best = -1
27-
for ch in chs:
28-
p = text.find(ch, startidx)
29-
if p != -1 and (best == -1 or p < best):
30-
best = p
31-
return best
37+
i = startidx
38+
n = len(text)
39+
40+
while i < n:
41+
best = -1
42+
for ch in chs:
43+
p = text.find(ch, i)
44+
if p != -1 and (best == -1 or p < best):
45+
best = p
46+
47+
# Find the next unescaped opening quote
48+
q = text.find('"', i)
49+
while q != -1 and _is_char_escaped(text, q):
50+
q = text.find('"', q + 1)
51+
52+
# If no quote comes before the best candidate, return it directly
53+
if q == -1 or (best != -1 and best < q):
54+
return best
55+
56+
# A quoted region starts before the candidate; skip over it
57+
close = text.find('"', q + 1)
58+
while close != -1 and _is_char_escaped(text, close):
59+
close = text.find('"', close + 1)
60+
61+
if close == -1:
62+
return -1
63+
64+
i = close + 1
65+
66+
return -1
3267

3368

3469
def apply():

datadog_checks_base/datadog_checks/base/checks/openmetrics/v2/scraper/base_scraper.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
from prometheus_client import Metric
1414
from prometheus_client.openmetrics.parser import text_fd_to_metric_families as parse_openmetrics
1515
from prometheus_client.parser import text_fd_to_metric_families as parse_prometheus
16-
17-
import datadog_checks.base.checks.openmetrics.parser_optimizations # noqa: F401
1816
from requests.exceptions import ConnectionError
1917

18+
import datadog_checks.base.checks.openmetrics.parser_optimizations # noqa: F401
2019
from datadog_checks.base.agent import datadog_agent
2120
from datadog_checks.base.checks.openmetrics.v2.first_scrape_handler import first_scrape_handler
2221
from datadog_checks.base.checks.openmetrics.v2.labels import LabelAggregator, get_label_normalizer

datadog_checks_base/tests/base/checks/openmetrics/test_parser_optimizations.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
original prometheus_client implementation across representative inputs.
88
"""
99

10-
import pytest
11-
1210
from datadog_checks.base.checks.openmetrics.parser_optimizations import (
1311
_next_unquoted_char,
1412
)
@@ -71,6 +69,20 @@ def test_last_char_is_target(self):
7169
def test_multiple_occurrences_returns_first(self):
7270
assert _next_unquoted_char('a{b{c', '{') == 1
7371

72+
def test_skip_target_char_inside_quotes(self):
73+
# comma inside quoted value must not be returned
74+
assert _next_unquoted_char('a="apn,gw",b', ',') == 10
75+
76+
def test_skip_brace_inside_quotes(self):
77+
assert _next_unquoted_char('label="val}ue"}', '}') == 14
78+
79+
def test_skip_equals_inside_quotes(self):
80+
assert _next_unquoted_char('label="a=b"} 1', '}') == 11
81+
82+
def test_escaped_quote_not_treated_as_delimiter(self):
83+
# backslash-escaped quote does not close the quoted region
84+
assert _next_unquoted_char(r'label="val\"still,inside",next', ',') == 25
85+
7486

7587
class TestNextUnquotedCharWithRealMetrics:
7688
"""Tests using real Prometheus metric line patterns."""
@@ -113,11 +125,7 @@ class TestParseFullMetricText:
113125
def test_parse_simple_metrics(self):
114126
from prometheus_client.parser import text_string_to_metric_families
115127

116-
text = (
117-
'# HELP test_gauge A test gauge.\n'
118-
'# TYPE test_gauge gauge\n'
119-
'test_gauge 42\n'
120-
)
128+
text = '# HELP test_gauge A test gauge.\n# TYPE test_gauge gauge\ntest_gauge 42\n'
121129
families = list(text_string_to_metric_families(text))
122130
assert len(families) == 1
123131
assert families[0].name == 'test_gauge'
@@ -159,11 +167,7 @@ def test_parse_histogram(self):
159167
def test_parse_escaped_label_value(self):
160168
from prometheus_client.parser import text_string_to_metric_families
161169

162-
text = (
163-
'# HELP test_metric A test.\n'
164-
'# TYPE test_metric gauge\n'
165-
'test_metric{label="value with \\"quotes\\""} 1\n'
166-
)
170+
text = '# HELP test_metric A test.\n# TYPE test_metric gauge\ntest_metric{label="value with \\"quotes\\""} 1\n'
167171
families = list(text_string_to_metric_families(text))
168172
assert len(families) == 1
169173
assert families[0].samples[0].labels == {'label': 'value with "quotes"'}
@@ -187,21 +191,26 @@ def test_parse_multiple_families(self):
187191
def test_parse_empty_label_value(self):
188192
from prometheus_client.parser import text_string_to_metric_families
189193

190-
text = (
191-
'# HELP test_metric A test.\n'
192-
'# TYPE test_metric gauge\n'
193-
'test_metric{label=""} 1\n'
194-
)
194+
text = '# HELP test_metric A test.\n# TYPE test_metric gauge\ntest_metric{label=""} 1\n'
195195
families = list(text_string_to_metric_families(text))
196196
assert families[0].samples[0].labels == {'label': ''}
197197

198198
def test_parse_newline_in_label_value(self):
199199
from prometheus_client.parser import text_string_to_metric_families
200200

201+
text = '# HELP test_metric A test.\n# TYPE test_metric gauge\ntest_metric{label="line1\\nline2"} 1\n'
202+
families = list(text_string_to_metric_families(text))
203+
assert families[0].samples[0].labels == {'label': 'line1\nline2'}
204+
205+
def test_parse_comma_in_label_value(self):
206+
from prometheus_client.parser import text_string_to_metric_families
207+
201208
text = (
202-
'# HELP test_metric A test.\n'
203-
'# TYPE test_metric gauge\n'
204-
'test_metric{label="line1\\nline2"} 1\n'
209+
'# HELP apn_active_connections Active connections.\n'
210+
'# TYPE apn_active_connections gauge\n'
211+
'apn_active_connections{func="apn,gw",proto="tcp"} 8\n'
205212
)
206213
families = list(text_string_to_metric_families(text))
207-
assert families[0].samples[0].labels == {'label': 'line1\nline2'}
214+
assert len(families) == 1
215+
assert families[0].samples[0].labels == {'func': 'apn,gw', 'proto': 'tcp'}
216+
assert families[0].samples[0].value == 8

0 commit comments

Comments
 (0)