@@ -35,67 +35,78 @@ def fetch_metrics(url):
3535
3636def parse_metrics (metrics_text ):
3737 """
38- Parse Prometheus exposition text into a structured dict of :
38+ Parse Prometheus exposition text into a dict:
3939 metric_name → { description, type, labels: [<label_keys>] }
40- Robust against any ordering of # HELP/# TYPE and handles samples
41- both with and without labels.
40+
41+ - Strips empty `{}` on unlabelled samples
42+ - Propagates HELP/TYPE from base metrics onto _bucket, _count, _sum
43+ - Works regardless of # HELP / # TYPE order
4244 """
43- import re
44- import logging
45+ import re , logging
4546
46- # Gather HELP/type metadata
47- meta = {} # name → { 'description':…, 'type':… }
4847 lines = metrics_text .splitlines ()
48+
49+ # Gather HELP/TYPE metadata in any order
50+ meta = {} # name → { 'description': str, 'type': str }
4951 for line in lines :
5052 if line .startswith ("# HELP" ):
5153 m = re .match (r"# HELP\s+(\S+)\s+(.+)" , line )
5254 if m :
53- name , desc = m .group ( 1 ), m . group ( 2 )
55+ name , desc = m .groups ( )
5456 meta .setdefault (name , {})['description' ] = desc
5557 elif line .startswith ("# TYPE" ):
5658 m = re .match (r"# TYPE\s+(\S+)\s+(\S+)" , line )
5759 if m :
58- name , t = m .group ( 1 ), m . group ( 2 )
59- meta .setdefault (name , {})['type' ] = t
60+ name , mtype = m .groups ( )
61+ meta .setdefault (name , {})['type' ] = mtype
6062
61- # Collect label keys from every sample line
63+ # Collect label keys from _every_ sample line
6264 label_map = {} # name → set(label_keys)
6365 for line in lines :
6466 if line .startswith ("#" ):
6567 continue
6668
67- # try labelled sample: metric {a="1",b="2"} 42
69+ # labelled: foo {a="1",b="2"} 42
6870 m_lbl = re .match (r"^(\S+)\{(.+?)\}\s+(.+)" , line )
6971 if m_lbl :
70- name , labels_str = m_lbl .group ( 1 ), m_lbl . group ( 2 )
71- keys = [kv .split ("=" , 1 )[0 ] for kv in labels_str .split ("," )]
72+ name , labels_str , _ = m_lbl .groups ( )
73+ keys = [kv .split ("=" ,1 )[0 ] for kv in labels_str .split ("," )]
7274 else :
73- # fallback to unlabelled: metric 42
74- m_unlbl = re .match (r"^(\S+)\s+(.+)" , line )
75- if m_unlbl :
76- name , keys = m_unlbl .group (1 ), []
77- else :
75+ # unlabelled, maybe with stray {}: foo{} 42 or just foo 42
76+ m_unlbl = re .match (r"^(\S+?)(?:\{\})?\s+(.+)" , line )
77+ if not m_unlbl :
7878 continue
79+ name , _ = m_unlbl .groups ()
80+ keys = []
7981
8082 label_map .setdefault (name , set ()).update (keys )
8183
82- # Merge into final structure, warn on missing pieces
84+ # Propagate HELP/TYPE from base histograms/summaries
85+ for series in list (label_map ):
86+ for suffix in ("_bucket" , "_count" , "_sum" ):
87+ if series .endswith (suffix ):
88+ base = series [:- len (suffix )]
89+ if base in meta :
90+ meta .setdefault (series , {}).update (meta [base ])
91+ break
92+
93+ # Merge into final metrics dict, with warnings
8394 metrics = {}
8495 all_names = set (meta ) | set (label_map )
8596 for name in sorted (all_names ):
8697 desc = meta .get (name , {}).get ("description" )
87- t = meta .get (name , {}).get ("type" )
98+ mtype = meta .get (name , {}).get ("type" )
8899 labels = sorted (label_map .get (name , []))
89100
90101 if desc is None :
91102 logging .warning (f"Metric '{ name } ' has samples but no # HELP." )
92103 desc = ""
93- if t is None :
104+ if mtype is None :
94105 logging .warning (f"Metric '{ name } ' has no # TYPE entry." )
95106
96107 metrics [name ] = {
97108 "description" : desc ,
98- "type" : t ,
109+ "type" : mtype ,
99110 "labels" : labels
100111 }
101112
0 commit comments