freeleaps-service-hub/apps/metrics/backend/infra/external_service/prometheus_client.py

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)