Skip to content

Commit da04f80

Browse files
authored
Merge pull request #6 from grafana/more-prometheus-tests
2 parents 34a478b + 44755e5 commit da04f80

File tree

3 files changed

+110
-29
lines changed

3 files changed

+110
-29
lines changed

src/mcp_grafana/client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ def __init__(self, url: str, api_key: str | None = None) -> None:
5555
async def get(self, path: str, params: dict[str, str] | None = None) -> bytes:
5656
r = await self.c.get(path, params=params)
5757
if not r.is_success:
58-
raise GrafanaError(r.read())
58+
raise GrafanaError(r.read().decode())
5959
return r.read()
6060

6161
async def post(self, path: str, json: dict[str, Any]) -> bytes:
6262
r = await self.c.post(path, json=json)
6363
if not r.is_success:
64-
raise GrafanaError(r.read())
64+
raise GrafanaError(r.read().decode())
6565
return r.read()
6666

6767
async def list_datasources(self) -> bytes:

src/mcp_grafana/tools/prometheus.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ async def query_prometheus(
2222
datasource_uid: str,
2323
expr: str,
2424
start_rfc3339: str,
25-
end_rfc3339: str,
26-
step_seconds: int,
25+
end_rfc3339: str | None = None,
26+
step_seconds: int | None = None,
2727
query_type: PrometheusQueryType = "range",
2828
) -> DSQueryResponse:
2929
"""
@@ -38,8 +38,13 @@ async def query_prometheus(
3838
step_seconds: The time series step size in seconds. Ignored if `query_type` is 'instant'.
3939
query_type: The type of query to use. Either 'range' or 'instant'.
4040
"""
41+
if query_type == "range" and (end_rfc3339 is None or step_seconds is None):
42+
raise ValueError(
43+
"end_rfc3339 and step_seconds must be provided when query_type is 'range'"
44+
)
4145
start = datetime.fromisoformat(start_rfc3339)
42-
end = datetime.fromisoformat(end_rfc3339)
46+
end = datetime.fromisoformat(end_rfc3339) if end_rfc3339 is not None else start
47+
interval_ms = step_seconds * 1000 if step_seconds is not None else None
4348
query = Query(
4449
refId="A",
4550
datasource=DatasourceRef(
@@ -48,7 +53,7 @@ async def query_prometheus(
4853
),
4954
queryType=query_type,
5055
expr=expr, # type: ignore
51-
intervalMs=step_seconds * 1000,
56+
intervalMs=interval_ms,
5257
)
5358
response = await grafana_client.query(start, end, [query])
5459
return DSQueryResponse.model_validate_json(response)

tests/tools/prometheus_test.py

+99-23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import pytest
44

5-
from mcp_grafana.grafana_types import LabelMatcher, Selector
5+
from mcp_grafana.client import GrafanaError
6+
from mcp_grafana.grafana_types import DSQueryResponse, LabelMatcher, Selector
67
from mcp_grafana.tools.prometheus import (
78
query_prometheus,
89
get_prometheus_metric_metadata,
@@ -16,29 +17,104 @@
1617
pytestmark = mark_integration
1718

1819

19-
@pytest.mark.parametrize("step_seconds", [15, 60, 300])
20-
async def test_query_prometheus_range(step_seconds):
21-
end = datetime.now()
22-
start = end - timedelta(minutes=10)
20+
class TestPrometheusQueries:
21+
@pytest.mark.parametrize("step_seconds", [15, 60, 300])
22+
async def test_query_prometheus_range(self, step_seconds):
23+
end = datetime.now()
24+
start = end - timedelta(minutes=10)
2325

24-
results = await query_prometheus(
25-
"robustperception",
26-
start_rfc3339=start.isoformat(),
27-
end_rfc3339=end.isoformat(),
28-
step_seconds=step_seconds,
29-
expr="node_load1",
30-
query_type="range",
31-
)
32-
query_result = results.results["A"]
33-
assert query_result.status == 200
34-
assert (
35-
query_result.frames[0].data.values[0][0] - start.timestamp() * 1000
36-
< step_seconds * 1000
37-
)
38-
assert (
39-
len(query_result.frames[0].data.values[0])
40-
== (end - start).total_seconds() // step_seconds + 1
41-
)
26+
results = await query_prometheus(
27+
"robustperception",
28+
start_rfc3339=start.isoformat(),
29+
end_rfc3339=end.isoformat(),
30+
step_seconds=step_seconds,
31+
expr="node_load1",
32+
query_type="range",
33+
)
34+
query_result = results.results["A"]
35+
assert query_result.status == 200
36+
assert (
37+
query_result.frames[0].data.values[0][0] - start.timestamp() * 1000
38+
< step_seconds * 1000
39+
)
40+
assert (
41+
len(query_result.frames[0].data.values[0])
42+
== (end - start).total_seconds() // step_seconds + 1
43+
)
44+
45+
async def test_query_prometheus_instant(self):
46+
timestamp = datetime.now()
47+
results = await query_prometheus(
48+
"robustperception",
49+
start_rfc3339=timestamp.isoformat(),
50+
expr="up",
51+
query_type="instant",
52+
)
53+
query_result = results.results["A"]
54+
55+
# Verify the response
56+
assert query_result.status == 200
57+
assert len(query_result.frames) > 0
58+
# Instant queries should return a single data point
59+
assert len(query_result.frames[0].data.values[0]) == 1
60+
# Verify we have both time and value columns
61+
assert len(query_result.frames[0].data.values) == 2
62+
# Verify the timestamp is close to our request time (within 60 seconds,
63+
# we don't know what the scrape interval is).
64+
assert (
65+
abs(query_result.frames[0].data.values[0][0] - timestamp.timestamp() * 1000)
66+
< 60000
67+
)
68+
69+
async def test_query_prometheus_range_missing_params(self):
70+
start = datetime.now()
71+
with pytest.raises(
72+
ValueError, match="end_rfc3339 and step_seconds must be provided"
73+
):
74+
await query_prometheus(
75+
"robustperception",
76+
start_rfc3339=start.isoformat(),
77+
expr="node_load1",
78+
query_type="range",
79+
)
80+
81+
async def test_query_prometheus_invalid_query(self):
82+
end = datetime.now()
83+
start = end - timedelta(minutes=10)
84+
85+
try:
86+
results = await query_prometheus(
87+
"robustperception",
88+
start_rfc3339=start.isoformat(),
89+
end_rfc3339=end.isoformat(),
90+
step_seconds=60,
91+
expr="invalid_metric{", # Invalid PromQL syntax
92+
query_type="range",
93+
)
94+
except Exception as e:
95+
assert isinstance(e, GrafanaError)
96+
results = DSQueryResponse.model_validate_json(str(e))
97+
query_result = results.results["A"]
98+
assert query_result.status == 400
99+
assert query_result.error is not None
100+
assert "parse error" in query_result.error
101+
102+
async def test_query_prometheus_empty_result(self):
103+
end = datetime.now()
104+
start = end - timedelta(minutes=10)
105+
106+
results = await query_prometheus(
107+
"robustperception",
108+
start_rfc3339=start.isoformat(),
109+
end_rfc3339=end.isoformat(),
110+
step_seconds=60,
111+
expr='metric_that_does_not_exist{label="value"}',
112+
query_type="range",
113+
)
114+
query_result = results.results["A"]
115+
assert query_result.status == 200
116+
assert len(query_result.frames) == 1
117+
assert len(query_result.frames[0].data.values) == 0
42118

43119

44120
async def test_get_prometheus_metric_metadata():

0 commit comments

Comments
 (0)