""" LogHive 日志 Handler 基于标准 logging.Handler,通过 REST API 将日志异步批量发送到 LogHive。 不依赖外部包,后台线程发送,失败不影响主业务。 """ import asyncio import atexit import json import logging import queue import threading import time import traceback as tb import httpx class LogHiveHandler(logging.Handler): """异步批量发送日志到 LogHive""" def __init__( self, endpoint: str, project: str, api_key: str, level: int = logging.INFO, batch_size: int = 50, flush_interval: float = 2.0, max_retries: int = 3, ): super().__init__(level=level) self._endpoint = endpoint.rstrip("/") + "/api/logs/ingest" self._project = project self._api_key = api_key self._batch_size = batch_size self._flush_interval = flush_interval self._max_retries = max_retries self._queue: queue.Queue = queue.Queue() self._client: httpx.Client | None = None self._thread: threading.Thread | None = None self._running = False def _ensure_client(self): if self._client is None: self._client = httpx.Client(timeout=10) def _ensure_thread(self): if self._thread is None or not self._thread.is_alive(): self._running = True self._thread = threading.Thread(target=self._send_loop, daemon=True) self._thread.start() def emit(self, record: logging.LogRecord): """接收日志记录,放入队列""" if not self._api_key: return self._ensure_thread() try: entry = { "level": record.levelname.lower(), "message": self.format(record), "logger": record.name, "timestamp": record.created, } if record.exc_info and record.exc_info[1]: entry["exception"] = "".join( tb.format_exception(*record.exc_info) ) self._queue.put_nowait(entry) except Exception: pass # 日志发送失败不能影响主业务 def _send_loop(self): """后台线程:定时批量发送""" while self._running: batch = [] deadline = time.monotonic() + self._flush_interval while len(batch) < self._batch_size: try: remaining = max(0, deadline - time.monotonic()) batch.append(self._queue.get(timeout=remaining)) except queue.Empty: break if batch: payload = {"project": self._project, "entries": batch} for attempt in range(self._max_retries): try: self._ensure_client() resp = self._client.post( self._endpoint, json=payload, headers={"Authorization": f"Bearer {self._api_key}"}, ) if resp.status_code < 500: break except Exception: if attempt == self._max_retries - 1: pass # 最终丢弃 else: time.sleep(0.5 * (attempt + 1)) def close(self): """关闭 handler,flush 剩余日志""" self._running = False if self._thread and self._thread.is_alive(): self._thread.join(timeout=5) if self._client: self._client.close() super().close() class AsyncLogHiveHandler: """ 用于 asyncio 事件循环的异步 handler。 在独立的线程中运行同步 LogHiveHandler,通过 asyncio 队列桥接。 """ def __init__(self, **kwargs): self._handler = LogHiveHandler(**kwargs) self._loop: asyncio.AbstractEventLoop | None = None def setup(self, loop: asyncio.AbstractEventLoop): self._loop = loop atexit.register(self._handler.close) async def emit(self, record: logging.LogRecord): """异步安全地提交日志记录""" # LogHiveHandler.emit 已经把日志放入内部队列,这里只需确保线程运行 self._handler.emit(record) def close(self): self._handler.close()