feat(grafana-metric-gateway): slice 1 — backend gateway transport
Route all prometheus widget queries through Grafana /api/ds/query instead of
direct Prom HTTP. PrometheusConfig: drop base_url, add grafana_url +
datasource_uid; secret grafana_api_key (required). PrometheusWidgetSource →
MetricSource with _gateway_query POST method. normalize_grafana_frames
recovered from 65bae95 + shared _dedup_label helper. Gateway-path status
check. Startup old-config validation. CHANGELOG migration note. All adapter
tests rewritten for POST /api/ds/query + Grafana frames mock. Backend: 331
pytest pass, ruff clean. Frontend: build green (unchanged in S1).
This commit is contained in:
@@ -6,6 +6,8 @@ import pytest
|
||||
|
||||
from media_library_viewer_api.widgets.prometheus_range import (
|
||||
WINDOW_PRESETS,
|
||||
_dedup_label,
|
||||
normalize_grafana_frames,
|
||||
normalize_prometheus_matrix,
|
||||
step_for_window,
|
||||
)
|
||||
@@ -104,3 +106,152 @@ class TestNormalizePrometheusMatrix:
|
||||
{"t": 1, "v": 3.5},
|
||||
{"t": 3, "v": None},
|
||||
]
|
||||
|
||||
|
||||
class TestDedupLabel:
|
||||
"""The shared label-dedup helper used by both normalizers (GM-104)."""
|
||||
|
||||
def test_first_use_returns_label_unchanged(self) -> None:
|
||||
seen: dict[str, int] = {}
|
||||
assert _dedup_label("value", seen) == "value"
|
||||
assert seen == {"value": 0}
|
||||
|
||||
def test_collision_appends_suffix(self) -> None:
|
||||
seen: dict[str, int] = {}
|
||||
assert _dedup_label("job=x", seen) == "job=x"
|
||||
assert _dedup_label("job=x", seen) == "job=x (1)"
|
||||
assert _dedup_label("job=x", seen) == "job=x (2)"
|
||||
|
||||
def test_different_labels_dont_collide(self) -> None:
|
||||
seen: dict[str, int] = {}
|
||||
assert _dedup_label("a", seen) == "a"
|
||||
assert _dedup_label("b", seen) == "b"
|
||||
|
||||
|
||||
class TestNormalizeGrafanaFrames:
|
||||
"""GM-104: frames normalizer recovered from 65bae95 + shared dedup."""
|
||||
|
||||
def test_empty_response(self) -> None:
|
||||
assert normalize_grafana_frames({"results": {}}) == []
|
||||
assert normalize_grafana_frames({}) == []
|
||||
|
||||
def test_single_frame_with_values(self) -> None:
|
||||
raw = {
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{
|
||||
"data": {"values": [[1000, 2000], [1.5, 2.5]]},
|
||||
"schema": {"fields": [{"name": "Time"}, {"name": "Value"}]},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
out = normalize_grafana_frames(raw)
|
||||
assert len(out) == 1
|
||||
assert out[0]["label"] == "Value"
|
||||
assert out[0]["points"] == [
|
||||
{"t": 1000, "v": 1.5},
|
||||
{"t": 2000, "v": 2.5},
|
||||
]
|
||||
|
||||
def test_display_name_takes_priority(self) -> None:
|
||||
raw = {
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{
|
||||
"data": {"values": [[100, 200], [0.75, 0.80]]},
|
||||
"schema": {
|
||||
"fields": [
|
||||
{"name": "Time"},
|
||||
{
|
||||
"name": "Value",
|
||||
"labels": {"instance": "host:9100"},
|
||||
"config": {"displayName": "CPU Usage"},
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
out = normalize_grafana_frames(raw)
|
||||
assert out[0]["label"] == "CPU Usage"
|
||||
|
||||
def test_labels_fallback_when_no_display_name(self) -> None:
|
||||
raw = {
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{
|
||||
"data": {"values": [[100], [1.0]]},
|
||||
"schema": {
|
||||
"fields": [
|
||||
{"name": "Time"},
|
||||
{
|
||||
"name": "Value",
|
||||
"labels": {"__name__": "up", "instance": "h:9100"},
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
out = normalize_grafana_frames(raw)
|
||||
assert out[0]["label"] == "instance=h:9100"
|
||||
|
||||
def test_falls_back_to_value_when_no_metadata(self) -> None:
|
||||
raw = {
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{
|
||||
"data": {"values": [[100], [1.0]]},
|
||||
"schema": {"fields": [{"name": "Time"}, {}]},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
out = normalize_grafana_frames(raw)
|
||||
assert out[0]["label"] == "value"
|
||||
|
||||
def test_dedup_collisions(self) -> None:
|
||||
raw = {
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{
|
||||
"data": {"values": [[100], [1.0]]},
|
||||
"schema": {"fields": [{}, {"name": "Value"}]},
|
||||
},
|
||||
{
|
||||
"data": {"values": [[100], [2.0]]},
|
||||
"schema": {"fields": [{}, {"name": "Value"}]},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
out = normalize_grafana_frames(raw)
|
||||
labels = [s["label"] for s in out]
|
||||
assert labels == ["Value", "Value (1)"]
|
||||
|
||||
def test_skips_frames_with_insufficient_values(self) -> None:
|
||||
raw = {
|
||||
"results": {
|
||||
"A": {
|
||||
"frames": [
|
||||
{"data": {"values": [[100]]}, "schema": {"fields": []}},
|
||||
{"data": {"values": [[100], [1.0]]}, "schema": {"fields": [{}, {}]}},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
out = normalize_grafana_frames(raw)
|
||||
assert len(out) == 1
|
||||
|
||||
Reference in New Issue
Block a user