Skip to content

Commit 48e2a85

Browse files
authored
feat: add channel frequency labels to metrics (#228)
Co-authored-by: Dennis Braun <itsDNNS@users.noreply.github.com>
1 parent 4723bcf commit 48e2a85

File tree

2 files changed

+95
-22
lines changed

2 files changed

+95
-22
lines changed

app/prometheus.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,46 @@
99
_HEALTH_MAP = {"good": 0, "tolerated": 1, "marginal": 2, "critical": 3}
1010

1111

12+
def _frequency_label(value):
13+
"""Normalize channel frequency for Prometheus labels.
14+
15+
Returns a string in MHz where possible, without the trailing unit.
16+
"""
17+
if value is None:
18+
return None
19+
20+
text = str(value).strip()
21+
if not text:
22+
return None
23+
24+
lowered = text.lower()
25+
if lowered.endswith("mhz"):
26+
text = text[:-3].strip()
27+
return text or None
28+
29+
try:
30+
numeric = float(text)
31+
except ValueError:
32+
return text
33+
34+
if abs(numeric) >= 1_000_000:
35+
numeric /= 1_000_000
36+
37+
if numeric.is_integer():
38+
return str(int(numeric))
39+
40+
return f"{numeric:.3f}".rstrip("0").rstrip(".")
41+
42+
43+
def _channel_labels(channel):
44+
"""Build Prometheus labels for a DOCSIS channel metric."""
45+
labels = {"channel_id": str(channel["channel_id"])}
46+
frequency = _frequency_label(channel.get("frequency"))
47+
if frequency is not None:
48+
labels["frequency"] = frequency
49+
return labels
50+
51+
1252
def _metric(lines, help_text, metric_type, name, value, labels=None):
1353
"""Append HELP, TYPE, and a single value line to lines list."""
1454
lines.append(f"# HELP {name} {help_text}")
@@ -102,7 +142,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
102142
ch_id = ch["channel_id"]
103143
if ch.get("power") is not None:
104144
_metric_value(lines, "docsight_downstream_power_dbmv", ch["power"],
105-
{"channel_id": str(ch_id)})
145+
_channel_labels(ch))
106146

107147
_metric_family_open(
108148
lines,
@@ -113,7 +153,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
113153
for ch in ds_channels:
114154
if ch.get("snr") is not None:
115155
_metric_value(lines, "docsight_downstream_snr_db", ch["snr"],
116-
{"channel_id": str(ch["channel_id"])})
156+
_channel_labels(ch))
117157

118158
_metric_family_open(
119159
lines,
@@ -124,7 +164,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
124164
for ch in ds_channels:
125165
_metric_value(lines, "docsight_downstream_corrected_errors_total",
126166
ch.get("correctable_errors", 0),
127-
{"channel_id": str(ch["channel_id"])})
167+
_channel_labels(ch))
128168

129169
_metric_family_open(
130170
lines,
@@ -135,7 +175,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
135175
for ch in ds_channels:
136176
_metric_value(lines, "docsight_downstream_uncorrected_errors_total",
137177
ch.get("uncorrectable_errors", 0),
138-
{"channel_id": str(ch["channel_id"])})
178+
_channel_labels(ch))
139179

140180
_metric_family_open(
141181
lines,
@@ -147,7 +187,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
147187
qam = _parse_qam_order(ch.get("modulation", ""))
148188
if qam is not None:
149189
_metric_value(lines, "docsight_downstream_modulation", qam,
150-
{"channel_id": str(ch["channel_id"])})
190+
_channel_labels(ch))
151191

152192
# --- Upstream channel metrics ---
153193
us_channels = analysis.get("us_channels", []) if analysis else []
@@ -162,7 +202,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
162202
for ch in us_channels:
163203
if ch.get("power") is not None:
164204
_metric_value(lines, "docsight_upstream_power_dbmv", ch["power"],
165-
{"channel_id": str(ch["channel_id"])})
205+
_channel_labels(ch))
166206

167207
_metric_family_open(
168208
lines,
@@ -174,7 +214,7 @@ def format_metrics(analysis, device_info, connection_info, last_poll_timestamp):
174214
qam = _parse_qam_order(ch.get("modulation", ""))
175215
if qam is not None:
176216
_metric_value(lines, "docsight_upstream_modulation", qam,
177-
{"channel_id": str(ch["channel_id"])})
217+
_channel_labels(ch))
178218

179219
# --- Device info ---
180220
if device_info is not None:

tests/test_prometheus.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,23 @@ def _has_metric_approx(output, prefix):
106106
class TestDownstreamChannelMetrics:
107107
def test_ds_power_dbmv(self):
108108
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
109-
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="1"} 3.0')
109+
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="1",frequency="474"} 3.0')
110110

111111
def test_ds_snr_db(self):
112112
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
113-
assert _has_metric(out, 'docsight_downstream_snr_db{channel_id="1"} 35.0')
113+
assert _has_metric(out, 'docsight_downstream_snr_db{channel_id="1",frequency="474"} 35.0')
114114

115115
def test_ds_corrected_errors_total(self):
116116
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
117-
assert _has_metric(out, 'docsight_downstream_corrected_errors_total{channel_id="1"} 100')
117+
assert _has_metric(out, 'docsight_downstream_corrected_errors_total{channel_id="1",frequency="474"} 100')
118118

119119
def test_ds_uncorrected_errors_total(self):
120120
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
121-
assert _has_metric(out, 'docsight_downstream_uncorrected_errors_total{channel_id="1"} 5')
121+
assert _has_metric(out, 'docsight_downstream_uncorrected_errors_total{channel_id="1",frequency="474"} 5')
122122

123123
def test_ds_modulation_256qam(self):
124124
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
125-
assert _has_metric(out, 'docsight_downstream_modulation{channel_id="1"} 256')
125+
assert _has_metric(out, 'docsight_downstream_modulation{channel_id="1",frequency="474"} 256')
126126

127127
def test_ds_snr_none_omits_line(self):
128128
analysis = {
@@ -146,7 +146,7 @@ def test_ds_snr_none_omits_line(self):
146146
"us_channels": [],
147147
}
148148
out = format_metrics(analysis, None, None, 0.0)
149-
assert not _has_metric_approx(out, 'docsight_downstream_snr_db{channel_id="5"}')
149+
assert not _has_metric_approx(out, 'docsight_downstream_snr_db{channel_id="5",frequency="474"}')
150150

151151
def test_ds_modulation_ofdm_omits_line(self):
152152
"""OFDM is not parseable as QAM order, so modulation line must be omitted."""
@@ -171,16 +171,16 @@ def test_ds_modulation_ofdm_omits_line(self):
171171
"us_channels": [],
172172
}
173173
out = format_metrics(analysis, None, None, 0.0)
174-
assert not _has_metric_approx(out, 'docsight_downstream_modulation{channel_id="7"}')
174+
assert not _has_metric_approx(out, 'docsight_downstream_modulation{channel_id="7",frequency="474"}')
175175

176176
def test_multiple_ds_channels_separate_lines(self):
177177
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
178-
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="1"} 3.0')
179-
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="2"} -1.5')
178+
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="1",frequency="474"} 3.0')
179+
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="2",frequency="482"} -1.5')
180180

181181
def test_ds_power_negative_value(self):
182182
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
183-
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="2"} -1.5')
183+
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="2",frequency="482"} -1.5')
184184

185185
def test_ds_has_help_comment(self):
186186
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
@@ -200,11 +200,11 @@ def test_ds_has_type_comment(self):
200200
class TestUpstreamChannelMetrics:
201201
def test_us_power_dbmv(self):
202202
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
203-
assert _has_metric(out, 'docsight_upstream_power_dbmv{channel_id="1"} 42.0')
203+
assert _has_metric(out, 'docsight_upstream_power_dbmv{channel_id="1",frequency="30"} 42.0')
204204

205205
def test_us_modulation_64qam(self):
206206
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)
207-
assert _has_metric(out, 'docsight_upstream_modulation{channel_id="1"} 64')
207+
assert _has_metric(out, 'docsight_upstream_modulation{channel_id="1",frequency="30"} 64')
208208

209209
def test_us_power_none_omits_line(self):
210210
analysis = {
@@ -225,7 +225,7 @@ def test_us_power_none_omits_line(self):
225225
}],
226226
}
227227
out = format_metrics(analysis, None, None, 0.0)
228-
assert not _has_metric_approx(out, 'docsight_upstream_power_dbmv{channel_id="3"}')
228+
assert not _has_metric_approx(out, 'docsight_upstream_power_dbmv{channel_id="3",frequency="30"}')
229229

230230
def test_multiple_us_channels_separate_lines(self):
231231
analysis = {
@@ -257,8 +257,41 @@ def test_multiple_us_channels_separate_lines(self):
257257
],
258258
}
259259
out = format_metrics(analysis, None, None, 0.0)
260-
assert _has_metric(out, 'docsight_upstream_power_dbmv{channel_id="1"} 42.0')
261-
assert _has_metric(out, 'docsight_upstream_power_dbmv{channel_id="2"} 45.0')
260+
assert _has_metric(out, 'docsight_upstream_power_dbmv{channel_id="1",frequency="30"} 42.0')
261+
assert _has_metric(out, 'docsight_upstream_power_dbmv{channel_id="2",frequency="38"} 45.0')
262+
263+
def test_frequency_label_normalizes_mhz_values(self):
264+
analysis = {
265+
"summary": {
266+
"ds_total": 1, "us_total": 1,
267+
"health": "good", "health_issues": [],
268+
"ds_correctable_errors": 0, "ds_uncorrectable_errors": 0,
269+
},
270+
"ds_channels": [{
271+
"channel_id": 9,
272+
"frequency": "114.0 MHz",
273+
"power": 2.5,
274+
"modulation": "256QAM",
275+
"snr": 36.0,
276+
"correctable_errors": 0,
277+
"uncorrectable_errors": 0,
278+
"docsis_version": "3.0",
279+
"health": "good",
280+
"health_detail": "",
281+
}],
282+
"us_channels": [{
283+
"channel_id": 2,
284+
"frequency": "44 MHz",
285+
"power": 43.0,
286+
"modulation": "64QAM",
287+
"docsis_version": "3.0",
288+
"health": "good",
289+
"health_detail": "",
290+
}],
291+
}
292+
out = format_metrics(analysis, None, None, 0.0)
293+
assert _has_metric(out, 'docsight_downstream_power_dbmv{channel_id="9",frequency="114.0"} 2.5')
294+
assert _has_metric(out, 'docsight_upstream_modulation{channel_id="2",frequency="44"} 64')
262295

263296
def test_us_has_help_comment(self):
264297
out = format_metrics(ANALYSIS_FULL, None, None, 0.0)

0 commit comments

Comments
 (0)