parser.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. #!/usr/bin/env python
  2. import io as StringIO
  3. import math
  4. import re
  5. from ..metrics_core import Metric
  6. from ..parser import (
  7. _last_unquoted_char, _next_unquoted_char, _parse_value, _split_quoted,
  8. _unquote_unescape, parse_labels,
  9. )
  10. from ..samples import BucketSpan, Exemplar, NativeHistogram, Sample, Timestamp
  11. from ..utils import floatToGoString
  12. from ..validation import _is_valid_legacy_metric_name, _validate_metric_name
  13. def text_string_to_metric_families(text):
  14. """Parse Openmetrics text format from a unicode string.
  15. See text_fd_to_metric_families.
  16. """
  17. yield from text_fd_to_metric_families(StringIO.StringIO(text))
  18. _CANONICAL_NUMBERS = {float("inf")}
  19. def _isUncanonicalNumber(s):
  20. f = float(s)
  21. if f not in _CANONICAL_NUMBERS:
  22. return False # Only the canonical numbers are required to be canonical.
  23. return s != floatToGoString(f)
  24. ESCAPE_SEQUENCES = {
  25. '\\\\': '\\',
  26. '\\n': '\n',
  27. '\\"': '"',
  28. }
  29. def _replace_escape_sequence(match):
  30. return ESCAPE_SEQUENCES[match.group(0)]
  31. ESCAPING_RE = re.compile(r'\\[\\n"]')
  32. def _replace_escaping(s):
  33. return ESCAPING_RE.sub(_replace_escape_sequence, s)
  34. def _unescape_help(text):
  35. result = []
  36. slash = False
  37. for char in text:
  38. if slash:
  39. if char == '\\':
  40. result.append('\\')
  41. elif char == '"':
  42. result.append('"')
  43. elif char == 'n':
  44. result.append('\n')
  45. else:
  46. result.append('\\' + char)
  47. slash = False
  48. else:
  49. if char == '\\':
  50. slash = True
  51. else:
  52. result.append(char)
  53. if slash:
  54. result.append('\\')
  55. return ''.join(result)
  56. def _parse_timestamp(timestamp):
  57. timestamp = ''.join(timestamp)
  58. if not timestamp:
  59. return None
  60. if timestamp != timestamp.strip() or '_' in timestamp:
  61. raise ValueError(f"Invalid timestamp: {timestamp!r}")
  62. try:
  63. # Simple int.
  64. return Timestamp(int(timestamp), 0)
  65. except ValueError:
  66. try:
  67. # aaaa.bbbb. Nanosecond resolution supported.
  68. parts = timestamp.split('.', 1)
  69. return Timestamp(int(parts[0]), int(parts[1][:9].ljust(9, "0")))
  70. except ValueError:
  71. # Float.
  72. ts = float(timestamp)
  73. if math.isnan(ts) or math.isinf(ts):
  74. raise ValueError(f"Invalid timestamp: {timestamp!r}")
  75. return ts
  76. def _is_character_escaped(s, charpos):
  77. num_bslashes = 0
  78. while (charpos > num_bslashes
  79. and s[charpos - 1 - num_bslashes] == '\\'):
  80. num_bslashes += 1
  81. return num_bslashes % 2 == 1
  82. def _parse_sample(text):
  83. separator = " # "
  84. # Detect the labels in the text
  85. label_start = _next_unquoted_char(text, '{')
  86. if label_start == -1 or separator in text[:label_start]:
  87. # We don't have labels, but there could be an exemplar.
  88. name_end = _next_unquoted_char(text, ' ')
  89. name = text[:name_end]
  90. if not _is_valid_legacy_metric_name(name):
  91. raise ValueError("invalid metric name:" + text)
  92. # Parse the remaining text after the name
  93. remaining_text = text[name_end + 1:]
  94. value, timestamp, exemplar = _parse_remaining_text(remaining_text)
  95. return Sample(name, {}, value, timestamp, exemplar)
  96. name = text[:label_start]
  97. label_end = _next_unquoted_char(text, '}')
  98. labels = parse_labels(text[label_start + 1:label_end], True)
  99. if not name:
  100. # Name might be in the labels
  101. if '__name__' not in labels:
  102. raise ValueError
  103. name = labels['__name__']
  104. del labels['__name__']
  105. elif '__name__' in labels:
  106. raise ValueError("metric name specified more than once")
  107. # Parsing labels succeeded, continue parsing the remaining text
  108. remaining_text = text[label_end + 2:]
  109. value, timestamp, exemplar = _parse_remaining_text(remaining_text)
  110. return Sample(name, labels, value, timestamp, exemplar)
  111. def _parse_remaining_text(text):
  112. split_text = text.split(" ", 1)
  113. val = _parse_value(split_text[0])
  114. if len(split_text) == 1:
  115. # We don't have timestamp or exemplar
  116. return val, None, None
  117. timestamp = []
  118. exemplar_value = []
  119. exemplar_timestamp = []
  120. exemplar_labels = None
  121. state = 'timestamp'
  122. text = split_text[1]
  123. it = iter(text)
  124. in_quotes = False
  125. for char in it:
  126. if char == '"':
  127. in_quotes = not in_quotes
  128. if in_quotes:
  129. continue
  130. if state == 'timestamp':
  131. if char == '#' and not timestamp:
  132. state = 'exemplarspace'
  133. elif char == ' ':
  134. state = 'exemplarhash'
  135. else:
  136. timestamp.append(char)
  137. elif state == 'exemplarhash':
  138. if char == '#':
  139. state = 'exemplarspace'
  140. else:
  141. raise ValueError("Invalid line: " + text)
  142. elif state == 'exemplarspace':
  143. if char == ' ':
  144. state = 'exemplarstartoflabels'
  145. else:
  146. raise ValueError("Invalid line: " + text)
  147. elif state == 'exemplarstartoflabels':
  148. if char == '{':
  149. label_start = _next_unquoted_char(text, '{')
  150. label_end = _last_unquoted_char(text, '}')
  151. exemplar_labels = parse_labels(text[label_start + 1:label_end], True)
  152. state = 'exemplarparsedlabels'
  153. else:
  154. raise ValueError("Invalid line: " + text)
  155. elif state == 'exemplarparsedlabels':
  156. if char == '}':
  157. state = 'exemplarvaluespace'
  158. elif state == 'exemplarvaluespace':
  159. if char == ' ':
  160. state = 'exemplarvalue'
  161. else:
  162. raise ValueError("Invalid line: " + text)
  163. elif state == 'exemplarvalue':
  164. if char == ' ' and not exemplar_value:
  165. raise ValueError("Invalid line: " + text)
  166. elif char == ' ':
  167. state = 'exemplartimestamp'
  168. else:
  169. exemplar_value.append(char)
  170. elif state == 'exemplartimestamp':
  171. exemplar_timestamp.append(char)
  172. # Trailing space after value.
  173. if state == 'timestamp' and not timestamp:
  174. raise ValueError("Invalid line: " + text)
  175. # Trailing space after value.
  176. if state == 'exemplartimestamp' and not exemplar_timestamp:
  177. raise ValueError("Invalid line: " + text)
  178. # Incomplete exemplar.
  179. if state in ['exemplarhash', 'exemplarspace', 'exemplarstartoflabels', 'exemplarparsedlabels']:
  180. raise ValueError("Invalid line: " + text)
  181. ts = _parse_timestamp(timestamp)
  182. exemplar = None
  183. if exemplar_labels is not None:
  184. exemplar_length = sum(len(k) + len(v) for k, v in exemplar_labels.items())
  185. if exemplar_length > 128:
  186. raise ValueError("Exemplar labels are too long: " + text)
  187. exemplar = Exemplar(
  188. exemplar_labels,
  189. _parse_value(exemplar_value),
  190. _parse_timestamp(exemplar_timestamp),
  191. )
  192. return val, ts, exemplar
  193. def _parse_nh_sample(text, suffixes):
  194. """Determines if the line has a native histogram sample, and parses it if so."""
  195. labels_start = _next_unquoted_char(text, '{')
  196. labels_end = -1
  197. # Finding a native histogram sample requires careful parsing of
  198. # possibly-quoted text, which can appear in metric names, label names, and
  199. # values.
  200. #
  201. # First, we need to determine if there are metric labels. Find the space
  202. # between the metric definition and the rest of the line. Look for unquoted
  203. # space or {.
  204. i = 0
  205. has_metric_labels = False
  206. i = _next_unquoted_char(text, ' {')
  207. if i == -1:
  208. return
  209. # If the first unquoted char was a {, then that is the metric labels (which
  210. # could contain a UTF-8 metric name).
  211. if text[i] == '{':
  212. has_metric_labels = True
  213. # Consume the labels -- jump ahead to the close bracket.
  214. labels_end = i = _next_unquoted_char(text, '}', i)
  215. if labels_end == -1:
  216. raise ValueError
  217. # If there is no subsequent unquoted {, then it's definitely not a nh.
  218. nh_value_start = _next_unquoted_char(text, '{', i + 1)
  219. if nh_value_start == -1:
  220. return
  221. # Edge case: if there is an unquoted # between the metric definition and the {,
  222. # then this is actually an exemplar
  223. exemplar = _next_unquoted_char(text, '#', i + 1)
  224. if exemplar != -1 and exemplar < nh_value_start:
  225. return
  226. nh_value_end = _next_unquoted_char(text, '}', nh_value_start)
  227. if nh_value_end == -1:
  228. raise ValueError
  229. if has_metric_labels:
  230. labelstext = text[labels_start + 1:labels_end]
  231. labels = parse_labels(labelstext, True)
  232. name_end = labels_start
  233. name = text[:name_end]
  234. if name.endswith(suffixes):
  235. raise ValueError("the sample name of a native histogram with labels should have no suffixes", name)
  236. if not name:
  237. # Name might be in the labels
  238. if '__name__' not in labels:
  239. raise ValueError
  240. name = labels['__name__']
  241. del labels['__name__']
  242. # Edge case: the only "label" is the name definition.
  243. if not labels:
  244. labels = None
  245. nh_value = text[nh_value_start:]
  246. nat_hist_value = _parse_nh_struct(nh_value)
  247. return Sample(name, labels, None, None, None, nat_hist_value)
  248. # check if it's a native histogram
  249. else:
  250. nh_value = text[nh_value_start:]
  251. name_end = nh_value_start - 1
  252. name = text[:name_end]
  253. if name.endswith(suffixes):
  254. raise ValueError("the sample name of a native histogram should have no suffixes", name)
  255. # Not possible for UTF-8 name here, that would have been caught as having a labelset.
  256. nat_hist_value = _parse_nh_struct(nh_value)
  257. return Sample(name, None, None, None, None, nat_hist_value)
  258. def _parse_nh_struct(text):
  259. pattern = r'(\w+):\s*([^,}]+)'
  260. re_spans = re.compile(r'(positive_spans|negative_spans):\[(\d+:\d+(,\d+:\d+)*)\]')
  261. re_deltas = re.compile(r'(positive_deltas|negative_deltas):\[(-?\d+(?:,-?\d+)*)\]')
  262. items = dict(re.findall(pattern, text))
  263. span_matches = re_spans.findall(text)
  264. deltas = dict(re_deltas.findall(text))
  265. count_value = int(items['count'])
  266. sum_value = int(items['sum'])
  267. schema = int(items['schema'])
  268. zero_threshold = float(items['zero_threshold'])
  269. zero_count = int(items['zero_count'])
  270. pos_spans = _compose_spans(span_matches, 'positive_spans')
  271. neg_spans = _compose_spans(span_matches, 'negative_spans')
  272. pos_deltas = _compose_deltas(deltas, 'positive_deltas')
  273. neg_deltas = _compose_deltas(deltas, 'negative_deltas')
  274. return NativeHistogram(
  275. count_value=count_value,
  276. sum_value=sum_value,
  277. schema=schema,
  278. zero_threshold=zero_threshold,
  279. zero_count=zero_count,
  280. pos_spans=pos_spans,
  281. neg_spans=neg_spans,
  282. pos_deltas=pos_deltas,
  283. neg_deltas=neg_deltas
  284. )
  285. def _compose_spans(span_matches, spans_name):
  286. """Takes a list of span matches (expected to be a list of tuples) and a string
  287. (the expected span list name) and processes the list so that the values extracted
  288. from the span matches can be used to compose a tuple of BucketSpan objects"""
  289. spans = {}
  290. for match in span_matches:
  291. # Extract the key from the match (first element of the tuple).
  292. key = match[0]
  293. # Extract the value from the match (second element of the tuple).
  294. # Split the value string by commas to get individual pairs,
  295. # split each pair by ':' to get start and end, and convert them to integers.
  296. value = [tuple(map(int, pair.split(':'))) for pair in match[1].split(',')]
  297. # Store the processed value in the spans dictionary with the key.
  298. spans[key] = value
  299. if spans_name not in spans:
  300. return None
  301. out_spans = []
  302. # Iterate over each start and end tuple in the list of tuples for the specified spans_name.
  303. for start, end in spans[spans_name]:
  304. # Compose a BucketSpan object with the start and end values
  305. # and append it to the out_spans list.
  306. out_spans.append(BucketSpan(start, end))
  307. # Convert to tuple
  308. out_spans_tuple = tuple(out_spans)
  309. return out_spans_tuple
  310. def _compose_deltas(deltas, deltas_name):
  311. """Takes a list of deltas matches (a dictionary) and a string (the expected delta list name),
  312. and processes its elements to compose a tuple of integers representing the deltas"""
  313. if deltas_name not in deltas:
  314. return None
  315. out_deltas = deltas.get(deltas_name)
  316. if out_deltas is not None and out_deltas.strip():
  317. elems = out_deltas.split(',')
  318. # Convert each element in the list elems to an integer
  319. # after stripping whitespace and create a tuple from these integers.
  320. out_deltas_tuple = tuple(int(x.strip()) for x in elems)
  321. return out_deltas_tuple
  322. def _group_for_sample(sample, name, typ):
  323. if typ == 'info':
  324. # We can't distinguish between groups for info metrics.
  325. return {}
  326. if typ == 'summary' and sample.name == name:
  327. d = sample.labels.copy()
  328. del d['quantile']
  329. return d
  330. if typ == 'stateset':
  331. d = sample.labels.copy()
  332. del d[name]
  333. return d
  334. if typ in ['histogram', 'gaugehistogram'] and sample.name == name + '_bucket':
  335. d = sample.labels.copy()
  336. del d['le']
  337. return d
  338. return sample.labels
  339. def _check_histogram(samples, name):
  340. group = None
  341. timestamp = None
  342. def do_checks():
  343. if bucket != float('+Inf'):
  344. raise ValueError("+Inf bucket missing: " + name)
  345. if count is not None and value != count:
  346. raise ValueError("Count does not match +Inf value: " + name)
  347. if has_sum and count is None:
  348. raise ValueError("_count must be present if _sum is present: " + name)
  349. if has_gsum and count is None:
  350. raise ValueError("_gcount must be present if _gsum is present: " + name)
  351. if not (has_sum or has_gsum) and count is not None:
  352. raise ValueError("_sum/_gsum must be present if _count is present: " + name)
  353. if has_negative_buckets and has_sum:
  354. raise ValueError("Cannot have _sum with negative buckets: " + name)
  355. if not has_negative_buckets and has_negative_gsum:
  356. raise ValueError("Cannot have negative _gsum with non-negative buckets: " + name)
  357. for s in samples:
  358. suffix = s.name[len(name):]
  359. g = _group_for_sample(s, name, 'histogram')
  360. if len(suffix) == 0:
  361. continue
  362. if g != group or s.timestamp != timestamp:
  363. if group is not None:
  364. do_checks()
  365. count = None
  366. bucket = None
  367. has_negative_buckets = False
  368. has_sum = False
  369. has_gsum = False
  370. has_negative_gsum = False
  371. value = 0
  372. group = g
  373. timestamp = s.timestamp
  374. if suffix == '_bucket':
  375. b = float(s.labels['le'])
  376. if b < 0:
  377. has_negative_buckets = True
  378. if bucket is not None and b <= bucket:
  379. raise ValueError("Buckets out of order: " + name)
  380. if s.value < value:
  381. raise ValueError("Bucket values out of order: " + name)
  382. bucket = b
  383. value = s.value
  384. elif suffix in ['_count', '_gcount']:
  385. count = s.value
  386. elif suffix in ['_sum']:
  387. has_sum = True
  388. elif suffix in ['_gsum']:
  389. has_gsum = True
  390. if s.value < 0:
  391. has_negative_gsum = True
  392. if group is not None:
  393. do_checks()
  394. def text_fd_to_metric_families(fd):
  395. """Parse Prometheus text format from a file descriptor.
  396. This is a laxer parser than the main Go parser,
  397. so successful parsing does not imply that the parsed
  398. text meets the specification.
  399. Yields Metric's.
  400. """
  401. name = None
  402. allowed_names = []
  403. eof = False
  404. seen_names = set()
  405. type_suffixes = {
  406. 'counter': ['_total', '_created'],
  407. 'summary': ['', '_count', '_sum', '_created'],
  408. 'histogram': ['_count', '_sum', '_bucket', '_created'],
  409. 'gaugehistogram': ['_gcount', '_gsum', '_bucket'],
  410. 'info': ['_info'],
  411. }
  412. def build_metric(name, documentation, typ, unit, samples):
  413. if typ is None:
  414. typ = 'unknown'
  415. for suffix in set(type_suffixes.get(typ, []) + [""]):
  416. if name + suffix in seen_names:
  417. raise ValueError("Clashing name: " + name + suffix)
  418. seen_names.add(name + suffix)
  419. if documentation is None:
  420. documentation = ''
  421. if unit is None:
  422. unit = ''
  423. if unit and not name.endswith("_" + unit):
  424. raise ValueError("Unit does not match metric name: " + name)
  425. if unit and typ in ['info', 'stateset']:
  426. raise ValueError("Units not allowed for this metric type: " + name)
  427. if typ in ['histogram', 'gaugehistogram']:
  428. _check_histogram(samples, name)
  429. _validate_metric_name(name)
  430. metric = Metric(name, documentation, typ, unit)
  431. # TODO: check labelvalues are valid utf8
  432. metric.samples = samples
  433. return metric
  434. is_nh = False
  435. typ = None
  436. for line in fd:
  437. if line[-1] == '\n':
  438. line = line[:-1]
  439. if eof:
  440. raise ValueError("Received line after # EOF: " + line)
  441. if not line:
  442. raise ValueError("Received blank line")
  443. if line == '# EOF':
  444. eof = True
  445. elif line.startswith('#'):
  446. parts = _split_quoted(line, ' ', 3)
  447. if len(parts) < 4:
  448. raise ValueError("Invalid line: " + line)
  449. candidate_name, quoted = _unquote_unescape(parts[2])
  450. if not quoted and not _is_valid_legacy_metric_name(candidate_name):
  451. raise ValueError
  452. if candidate_name == name and samples:
  453. raise ValueError("Received metadata after samples: " + line)
  454. if candidate_name != name:
  455. if name is not None:
  456. yield build_metric(name, documentation, typ, unit, samples)
  457. # New metric
  458. name = candidate_name
  459. unit = None
  460. typ = None
  461. documentation = None
  462. group = None
  463. seen_groups = set()
  464. group_timestamp = None
  465. group_timestamp_samples = set()
  466. samples = []
  467. allowed_names = [candidate_name]
  468. if parts[1] == 'HELP':
  469. if documentation is not None:
  470. raise ValueError("More than one HELP for metric: " + line)
  471. documentation = _unescape_help(parts[3])
  472. elif parts[1] == 'TYPE':
  473. if typ is not None:
  474. raise ValueError("More than one TYPE for metric: " + line)
  475. typ = parts[3]
  476. if typ == 'untyped':
  477. raise ValueError("Invalid TYPE for metric: " + line)
  478. allowed_names = [name + n for n in type_suffixes.get(typ, [''])]
  479. elif parts[1] == 'UNIT':
  480. if unit is not None:
  481. raise ValueError("More than one UNIT for metric: " + line)
  482. unit = parts[3]
  483. else:
  484. raise ValueError("Invalid line: " + line)
  485. else:
  486. if typ == 'histogram':
  487. # set to true to account for native histograms naming exceptions/sanitizing differences
  488. is_nh = True
  489. sample = _parse_nh_sample(line, tuple(type_suffixes['histogram']))
  490. # It's not a native histogram
  491. if sample is None:
  492. is_nh = False
  493. sample = _parse_sample(line)
  494. else:
  495. is_nh = False
  496. sample = _parse_sample(line)
  497. if sample.name not in allowed_names and not is_nh:
  498. if name is not None:
  499. yield build_metric(name, documentation, typ, unit, samples)
  500. # Start an unknown metric.
  501. candidate_name, quoted = _unquote_unescape(sample.name)
  502. if not quoted and not _is_valid_legacy_metric_name(candidate_name):
  503. raise ValueError
  504. name = candidate_name
  505. documentation = None
  506. unit = None
  507. typ = 'unknown'
  508. samples = []
  509. group = None
  510. group_timestamp = None
  511. group_timestamp_samples = set()
  512. seen_groups = set()
  513. allowed_names = [sample.name]
  514. if typ == 'stateset' and name not in sample.labels:
  515. raise ValueError("Stateset missing label: " + line)
  516. if (name + '_bucket' == sample.name
  517. and (sample.labels.get('le', "NaN") == "NaN"
  518. or _isUncanonicalNumber(sample.labels['le']))):
  519. raise ValueError("Invalid le label: " + line)
  520. if (name + '_bucket' == sample.name
  521. and (not isinstance(sample.value, int) and not sample.value.is_integer())):
  522. raise ValueError("Bucket value must be an integer: " + line)
  523. if ((name + '_count' == sample.name or name + '_gcount' == sample.name)
  524. and (not isinstance(sample.value, int) and not sample.value.is_integer())):
  525. raise ValueError("Count value must be an integer: " + line)
  526. if (typ == 'summary' and name == sample.name
  527. and (not (0 <= float(sample.labels.get('quantile', -1)) <= 1)
  528. or _isUncanonicalNumber(sample.labels['quantile']))):
  529. raise ValueError("Invalid quantile label: " + line)
  530. if not is_nh:
  531. g = tuple(sorted(_group_for_sample(sample, name, typ).items()))
  532. if group is not None and g != group and g in seen_groups:
  533. raise ValueError("Invalid metric grouping: " + line)
  534. if group is not None and g == group:
  535. if (sample.timestamp is None) != (group_timestamp is None):
  536. raise ValueError("Mix of timestamp presence within a group: " + line)
  537. if group_timestamp is not None and group_timestamp > sample.timestamp and typ != 'info':
  538. raise ValueError("Timestamps went backwards within a group: " + line)
  539. else:
  540. group_timestamp_samples = set()
  541. series_id = (sample.name, tuple(sorted(sample.labels.items())))
  542. if sample.timestamp != group_timestamp or series_id not in group_timestamp_samples:
  543. # Not a duplicate due to timestamp truncation.
  544. samples.append(sample)
  545. group_timestamp_samples.add(series_id)
  546. group = g
  547. group_timestamp = sample.timestamp
  548. seen_groups.add(g)
  549. else:
  550. samples.append(sample)
  551. if typ == 'stateset' and sample.value not in [0, 1]:
  552. raise ValueError("Stateset samples can only have values zero and one: " + line)
  553. if typ == 'info' and sample.value != 1:
  554. raise ValueError("Info samples can only have value one: " + line)
  555. if typ == 'summary' and name == sample.name and sample.value < 0:
  556. raise ValueError("Quantile values cannot be negative: " + line)
  557. if sample.name[len(name):] in ['_total', '_sum', '_count', '_bucket', '_gcount', '_gsum'] and math.isnan(
  558. sample.value):
  559. raise ValueError("Counter-like samples cannot be NaN: " + line)
  560. if sample.name[len(name):] in ['_total', '_sum', '_count', '_bucket', '_gcount'] and sample.value < 0:
  561. raise ValueError("Counter-like samples cannot be negative: " + line)
  562. if sample.exemplar and not (
  563. (typ in ['histogram', 'gaugehistogram'] and sample.name.endswith('_bucket'))
  564. or (typ in ['counter'] and sample.name.endswith('_total'))):
  565. raise ValueError("Invalid line only histogram/gaugehistogram buckets and counters can have exemplars: " + line)
  566. if name is not None:
  567. yield build_metric(name, documentation, typ, unit, samples)
  568. if not eof:
  569. raise ValueError("Missing # EOF at end")