""" HTTP Client Manager - 永久連線池管理 ===================================== 統帥鐵律: 禁止 subprocess+curl,必須用 httpx AsyncClient Features: - Lifespan 管理 (startup/shutdown) - 連線池復用 (Connection Pooling) - 強制 trust_env=False (禁止 HTTP_PROXY 干擾) - ClickHouse/SignOz 專用 Client """ import httpx import structlog from src.core.config import settings logger = structlog.get_logger(__name__) # ============================================================================= # Singleton Clients # ============================================================================= _clickhouse_client: httpx.AsyncClient | None = None _general_client: httpx.AsyncClient | None = None # ============================================================================= # ClickHouse Client (SignOz Backend) # ============================================================================= async def get_clickhouse_client() -> httpx.AsyncClient: """ 取得 ClickHouse HTTP Client 配置: - base_url: 192.168.0.188:8123 (ClickHouse HTTP API) - trust_env: False (禁止 HTTP_PROXY 干擾) - timeout: 30 秒 - 連線池: limits=100 """ global _clickhouse_client if _clickhouse_client is None or _clickhouse_client.is_closed: _clickhouse_client = httpx.AsyncClient( base_url=settings.CLICKHOUSE_URL.rstrip("/"), timeout=httpx.Timeout(30.0, connect=10.0), trust_env=False, # 🔧 關鍵: 禁止讀取 HTTP_PROXY limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), headers={ "Content-Type": "text/plain", # ClickHouse 需要 plain text }, ) logger.info( "clickhouse_client_initialized", base_url=settings.CLICKHOUSE_URL, trust_env=False, ) return _clickhouse_client async def init_clickhouse_client() -> httpx.AsyncClient: """ 初始化 ClickHouse Client (在 Lifespan 啟動時調用) """ return await get_clickhouse_client() async def close_clickhouse_client() -> None: """ 關閉 ClickHouse Client (在 Lifespan 關閉時調用) """ global _clickhouse_client if _clickhouse_client and not _clickhouse_client.is_closed: await _clickhouse_client.aclose() logger.info("clickhouse_client_closed") _clickhouse_client = None # ============================================================================= # General HTTP Client # ============================================================================= async def get_general_client() -> httpx.AsyncClient: """ 取得通用 HTTP Client (Ollama, Gemini, Claude) """ global _general_client if _general_client is None or _general_client.is_closed: _general_client = httpx.AsyncClient( timeout=httpx.Timeout(float(settings.OPENCLAW_TIMEOUT), connect=10.0), trust_env=False, limits=httpx.Limits(max_connections=50, max_keepalive_connections=10), ) logger.info( "general_client_initialized", timeout=settings.OPENCLAW_TIMEOUT, ) return _general_client async def init_general_client() -> httpx.AsyncClient: """初始化通用 Client""" return await get_general_client() async def close_general_client() -> None: """關閉通用 Client""" global _general_client if _general_client and not _general_client.is_closed: await _general_client.aclose() logger.info("general_client_closed") _general_client = None # ============================================================================= # All Clients Lifecycle # ============================================================================= async def init_all_http_clients() -> None: """ 初始化所有 HTTP Clients (在 Lifespan 調用) """ await init_clickhouse_client() await init_general_client() logger.info("all_http_clients_initialized") async def close_all_http_clients() -> None: """ 關閉所有 HTTP Clients (在 Lifespan 調用) """ await close_clickhouse_client() await close_general_client() logger.info("all_http_clients_closed")