api_logger.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. """
  2. API接口日志记录器
  3. 记录入参密文、入参明文、出参明文、出参密文
  4. 日志文件按天滚动,每天一个日志文件
  5. """
  6. import os
  7. import json
  8. import time
  9. from datetime import datetime
  10. from pathlib import Path
  11. from typing import Any, Dict, Optional
  12. from loguru import logger as loguru_logger
  13. # 日志配置
  14. API_LOG_DIR = Path("logs/api")
  15. API_LOG_DIR.mkdir(parents=True, exist_ok=True)
  16. # 日志文件按天滚动
  17. def get_api_log_path() -> Path:
  18. """获取今天的API日志文件路径"""
  19. return API_LOG_DIR / f"api_{datetime.now().strftime('%Y-%m-%d')}.log"
  20. # 敏感字段脱敏
  21. REDACTED_FIELDS = {
  22. "password",
  23. "card_code",
  24. "shared_secret",
  25. "identity_secret",
  26. "aes_key",
  27. "encrypted_aes_key",
  28. "signature",
  29. "access_token",
  30. "refresh_token",
  31. "session_token",
  32. "steamLoginSecure",
  33. "machine_code",
  34. }
  35. def _redact_value(value: Any) -> Any:
  36. """脱敏单个值(敏感字段统一显示为 ****)"""
  37. if value is None:
  38. return None
  39. if isinstance(value, str):
  40. if len(value) > 8:
  41. return value[:2] + "****" + value[-2:] if len(value) > 4 else "****"
  42. return "****" if value else ""
  43. return "****"
  44. def redact_for_db(data: Any, max_str_len: int = 2000) -> Optional[str]:
  45. """
  46. 将数据脱敏后序列化为 JSON 字符串,用于写入 api_logs 表(明文记录,符合文档 4.5)。
  47. 敏感字段会替换为 ****。
  48. """
  49. if data is None:
  50. return None
  51. try:
  52. redacted = _redact_data(data)
  53. truncated = _truncate_data(redacted, max_str_len)
  54. s = json.dumps(truncated, ensure_ascii=False, default=str)
  55. return s[:max_str_len] + ("..." if len(s) > max_str_len else "")
  56. except Exception:
  57. return None
  58. def _redact_data(data: Any, max_length: int = 10000) -> Any:
  59. """递归脱敏数据中的敏感字段"""
  60. if data is None:
  61. return None
  62. if isinstance(data, dict):
  63. redacted = {}
  64. for k, v in data.items():
  65. if k in REDACTED_FIELDS:
  66. redacted[k] = _redact_value(v)
  67. else:
  68. redacted[k] = _redact_data(v)
  69. return redacted
  70. if isinstance(data, list):
  71. return [_redact_data(item) for item in data]
  72. return data
  73. def _truncate_data(data: Any, max_length: int = 10000) -> Any:
  74. """截断过长的数据"""
  75. if data is None:
  76. return None
  77. if isinstance(data, dict):
  78. truncated = {}
  79. for k, v in data.items():
  80. truncated[k] = _truncate_data(v)
  81. return truncated
  82. if isinstance(data, list):
  83. return data[:1000] # 最多1000个元素
  84. if isinstance(data, str) and len(data) > max_length:
  85. return data[:max_length] + "...[truncated]"
  86. return data
  87. class APILogger:
  88. """API接口日志记录器"""
  89. def __init__(self):
  90. self._log_path = get_api_log_path()
  91. self._setup_logger()
  92. def _setup_logger(self):
  93. """配置日志处理器"""
  94. # 移除默认处理器
  95. try:
  96. loguru_logger.remove()
  97. except Exception:
  98. pass
  99. # 添加文件处理器
  100. self._handler_id = loguru_logger.add(
  101. str(self._log_path),
  102. rotation="00:00", # 每天午夜滚动
  103. retention="30 days",
  104. format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}",
  105. level="DEBUG",
  106. encoding="utf-8",
  107. serialize=False,
  108. )
  109. def log_api_call(
  110. self,
  111. endpoint: str,
  112. method: str,
  113. encrypted_request: Optional[str] = None,
  114. decrypted_request: Optional[Dict[str, Any]] = None,
  115. encrypted_response: Optional[str] = None,
  116. decrypted_response: Optional[Dict[str, Any]] = None,
  117. processing_time_ms: int = 0,
  118. status_code: int = 200,
  119. user_id: Optional[int] = None,
  120. client_ip: Optional[str] = None,
  121. error_message: Optional[str] = None,
  122. ):
  123. """
  124. 记录完整的API调用信息
  125. Args:
  126. endpoint: 接口路径
  127. method: HTTP方法
  128. encrypted_request: 加密的请求数据
  129. decrypted_request: 解密后的请求数据
  130. encrypted_response: 加密的响应数据
  131. decrypted_response: 解密后的响应数据
  132. processing_time_ms: 处理时间(毫秒)
  133. status_code: HTTP状态码
  134. user_id: 用户ID
  135. client_ip: 客户端IP
  136. error_message: 错误信息
  137. """
  138. try:
  139. # 构建日志条目
  140. log_entry = {
  141. "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
  142. "endpoint": endpoint,
  143. "method": method,
  144. "user_id": user_id,
  145. "client_ip": client_ip,
  146. "processing_time_ms": processing_time_ms,
  147. "status_code": status_code,
  148. "error": error_message,
  149. "request": {
  150. "encrypted": encrypted_request,
  151. "decrypted": _truncate_data(_redact_data(decrypted_request)),
  152. },
  153. "response": {
  154. "encrypted": encrypted_response,
  155. "decrypted": _truncate_data(_redact_data(decrypted_response)),
  156. },
  157. }
  158. # 写入日志文件
  159. log_line = json.dumps(log_entry, ensure_ascii=False)
  160. loguru_logger.debug(log_line)
  161. except Exception as e:
  162. loguru_logger.error(f"记录API日志失败: {e}")
  163. def log_request_received(
  164. self,
  165. endpoint: str,
  166. method: str,
  167. encrypted_request: str,
  168. user_id: Optional[int] = None,
  169. client_ip: Optional[str] = None,
  170. ):
  171. """记录收到请求(仅入参密文)"""
  172. try:
  173. log_entry = {
  174. "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
  175. "event": "REQUEST_RECEIVED",
  176. "endpoint": endpoint,
  177. "method": method,
  178. "user_id": user_id,
  179. "client_ip": client_ip,
  180. "encrypted_request": encrypted_request,
  181. }
  182. loguru_logger.debug(json.dumps(log_entry, ensure_ascii=False))
  183. except Exception as e:
  184. loguru_logger.error(f"记录请求日志失败: {e}")
  185. def log_request_decrypted(
  186. self,
  187. endpoint: str,
  188. method: str,
  189. decrypted_request: Dict[str, Any],
  190. user_id: Optional[int] = None,
  191. ):
  192. """记录请求解密完成(入参明文)"""
  193. try:
  194. log_entry = {
  195. "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
  196. "event": "REQUEST_DECRYPTED",
  197. "endpoint": endpoint,
  198. "method": method,
  199. "user_id": user_id,
  200. "decrypted_request": _truncate_data(_redact_data(decrypted_request)),
  201. }
  202. loguru_logger.debug(json.dumps(log_entry, ensure_ascii=False))
  203. except Exception as e:
  204. loguru_logger.error(f"记录请求解密日志失败: {e}")
  205. def log_response_ready(
  206. self,
  207. endpoint: str,
  208. method: str,
  209. decrypted_response: Dict[str, Any],
  210. user_id: Optional[int] = None,
  211. ):
  212. """记录响应准备完成(出参明文)"""
  213. try:
  214. log_entry = {
  215. "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
  216. "event": "RESPONSE_READY",
  217. "endpoint": endpoint,
  218. "method": method,
  219. "user_id": user_id,
  220. "decrypted_response": _truncate_data(_redact_data(decrypted_response)),
  221. }
  222. loguru_logger.debug(json.dumps(log_entry, ensure_ascii=False))
  223. except Exception as e:
  224. loguru_logger.error(f"记录响应准备日志失败: {e}")
  225. def log_response_sent(
  226. self,
  227. endpoint: str,
  228. method: str,
  229. encrypted_response: str,
  230. processing_time_ms: int,
  231. status_code: int,
  232. user_id: Optional[int] = None,
  233. error_message: Optional[str] = None,
  234. ):
  235. """记录响应发送完成(出参密文)"""
  236. try:
  237. log_entry = {
  238. "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
  239. "event": "RESPONSE_SENT",
  240. "endpoint": endpoint,
  241. "method": method,
  242. "user_id": user_id,
  243. "processing_time_ms": processing_time_ms,
  244. "status_code": status_code,
  245. "error": error_message,
  246. "encrypted_response": encrypted_response[:500] + "..." if len(encrypted_response or "") > 500 else encrypted_response,
  247. }
  248. loguru_logger.debug(json.dumps(log_entry, ensure_ascii=False))
  249. except Exception as e:
  250. loguru_logger.error(f"记录响应发送日志失败: {e}")
  251. # 全局实例
  252. api_logger = APILogger()
  253. def log_api_call(
  254. endpoint: str,
  255. method: str,
  256. encrypted_request: Optional[str] = None,
  257. decrypted_request: Optional[Dict[str, Any]] = None,
  258. encrypted_response: Optional[str] = None,
  259. decrypted_response: Optional[Dict[str, Any]] = None,
  260. processing_time_ms: int = 0,
  261. status_code: int = 200,
  262. user_id: Optional[int] = None,
  263. client_ip: Optional[str] = None,
  264. error_message: Optional[str] = None,
  265. ):
  266. """便捷的API调用日志记录函数"""
  267. api_logger.log_api_call(
  268. endpoint=endpoint,
  269. method=method,
  270. encrypted_request=encrypted_request,
  271. decrypted_request=decrypted_request,
  272. encrypted_response=encrypted_response,
  273. decrypted_response=decrypted_response,
  274. processing_time_ms=processing_time_ms,
  275. status_code=status_code,
  276. user_id=user_id,
  277. client_ip=client_ip,
  278. error_message=error_message,
  279. )