feat(tool): add Prometheus tool support (#7047)
Adds prometheus_query (instant PromQL) and prometheus_query_range (time-range) tools. Includes credential spec, /-/ready health check, unit tests, and docs. Optional Bearer token and Basic auth via env vars (PROMETHEUS_TOKEN, PROMETHEUS_USERNAME/PASSWORD). Fixes #6945.
This commit is contained in:
committed by
GitHub
parent
a881fe68da
commit
199c3a235e
@@ -110,6 +110,7 @@ from .pipedrive import PIPEDRIVE_CREDENTIALS
|
||||
from .plaid import PLAID_CREDENTIALS
|
||||
from .postgres import POSTGRES_CREDENTIALS
|
||||
from .powerbi import POWERBI_CREDENTIALS
|
||||
from .prometheus import PROMETHEUS_CREDENTIALS
|
||||
from .pushover import PUSHOVER_CREDENTIALS
|
||||
from .quickbooks import QUICKBOOKS_CREDENTIALS
|
||||
from .razorpay import RAZORPAY_CREDENTIALS
|
||||
@@ -197,6 +198,7 @@ CREDENTIAL_SPECS = {
|
||||
**PLAID_CREDENTIALS,
|
||||
**POSTGRES_CREDENTIALS,
|
||||
**POWERBI_CREDENTIALS,
|
||||
**PROMETHEUS_CREDENTIALS,
|
||||
**PUSHOVER_CREDENTIALS,
|
||||
**QUICKBOOKS_CREDENTIALS,
|
||||
**RAZORPAY_CREDENTIALS,
|
||||
@@ -293,6 +295,7 @@ __all__ = [
|
||||
"PLAID_CREDENTIALS",
|
||||
"POSTGRES_CREDENTIALS",
|
||||
"POWERBI_CREDENTIALS",
|
||||
"PROMETHEUS_CREDENTIALS",
|
||||
"PUSHOVER_CREDENTIALS",
|
||||
"QUICKBOOKS_CREDENTIALS",
|
||||
"RAZORPAY_CREDENTIALS",
|
||||
|
||||
@@ -1045,6 +1045,47 @@ class LushaHealthChecker:
|
||||
)
|
||||
|
||||
|
||||
class PrometheusHealthChecker:
|
||||
"""Health checker for Prometheus (no authentication)."""
|
||||
|
||||
TIMEOUT = 5.0
|
||||
|
||||
def check(self, base_url: str) -> HealthCheckResult:
|
||||
"""
|
||||
Validate Prometheus by hitting /-/ready endpoint.
|
||||
"""
|
||||
url = base_url.rstrip("/") + "/-/ready"
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=self.TIMEOUT) as client:
|
||||
response = client.get(url)
|
||||
|
||||
if response.status_code == 200:
|
||||
return HealthCheckResult(
|
||||
valid=True,
|
||||
message="Prometheus is healthy",
|
||||
)
|
||||
else:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Prometheus returned status {response.status_code}",
|
||||
details={"status_code": response.status_code},
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message="Prometheus health check timed out",
|
||||
details={"error": "timeout"},
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
return HealthCheckResult(
|
||||
valid=False,
|
||||
message=f"Failed to connect to Prometheus: {e}",
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
|
||||
# --- New checkers using BaseHttpHealthChecker ---
|
||||
|
||||
|
||||
@@ -1342,6 +1383,7 @@ HEALTH_CHECKERS: dict[str, CredentialHealthChecker] = {
|
||||
"notion_token": NotionHealthChecker(),
|
||||
"pinecone": PineconeHealthChecker(),
|
||||
"pipedrive": PipedriveHealthChecker(),
|
||||
"prometheus": PrometheusHealthChecker(),
|
||||
"resend": ResendHealthChecker(),
|
||||
"serpapi": SerpApiHealthChecker(),
|
||||
"slack": SlackHealthChecker(),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CredentialSpec
|
||||
|
||||
PROMETHEUS_CREDENTIALS = {
|
||||
"prometheus": CredentialSpec(
|
||||
env_var="PROMETHEUS_BASE_URL",
|
||||
tools=[
|
||||
"prometheus_query",
|
||||
"prometheus_query_range",
|
||||
],
|
||||
required=True,
|
||||
startup_required=False,
|
||||
help_url="https://prometheus.io/docs/prometheus/latest/querying/api/",
|
||||
description="Base URL of Prometheus server",
|
||||
aden_supported=False,
|
||||
direct_api_key_supported=False,
|
||||
api_key_instructions="""To configure Prometheus access:
|
||||
|
||||
1. Set your Prometheus base URL:
|
||||
export PROMETHEUS_BASE_URL=http://localhost:9090
|
||||
|
||||
Optional authentication:
|
||||
|
||||
2. For Bearer Token:
|
||||
export PROMETHEUS_TOKEN=your-token
|
||||
|
||||
3. For Basic Auth:
|
||||
export PROMETHEUS_USERNAME=admin
|
||||
export PROMETHEUS_PASSWORD=secret
|
||||
|
||||
Notes:
|
||||
- PROMETHEUS_BASE_URL is required
|
||||
- Authentication is optional (most local setups don’t need it)
|
||||
""",
|
||||
health_check_endpoint="/-/ready",
|
||||
health_check_method="GET",
|
||||
credential_id="prometheus",
|
||||
credential_key="base_url",
|
||||
),
|
||||
}
|
||||
@@ -105,6 +105,7 @@ from .plaid_tool import register_tools as register_plaid
|
||||
from .port_scanner import register_tools as register_port_scanner
|
||||
from .postgres_tool import register_tools as register_postgres
|
||||
from .powerbi_tool import register_tools as register_powerbi
|
||||
from .prometheus_tool import register_tools as register_prometheus
|
||||
from .pushover_tool import register_tools as register_pushover
|
||||
from .quickbooks_tool import register_tools as register_quickbooks
|
||||
from .razorpay_tool import register_tools as register_razorpay
|
||||
@@ -310,6 +311,7 @@ def _register_unverified(
|
||||
register_pipedrive(mcp, credentials=credentials)
|
||||
register_plaid(mcp, credentials=credentials)
|
||||
register_powerbi(mcp, credentials=credentials)
|
||||
register_prometheus(mcp, credentials=credentials)
|
||||
register_pushover(mcp, credentials=credentials)
|
||||
register_quickbooks(mcp, credentials=credentials)
|
||||
register_reddit(mcp, credentials=credentials)
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# Prometheus Metrics Query Tool
|
||||
|
||||
Provides PromQL-based querying tools for agents to fetch real-time and historical metrics from a Prometheus server.
|
||||
|
||||
## Authentication
|
||||
|
||||
Authentication is **optional** as most prometheus servers are deployed within private infrastructure. If no credentials are set, requests are sent without auth headers (suitable for open/internal Prometheus instances).
|
||||
|
||||
When credentials are present, **Bearer token takes priority** over Basic Auth:
|
||||
|
||||
| Priority | Mode | Environment Variables |
|
||||
| -------- | ------------ | --------------------------------------------- |
|
||||
| 1 | Bearer Token | `PROMETHEUS_TOKEN` |
|
||||
| 2 | Basic Auth | `PROMETHEUS_USERNAME` + `PROMETHEUS_PASSWORD` |
|
||||
| 3 | None | _(no variables set)_ |
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export PROMETHEUS_BASE_URL="http://your-prometheus-host:9090"
|
||||
|
||||
# Optional — Bearer token (takes priority if set)
|
||||
export PROMETHEUS_TOKEN="your_token_here"
|
||||
|
||||
# Optional — Basic auth
|
||||
export PROMETHEUS_USERNAME="admin"
|
||||
export PROMETHEUS_PASSWORD="secret"
|
||||
```
|
||||
|
||||
> \_Note: Base URL can also be configured via the Aden Credential Store under the `prometheus` key.
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
| ------------------------ | --------------------------------------------------------- |
|
||||
| `prometheus_query` | Run an instant PromQL query — returns current value(s) |
|
||||
| `prometheus_query_range` | Run a PromQL query over a time range with step resolution |
|
||||
|
||||
---
|
||||
|
||||
## Tool Reference
|
||||
|
||||
### `prometheus_query`
|
||||
|
||||
Executes a PromQL expression against `/api/v1/query`. Returns the latest value for matching time series.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
| --------- | ---- | -------- | ------- | ----------------------------------------------------------------- |
|
||||
| `query` | str | ✅ | — | PromQL expression (max 1000 chars) |
|
||||
| `timeout` | int | ❌ | `5` | Request timeout in seconds (1–30; out-of-range values reset to 5) |
|
||||
|
||||
**Returns:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"query": "up",
|
||||
"result": [
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "up",
|
||||
"job": "prometheus",
|
||||
"instance": "localhost:9090"
|
||||
},
|
||||
"value": [1700000000.0, "1"]
|
||||
}
|
||||
],
|
||||
"raw": {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "vector",
|
||||
"result": [
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "up",
|
||||
"job": "prometheus",
|
||||
"instance": "localhost:9090"
|
||||
},
|
||||
"value": [1700000000.0, "1"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `prometheus_query_range`
|
||||
|
||||
Executes a PromQL expression against `/api/v1/query_range`. Returns a matrix of values over time — useful for graphing, trends, and historical analysis.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
| --------- | ---- | -------- | ------- | ---------------------------------------- |
|
||||
| `query` | str | ✅ | — | PromQL expression (max 1000 chars) |
|
||||
| `start` | str | ✅ | — | Start time — Unix timestamp or RFC3339 |
|
||||
| `end` | str | ✅ | — | End time — Unix timestamp or RFC3339 |
|
||||
| `step` | str | ❌ | `"60s"` | Resolution step (e.g. `15s`, `5m`, `1h`) |
|
||||
| `timeout` | int | ❌ | `5` | Request timeout in seconds (1–30) |
|
||||
|
||||
**Returns:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"query": "rate(http_requests_total[5m])",
|
||||
"start": "2024-01-01T00:00:00Z",
|
||||
"end": "2024-01-01T01:00:00Z",
|
||||
"step": "60s",
|
||||
"result": [
|
||||
{
|
||||
"metric": { "job": "api-server" },
|
||||
"values": [
|
||||
[1704067200, "3.14"],
|
||||
[1704067260, "3.22"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
All tools return structured error dicts on failure.
|
||||
|
||||
```json
|
||||
{ "error": "Request to Prometheus timed out" }
|
||||
{ "error": "Failed to connect to Prometheus", "help": "Check if Prometheus is running and base_url is correct" }
|
||||
{ "error": "Prometheus returned status 400", "details": "..." }
|
||||
{ "error": "Query must be between 1 and 1000 characters" }
|
||||
{ "error": "start and end time are required" }
|
||||
{
|
||||
"error": "Missing required credential: <description>",
|
||||
"help": "<setup instructions>",
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limits & Safeguards
|
||||
|
||||
| Guard | Value |
|
||||
| ----------------- | ------------------------------------------------------------------- |
|
||||
| Base URL priority | Credential store (`prometheus`) → fallback to `PROMETHEUS_BASE_URL` |
|
||||
| Timeout handling | Out-of-range values reset to `5s` |
|
||||
| Query limit | Must be 1–1000 characters |
|
||||
| URL normalization | Trailing `/` removed using `.rstrip('/')` |
|
||||
| Timeout range | 1–30 seconds (values outside reset defaults to 5s) |
|
||||
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .prometheus_tool import register_tools
|
||||
|
||||
__all__ = ["register_tools"]
|
||||
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Prometheus Tool - Query metrics from a Prometheus server using PromQL.
|
||||
|
||||
Required:
|
||||
- PROMETHEUS_BASE_URL
|
||||
|
||||
Optional Authentication:
|
||||
- PROMETHEUS_TOKEN (Bearer token)
|
||||
- PROMETHEUS_USERNAME and PROMETHEUS_PASSWORD (Basic Auth)
|
||||
|
||||
API Reference: https://prometheus.io/docs/prometheus/latest/querying/api/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from aden_tools.credentials import CREDENTIAL_SPECS
|
||||
from aden_tools.credentials.store_adapter import CredentialStoreAdapter
|
||||
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
|
||||
def _get_prometheus_base_url(
|
||||
credentials: CredentialStoreAdapter | None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Return Prometheus base URL.
|
||||
|
||||
Priority:
|
||||
1. Credential store
|
||||
2. Environment variable fallback
|
||||
|
||||
Parameters:
|
||||
credentials: Credential store to query
|
||||
|
||||
Returns:
|
||||
Base URL string or None
|
||||
"""
|
||||
base_url: str | None = None
|
||||
|
||||
if credentials:
|
||||
base_url = credentials.get("prometheus")
|
||||
|
||||
if not base_url:
|
||||
base_url = os.getenv("PROMETHEUS_BASE_URL")
|
||||
|
||||
return base_url
|
||||
|
||||
|
||||
def _missing_prometheus_credential_response() -> dict:
|
||||
"""
|
||||
Return a standardized response for missing Prometheus configuration.
|
||||
"""
|
||||
spec = CREDENTIAL_SPECS["prometheus"]
|
||||
|
||||
return {
|
||||
"error": f"Missing required credential: {spec.description}",
|
||||
"help": spec.api_key_instructions,
|
||||
"success": False,
|
||||
}
|
||||
|
||||
|
||||
def _get_auth() -> tuple[dict[str, str], httpx.BasicAuth | None]:
|
||||
headers: dict[str, str] = {}
|
||||
auth = None
|
||||
|
||||
# Bearer token
|
||||
token = os.getenv("PROMETHEUS_TOKEN")
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
return headers, None
|
||||
|
||||
# Basic auth
|
||||
username = os.getenv("PROMETHEUS_USERNAME")
|
||||
password = os.getenv("PROMETHEUS_PASSWORD")
|
||||
if username and password:
|
||||
auth = httpx.BasicAuth(username, password)
|
||||
|
||||
return headers, auth
|
||||
|
||||
|
||||
def register_tools(
|
||||
mcp: FastMCP,
|
||||
credentials: CredentialStoreAdapter | None = None,
|
||||
) -> None:
|
||||
"""Register Prometheus tools with MCP."""
|
||||
|
||||
@mcp.tool()
|
||||
def prometheus_query(
|
||||
query: str,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> dict:
|
||||
"""
|
||||
Query Prometheus using PromQL.
|
||||
|
||||
Use this tool to fetch real-time metrics from a Prometheus server.
|
||||
|
||||
Args:
|
||||
query: PromQL query string (e.g., 'up', 'sum(rate(http_requests_total[1m]))')
|
||||
timeout: Request timeout in seconds (1-30)
|
||||
|
||||
Returns:
|
||||
Dict containing query results or error
|
||||
"""
|
||||
|
||||
# limit query length
|
||||
if not query or len(query) > 1000:
|
||||
return {"error": "Query must be between 1 and 1000 characters"}
|
||||
|
||||
if timeout < 1 or timeout > 30:
|
||||
timeout = DEFAULT_TIMEOUT
|
||||
|
||||
base_url = _get_prometheus_base_url(credentials)
|
||||
|
||||
if not base_url:
|
||||
return _missing_prometheus_credential_response()
|
||||
|
||||
url = f"{base_url.rstrip('/')}/api/v1/query"
|
||||
|
||||
headers, auth = _get_auth()
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
url,
|
||||
params={"query": query},
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"error": f"Prometheus returned status {response.status_code}",
|
||||
"details": response.text,
|
||||
}
|
||||
|
||||
data = response.json()
|
||||
|
||||
if data.get("status") != "success":
|
||||
return {
|
||||
"error": "Prometheus query failed",
|
||||
"details": data,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"result": data.get("data", {}).get("result", []),
|
||||
"raw": data,
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request to Prometheus timed out"}
|
||||
|
||||
except httpx.ConnectError:
|
||||
return {
|
||||
"error": "Failed to connect to Prometheus",
|
||||
"help": "Check if Prometheus is running and base_url is correct",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Unexpected error: {str(e)}"}
|
||||
|
||||
@mcp.tool()
|
||||
def prometheus_query_range(
|
||||
query: str,
|
||||
start: str,
|
||||
end: str,
|
||||
step: str = "60s",
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> dict:
|
||||
"""
|
||||
Query Prometheus over a time range using PromQL.
|
||||
|
||||
Use this tool to fetch historical metrics and time series data
|
||||
from a Prometheus server. Suitable for trend analysis, graphing,
|
||||
and monitoring over a defined time window.
|
||||
|
||||
Args:
|
||||
query: PromQL query string (e.g., 'rate(http_requests_total[5m])')
|
||||
start: Start time (Unix timestamp or RFC3339 format, e.g., "2024-01-01T00:00:00Z")
|
||||
end: End time (Unix timestamp or RFC3339 format, e.g., "2024-01-01T00:00:00Z")
|
||||
step: Query resolution step (e.g., '15s', '5m', '1h')
|
||||
timeout: Request timeout in seconds (1-30)
|
||||
|
||||
Returns:
|
||||
Dict containing time-series results or error
|
||||
"""
|
||||
|
||||
if not query or len(query) > 1000:
|
||||
return {"error": "Query must be between 1 and 1000 characters"}
|
||||
|
||||
if not start or not end:
|
||||
return {"error": "start and end time are required"}
|
||||
|
||||
if timeout < 1 or timeout > 30:
|
||||
timeout = DEFAULT_TIMEOUT
|
||||
|
||||
base_url = _get_prometheus_base_url(credentials)
|
||||
|
||||
if not base_url:
|
||||
return _missing_prometheus_credential_response()
|
||||
|
||||
url = f"{base_url.rstrip('/')}/api/v1/query_range"
|
||||
|
||||
headers, auth = _get_auth()
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
url,
|
||||
params={
|
||||
"query": query,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"step": step,
|
||||
},
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"error": f"Prometheus returned status {response.status_code}",
|
||||
"details": response.text,
|
||||
}
|
||||
|
||||
data = response.json()
|
||||
|
||||
if data.get("status") != "success":
|
||||
return {
|
||||
"error": "Prometheus range query failed",
|
||||
"details": data,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"step": step,
|
||||
"result": data.get("data", {}).get("result", []),
|
||||
"raw": data,
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {"error": "Request to Prometheus timed out"}
|
||||
|
||||
except httpx.ConnectError:
|
||||
return {
|
||||
"error": "Failed to connect to Prometheus",
|
||||
"help": "Check if Prometheus is running and base_url is correct",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Unexpected error: {str(e)}"}
|
||||
@@ -88,6 +88,7 @@ class TestHealthCheckerRegistry:
|
||||
"notion_token",
|
||||
"pinecone",
|
||||
"pipedrive",
|
||||
"prometheus",
|
||||
"resend",
|
||||
"serpapi",
|
||||
"slack",
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from aden_tools.tools.prometheus_tool import register_tools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp() -> FastMCP:
|
||||
server = FastMCP("test")
|
||||
register_tools(server)
|
||||
return server
|
||||
|
||||
|
||||
def test_prometheus_query_validation(mcp: FastMCP) -> None:
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
result = tool_fn(query="")
|
||||
|
||||
assert "error" in result
|
||||
|
||||
|
||||
def test_prometheus_query_success(mcp: FastMCP, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("PROMETHEUS_BASE_URL", "http://fake-prometheus:9090")
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
class MockResponse:
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"result": [{"metric": {}, "value": [123, "1"]}]},
|
||||
}
|
||||
|
||||
def mock_get(*args, **kwargs):
|
||||
return MockResponse()
|
||||
|
||||
monkeypatch.setattr("aden_tools.tools.prometheus_tool.prometheus_tool.httpx.get", mock_get)
|
||||
|
||||
result = tool_fn(query="up")
|
||||
|
||||
assert result["success"] is True
|
||||
assert "result" in result
|
||||
|
||||
|
||||
def test_prometheus_query_range_success(mcp: FastMCP, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("PROMETHEUS_BASE_URL", "http://fake-prometheus:9090")
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query_range"].fn
|
||||
|
||||
class MockResponse:
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"result": [{"values": [[123, "1"]]}]},
|
||||
}
|
||||
|
||||
monkeypatch.setattr("aden_tools.tools.prometheus_tool.prometheus_tool.httpx.get", lambda *a, **k: MockResponse())
|
||||
|
||||
result = tool_fn(
|
||||
query="up",
|
||||
start="2026-01-01T00:00:00Z",
|
||||
end="2026-01-01T01:00:00Z",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
|
||||
def test_prometheus_non_200(mcp: FastMCP, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("PROMETHEUS_BASE_URL", "http://fake-prometheus:9090")
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
class MockResponse:
|
||||
status_code = 500
|
||||
text = "Internal error"
|
||||
|
||||
def json(self):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr("aden_tools.tools.prometheus_tool.prometheus_tool.httpx.get", lambda *a, **k: MockResponse())
|
||||
|
||||
result = tool_fn(query="up")
|
||||
|
||||
assert "error" in result
|
||||
|
||||
|
||||
def test_timeout(mcp: FastMCP, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("PROMETHEUS_BASE_URL", "http://fake-prometheus:9090")
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
def mock_query(*args, **kwargs):
|
||||
raise httpx.TimeoutException("Request timed out")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"aden_tools.tools.prometheus_tool.prometheus_tool.httpx.get",
|
||||
mock_query,
|
||||
)
|
||||
|
||||
result = tool_fn(query="up")
|
||||
|
||||
assert "error" in result
|
||||
assert "timed out" in result["error"].lower()
|
||||
|
||||
|
||||
def test_prometheus_connection_error(mcp: FastMCP, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("PROMETHEUS_BASE_URL", "http://fake-prometheus:9090")
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
def mock_get(*args, **kwargs):
|
||||
raise Exception("Connection failed")
|
||||
|
||||
monkeypatch.setattr("aden_tools.tools.prometheus_tool.prometheus_tool.httpx.get", mock_get)
|
||||
|
||||
result = tool_fn(query="up")
|
||||
|
||||
assert "error" in result
|
||||
|
||||
|
||||
def test_missing_base_url(mcp: FastMCP, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
monkeypatch.delenv("PROMETHEUS_BASE_URL", raising=False)
|
||||
|
||||
result = tool_fn(query="up")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "Missing required credential" in result["error"]
|
||||
|
||||
|
||||
def test_base_url_credentials_priority_over_env(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("PROMETHEUS_BASE_URL", "http://fake-prometheus:9090")
|
||||
|
||||
class FakeCredentialStore:
|
||||
def get(self, key: str):
|
||||
return "http://cred-prometheus:9090"
|
||||
|
||||
mcp = FastMCP("test-cred-override")
|
||||
register_tools(mcp, credentials=FakeCredentialStore())
|
||||
|
||||
called_urls = []
|
||||
|
||||
def fake_get(url, *args, **kwargs):
|
||||
called_urls.append(url)
|
||||
|
||||
class Resp:
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {"status": "success", "data": {"result": []}}
|
||||
|
||||
return Resp()
|
||||
|
||||
monkeypatch.setattr("aden_tools.tools.prometheus_tool.prometheus_tool.httpx.get", fake_get)
|
||||
|
||||
tool_fn = mcp._tool_manager._tools["prometheus_query"].fn
|
||||
|
||||
result = tool_fn(query="up")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["query"] == "up"
|
||||
assert "cred-prometheus:9090" in called_urls[0]
|
||||
Reference in New Issue
Block a user