Files
PingWatch/backend/app/core/loghive.py
T
v6ole 848f804169 PingWatch 网络设备离线监控系统
- FastAPI 后端 + Vue 3 前端
- Docker Compose 一键部署
- Casdoor OAuth 认证集成
- LogHive 集中式日志
- 设备批量 CSV 导入/导出
- WebSocket 实时状态推送
- 企业微信告警通知
- fping 高性能并发 Ping 检测

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:02:04 +08:00

138 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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):
"""关闭 handlerflush 剩余日志"""
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()