126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
import httpx
|
|
from typing import Dict, Any, Optional, Union
|
|
from datetime import datetime, timezone
|
|
import json
|
|
from fastapi import HTTPException
|
|
|
|
from common.config.app_settings import app_settings
|
|
from common.log.module_logger import ModuleLogger
|
|
|
|
|
|
class PrometheusClient:
|
|
"""
|
|
Async Prometheus client for querying metrics data using PromQL.
|
|
|
|
This client provides methods to:
|
|
- Query data using PromQL expressions
|
|
- Get all available metrics
|
|
- Get labels for specific metrics
|
|
- Query metric series with label filters
|
|
"""
|
|
|
|
def __init__(self, endpoint: Optional[str] = None):
|
|
"""
|
|
Initialize Prometheus client.
|
|
|
|
Args:
|
|
endpoint: Prometheus server endpoint. If None, uses PROMETHEUS_ENDPOINT from settings.
|
|
"""
|
|
self.module_logger = ModuleLogger(__file__)
|
|
self.endpoint = endpoint or app_settings.PROMETHEUS_ENDPOINT
|
|
self.base_url = f"{self.endpoint.rstrip('/')}/api/v1"
|
|
|
|
async def request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""
|
|
Make HTTP request to Prometheus API.
|
|
|
|
Args:
|
|
endpoint: API endpoint path
|
|
params: Query parameters
|
|
|
|
Returns:
|
|
JSON response data
|
|
|
|
Raises:
|
|
httpx.HTTPError: If request fails
|
|
ValueError: If response is not valid JSON
|
|
"""
|
|
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
|
|
try:
|
|
await self.module_logger.log_info(f"Making request to Prometheus: {url} with params: {params}")
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.get(url, params=params)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
if data.get("status") != "success":
|
|
error_msg = data.get('error', 'Unknown error')
|
|
await self.module_logger.log_error(f"Prometheus API error: {error_msg}")
|
|
raise HTTPException(status_code=400, detail=f"Prometheus API error: {error_msg}")
|
|
|
|
return data
|
|
|
|
except httpx.HTTPError as e:
|
|
await self.module_logger.log_error(f"HTTP error querying Prometheus: {e}")
|
|
raise HTTPException(status_code=502, detail=f"Failed to connect to Prometheus: {str(e)}")
|
|
except json.JSONDecodeError as e:
|
|
await self.module_logger.log_error(f"Invalid JSON response from Prometheus: {e}")
|
|
raise HTTPException(status_code=400, detail=f"Invalid response from Prometheus: {str(e)}")
|
|
|
|
async def query_range(
|
|
self,
|
|
query: str,
|
|
start: Union[str, datetime],
|
|
end: Union[str, datetime],
|
|
step: str = "15s"
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Execute a PromQL range query.
|
|
|
|
Args:
|
|
query: PromQL query string
|
|
start: Start time (RFC3339 string or datetime, assumed to be UTC)
|
|
end: End time (RFC3339 string or datetime, assumed to be UTC)
|
|
step: Query resolution step width (e.g., "15s", "1m", "1h")
|
|
|
|
Returns:
|
|
Range query result data
|
|
|
|
Example:
|
|
result = await client.query_range(
|
|
"up{job='prometheus'}",
|
|
start=datetime.now(timezone.utc) - timedelta(hours=1),
|
|
end=datetime.now(timezone.utc),
|
|
step="1m"
|
|
)
|
|
"""
|
|
params = {
|
|
"query": query,
|
|
"step": step
|
|
}
|
|
|
|
# Convert datetime to RFC3339 string (assume input is UTC)
|
|
if isinstance(start, datetime):
|
|
# Assume datetime is already UTC, just ensure it's timezone-aware
|
|
if start.tzinfo is None:
|
|
start_utc = start.replace(tzinfo=timezone.utc)
|
|
else:
|
|
start_utc = start # Assume it's already UTC
|
|
params["start"] = start_utc.isoformat()
|
|
else:
|
|
# Assume string is already in UTC format
|
|
params["start"] = start
|
|
|
|
if isinstance(end, datetime):
|
|
# Assume datetime is already UTC, just ensure it's timezone-aware
|
|
if end.tzinfo is None:
|
|
end_utc = end.replace(tzinfo=timezone.utc)
|
|
else:
|
|
end_utc = end # Assume it's already UTC
|
|
params["end"] = end_utc.isoformat()
|
|
else:
|
|
# Assume string is already in UTC format
|
|
params["end"] = end
|
|
|
|
return await self.request("query_range", params)
|