logging.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. """异步日志服务 —— 基于 Queue + 后台 worker 推送到阿里云 SLS
  2. 职责:
  3. - 接收结构化 LogRecord,序列化后批量推送
  4. - 提供 log_raw() 向下兼容旧的 dict 调用方式
  5. - Queue 满时静默丢弃,不阻塞业务
  6. """
  7. import asyncio
  8. import contextlib
  9. import datetime
  10. import json
  11. import logging
  12. import time
  13. import traceback
  14. from typing import Optional
  15. from aliyun.log import LogClient, LogItem, PutLogsRequest
  16. from supply.config.aliyun import AliyunLogConfig
  17. from supply.infra.observability.log_schema import LogRecord
  18. logger = logging.getLogger(__name__)
  19. class LogService:
  20. """异步日志服务:将结构化日志放入 Queue,后台 worker 批量推送到 SLS
  21. 使用方式:
  22. service = LogService(AliyunLogConfig())
  23. await service.start()
  24. # 结构化方式(推荐)
  25. record = LogRecord(
  26. level=LogLevel.INFO,
  27. category=LogCategory.TASK,
  28. event="task_started",
  29. trace_id="trace-123",
  30. task_name="content_generation",
  31. )
  32. await service.log(record)
  33. # 向后兼容:自由 dict
  34. await service.log_raw({"event": "legacy_call", "key": "val"})
  35. await service.stop()
  36. """
  37. def __init__(self, log_config: AliyunLogConfig):
  38. self.config = log_config
  39. self.client: Optional[LogClient] = None
  40. self.queue: Optional[asyncio.Queue] = None
  41. self._worker_task: Optional[asyncio.Task] = None
  42. self._running = False
  43. # ==================== 生命周期 ====================
  44. async def start(self):
  45. if self._running:
  46. return
  47. self.client = LogClient(
  48. self.config.endpoint,
  49. self.config.access_key_id,
  50. self.config.access_key_secret,
  51. )
  52. self.queue = asyncio.Queue(maxsize=10000)
  53. self._running = True
  54. self._worker_task = asyncio.create_task(self._worker())
  55. logger.info("LogService 已启动 → %s/%s", self.config.project, self.config.logstore)
  56. async def stop(self):
  57. if not self._running:
  58. return
  59. self._running = False
  60. if self._worker_task:
  61. self._worker_task.cancel()
  62. with contextlib.suppress(asyncio.CancelledError):
  63. await self._worker_task
  64. self._worker_task = None
  65. self.queue = None
  66. self.client = None
  67. logger.info("LogService 已停止")
  68. # ==================== 对外接口 ====================
  69. async def log(self, record: LogRecord) -> None:
  70. """推送结构化日志(推荐方式,非阻塞)"""
  71. if not self._running or self.queue is None:
  72. return
  73. try:
  74. self.queue.put_nowait(record)
  75. except asyncio.QueueFull:
  76. pass
  77. async def log_raw(self, contents: dict) -> None:
  78. """向后兼容:接受自由 dict,内部转为 LogRecord 推送
  79. 所有值会被 str() 化以匹配 SLS API 要求。
  80. """
  81. if not self._running or self.queue is None:
  82. return
  83. try:
  84. # 从 dict 提取已知字段,其余塞入 extra
  85. level_str = str(contents.pop("level", "INFO")).upper()
  86. category_str = str(contents.pop("category", "system")).lower()
  87. try:
  88. from supply.infra.observability.log_schema import LogLevel, LogCategory
  89. level = LogLevel(level_str) if level_str in LogLevel._value2member_map_ else LogLevel.INFO
  90. category = LogCategory(category_str) if category_str in LogCategory._value2member_map_ else LogCategory.SYSTEM
  91. except (KeyError, ValueError):
  92. level = LogLevel.INFO
  93. category = LogCategory.SYSTEM
  94. record = LogRecord(
  95. level=level,
  96. category=category,
  97. event=str(contents.pop("event", "")),
  98. message=str(contents.pop("message", "")),
  99. trace_id=str(contents.pop("trace_id", "")),
  100. task_name=str(contents.pop("task_name", "")),
  101. agent_name=str(contents.pop("agent_name", "")),
  102. tool_name=str(contents.pop("tool_name", "")),
  103. duration_ms=int(contents.pop("duration_ms", 0)),
  104. status=str(contents.pop("status", "success")),
  105. error_type=str(contents.pop("error_type", "")),
  106. error_message=str(contents.pop("error_message", "")),
  107. extra=contents, # 剩余字段
  108. )
  109. self.queue.put_nowait(record)
  110. except asyncio.QueueFull:
  111. pass
  112. # ==================== 后台 Worker ====================
  113. async def _worker(self):
  114. try:
  115. while self._running:
  116. record = await self.queue.get()
  117. try:
  118. await asyncio.to_thread(self._put_log, record)
  119. except Exception as e:
  120. print(f"[Log Error] {e}")
  121. print(traceback.format_exc())
  122. except asyncio.CancelledError:
  123. pass
  124. def _put_log(self, record: LogRecord):
  125. """同步推送到 SLS"""
  126. timestamp = int(time.time())
  127. # LogRecord → SLS LogItem
  128. items = record.to_sls_items()
  129. # 追加内置字段
  130. items.append(("datetime", datetime.datetime.now().isoformat()))
  131. log_item = LogItem(timestamp=timestamp, contents=items)
  132. req = PutLogsRequest(
  133. self.config.project,
  134. self.config.logstore,
  135. topic=record.category.value, # topic 用 category,方便消费端路由
  136. source="",
  137. logitems=[log_item],
  138. )
  139. self.client.put_logs(req)