import httpx from typing import Dict, Any, Optional, Union from datetime import datetime 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) end: End time (RFC3339 string or datetime) 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() - timedelta(hours=1), end=datetime.now(), step="1m" ) """ params = { "query": query, "step": step } # Convert datetime to RFC3339 string if needed if isinstance(start, datetime): if start.tzinfo is None: params["start"] = start.isoformat() + "Z" else: params["start"] = start.isoformat() else: params["start"] = start if isinstance(end, datetime): if end.tzinfo is None: params["end"] = end.isoformat() + "Z" else: params["end"] = end.isoformat() else: params["end"] = end return await self.request("query_range", params)