This commit is contained in:
2026-03-24 18:07:22 +08:00
parent e062368ef2
commit 9a16f738d8
121 changed files with 8904 additions and 3940 deletions
+14
View File
@@ -0,0 +1,14 @@
"""数据存储模块"""
from .message_storage import get_message_storage, MessageStorage
from .structured_logger import get_structured_logger
from .cache import CacheBase, NoopCache, RedisCache
__all__ = [
"get_message_storage",
"MessageStorage",
"get_structured_logger",
"CacheBase",
"NoopCache",
"RedisCache",
]
+64
View File
@@ -0,0 +1,64 @@
from __future__ import annotations
from typing import List, Optional
try:
import redis
except Exception:
redis = None
class CacheBase:
"""缓存接口"""
def get(self, key: str) -> Optional[str]:
raise NotImplementedError
def set(self, key: str, value: str, ttl: int = None) -> None:
raise NotImplementedError
def delete(self, key: str) -> None:
raise NotImplementedError
def keys(self, pattern: str) -> List[str]:
raise NotImplementedError
class NoopCache(CacheBase):
"""空实现缓存"""
def get(self, key: str) -> Optional[str]:
return None
def set(self, key: str, value: str, ttl: int = None) -> None:
return None
def delete(self, key: str) -> None:
return None
def keys(self, pattern: str) -> List[str]:
return []
class RedisCache(CacheBase):
"""Redis 缓存实现"""
def __init__(self, url: str, db: int = 0):
if redis is None:
raise ImportError("未安装 redis 依赖")
self._client = redis.Redis.from_url(url, db=db, decode_responses=True)
def get(self, key: str) -> Optional[str]:
return self._client.get(key)
def set(self, key: str, value: str, ttl: int = None) -> None:
if ttl:
self._client.set(key, value, ex=ttl)
else:
self._client.set(key, value)
def delete(self, key: str) -> None:
self._client.delete(key)
def keys(self, pattern: str) -> List[str]:
return self._client.keys(pattern)
+885
View File
@@ -0,0 +1,885 @@
"""
消息存储服务 - 将每次查询的消息记录存储到 MySQL
"""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional
import pymysql
from config import Config
from services.common.datetime_utils import DateTimeGenerator
from schemas.messages import MessagesDTO
class MessageStorage:
"""消息存储服务"""
def __init__(self):
cfg = Config.get_section("logging_mysql")
self.enabled = str(cfg.get("enabled", "false")).lower() in ("1", "true", "yes")
self.entity_debug_enabled = str(cfg.get("entity_debug_enabled", "false")).lower() in ("1", "true", "yes")
self.host = cfg.get("host", "127.0.0.1")
self.port = int(cfg.get("port", 3306))
self.user = cfg.get("user", "root")
self.password = cfg.get("password", "")
self.database = cfg.get("database", "more_dots")
# 消息落库与结构化日志分表,避免误用 logging_mysql.table=structured_logs
self.table = cfg.get("messages_table", "ipc_apbo.messages")
self.conversation_table = cfg.get("conversation_table", "ipc_apbo.conversations")
self.connect_timeout = int(cfg.get("connect_timeout", 5))
self._inited = False
self._conversation_schema_checked = False
def _get_conn(self):
"""获取数据库连接"""
return pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
charset="utf8mb4",
autocommit=True,
connect_timeout=self.connect_timeout,
)
@staticmethod
def _log_local(event: str, payload: Optional[Dict[str, Any]] = None) -> None:
print(json.dumps({
"level": "ERROR",
"event": event,
"payload": payload or {},
"created_at": DateTimeGenerator.now().epoch_millis,
}, ensure_ascii=False))
def _log_entity_debug(self, event: str, payload: Optional[Dict[str, Any]] = None) -> None:
if not self.entity_debug_enabled:
return
print(json.dumps({
"level": "DEBUG",
"event": event,
"payload": payload or {},
"created_at": DateTimeGenerator.now().epoch_millis,
}, ensure_ascii=False))
@staticmethod
def _now_ms() -> int:
return DateTimeGenerator.now().epoch_millis
@staticmethod
def _build_audit_fields(
*,
random_code: str,
user: Optional[str],
created_value: Any = None,
updated_value: Any = None,
) -> Dict[str, Any]:
created_ms = MessageStorage._resolve_epoch_millis(created_value)
updated_ms = MessageStorage._resolve_epoch_millis(updated_value, fallback=created_ms)
created_bundle = DateTimeGenerator.bundle(created_ms, default_to_now=True)
updated_bundle = DateTimeGenerator.bundle(updated_ms, default_to_now=True)
operator = (user or "system").strip() if isinstance(user, str) else "system"
return {
"random_code": random_code,
"create_user": operator,
"create_date": created_bundle.db_datetime,
"update_user": operator,
"update_date": updated_bundle.db_datetime,
"create_user_name": operator,
"update_user_name": operator,
"created_at": created_ms,
"updated_at": updated_ms,
}
@staticmethod
def _resolve_epoch_millis(value: Any, fallback: Optional[int] = None) -> int:
if value is None:
return fallback if fallback is not None else DateTimeGenerator.now().epoch_millis
if isinstance(value, (int, float)):
raw = int(value)
digits = len(str(abs(raw)))
if digits == 10:
return raw * 1000
return raw
if isinstance(value, str):
text = value.strip()
if text.isdigit():
raw = int(text)
digits = len(text)
if digits == 10:
return raw * 1000
if digits == 13:
return raw
parsed = DateTimeGenerator.bundle(value, default_to_now=True)
return parsed.epoch_millis
parsed = DateTimeGenerator.bundle(value, default_to_now=True)
return parsed.epoch_millis
def _log_entity_stage(self, entity: str, action: str, stage: str, started_at: int, payload: Optional[Dict[str, Any]] = None) -> None:
debug_payload = dict(payload or {})
debug_payload.update({
"entity": entity,
"action": action,
"stage": stage,
"elapsed_ms": max(0, self._now_ms() - started_at),
})
self._log_entity_debug(f"message_storage.{entity}.{action}.{stage}", debug_payload)
@staticmethod
def _split_table_reference(table_name: str, default_schema: str) -> tuple[str, str]:
cleaned = str(table_name or "").strip()
if "." in cleaned:
schema_name, physical_table_name = cleaned.split(".", 1)
else:
schema_name, physical_table_name = default_schema, cleaned
return schema_name.strip().strip("`"), physical_table_name.strip().strip("`")
def _ensure_conversation_schema(self, conn) -> None:
if self._conversation_schema_checked or not self.enabled:
return
started_at = self._now_ms()
self._log_entity_stage(
"conversations",
"ensure_schema",
"start",
started_at,
{"conversation_table": self.conversation_table},
)
schema_name, table_name = self._split_table_reference(self.conversation_table, self.database)
probe_sql = """
SELECT 1
FROM information_schema.columns
WHERE table_schema = %s AND table_name = %s AND column_name = %s
LIMIT 1
"""
with conn.cursor() as cur:
cur.execute(probe_sql, (schema_name, table_name, "name"))
if cur.fetchone() is None:
self._log_entity_stage(
"conversations",
"ensure_schema",
"alter_needed",
started_at,
{"conversation_table": self.conversation_table, "missing_column": "name"},
)
alter_sql = f"""
ALTER TABLE {self.conversation_table}
ADD COLUMN name VARCHAR(255) NULL COMMENT '会话名称' AFTER user
"""
cur.execute(alter_sql)
self._conversation_schema_checked = True
self._log_entity_stage(
"conversations",
"ensure_schema",
"success",
started_at,
{"conversation_table": self.conversation_table, "schema_checked": True},
)
def _ensure_table(self) -> None:
"""确保消息表存在"""
if self._inited or not self.enabled:
return
started_at = self._now_ms()
self._log_entity_stage(
"storage",
"ensure_table",
"start",
started_at,
{"messages_table": self.table, "conversation_table": self.conversation_table},
)
message_sql = f"""
CREATE TABLE IF NOT EXISTS {self.table} (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
random_code VARCHAR(100) NULL COMMENT '业务主键',
create_user VARCHAR(100) NULL COMMENT '创建人',
create_date DATETIME NULL COMMENT '创建时间',
update_user VARCHAR(100) NULL COMMENT '修改人',
update_date DATETIME NULL COMMENT '修改时间',
create_user_name VARCHAR(255) NULL COMMENT '创建人姓名',
update_user_name VARCHAR(255) NULL COMMENT '修改人姓名',
message_id VARCHAR(255) NULL COMMENT '消息 ID',
conversation_id VARCHAR(255) NULL COMMENT '会话 ID',
user VARCHAR(255) NULL COMMENT '用户标识',
query LONGTEXT NULL COMMENT '用户查询',
answer LONGTEXT NULL COMMENT '回答消息内容',
feedback VARCHAR(255) NULL COMMENT '点赞 like / 点踩 dislike',
feedback_content TEXT NULL COMMENT '点踩内容',
created_at BIGINT NULL COMMENT '创建时间(毫秒时间戳)',
updated_at BIGINT NULL COMMENT '更新时间(毫秒时间戳)',
`log` JSON NULL COMMENT '当前对话日志(JSON字符串)',
UNIQUE KEY uk_random_code (random_code),
UNIQUE KEY uk_message_id (message_id),
INDEX idx_conversation_id (conversation_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息记录表';
"""
conversation_sql = f"""
CREATE TABLE IF NOT EXISTS {self.conversation_table} (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
random_code VARCHAR(100) NULL COMMENT '业务主键',
create_user VARCHAR(100) NULL COMMENT '创建人',
create_date DATETIME NULL COMMENT '创建时间',
update_user VARCHAR(100) NULL COMMENT '修改人',
update_date DATETIME NULL COMMENT '修改时间',
create_user_name VARCHAR(255) NULL COMMENT '创建人姓名',
update_user_name VARCHAR(255) NULL COMMENT '修改人姓名',
conversation_id VARCHAR(255) NULL COMMENT '会话 ID',
user VARCHAR(255) NULL COMMENT '用户',
name VARCHAR(512) NULL COMMENT '会话名称',
status VARCHAR(255) NULL COMMENT '状态',
introduction VARCHAR(255) NULL COMMENT '开场白',
created_at BIGINT NULL COMMENT '创建时间(毫秒时间戳)',
updated_at BIGINT NULL COMMENT '更新时间(毫秒时间戳)',
UNIQUE KEY uk_conversation_random_code (random_code),
UNIQUE KEY uk_conversation_id (conversation_id),
INDEX idx_conversation_updated_at (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话记录表';
"""
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
cur.execute(message_sql)
cur.execute(conversation_sql)
self._ensure_conversation_schema(conn)
self._inited = True
self._log_entity_stage(
"storage",
"ensure_table",
"success",
started_at,
{"messages_table": self.table, "conversation_table": self.conversation_table},
)
except Exception as exc:
self._log_entity_stage(
"storage",
"ensure_table",
"failed",
started_at,
{"messages_table": self.table, "conversation_table": self.conversation_table, "error": str(exc)},
)
self._log_local("message_storage.ensure_table_failed", {
"error": str(exc),
"messages_table": self.table,
"conversation_table": self.conversation_table,
})
# 开发阶段容错,避免初始化失败影响主流程
self.enabled = False
def create_conversation(
self,
conversation_id: str,
user: Optional[str],
name: Optional[str],
status: str,
introduction: Optional[str],
created_at: int,
updated_at: int,
) -> bool:
started_at = self._now_ms()
self._log_entity_stage(
"conversations",
"create",
"start",
started_at,
{
"conversation_id": conversation_id,
"user": user,
"name_len": len(name or ""),
"status": status,
},
)
if not self.enabled:
self._log_entity_stage(
"conversations",
"create",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled"},
)
return False
self._ensure_table()
if not self.enabled:
self._log_entity_stage(
"conversations",
"create",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
)
return False
insert_sql = f"""
INSERT INTO {self.conversation_table}(
random_code, create_user, create_date, update_user, update_date,
create_user_name, update_user_name,
conversation_id, user, name, status, introduction, created_at, updated_at
) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
try:
audit = self._build_audit_fields(
random_code=conversation_id,
user=user,
created_value=created_at,
updated_value=updated_at,
)
with self._get_conn() as conn:
self._ensure_conversation_schema(conn)
with conn.cursor() as cur:
self._log_entity_stage(
"conversations",
"create",
"sql_execute",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table},
)
cur.execute(
insert_sql,
(
audit["random_code"],
audit["create_user"],
audit["create_date"],
audit["update_user"],
audit["update_date"],
audit["create_user_name"],
audit["update_user_name"],
conversation_id,
user,
name,
status,
introduction,
audit["created_at"],
audit["updated_at"],
),
)
self._log_entity_stage(
"conversations",
"create",
"success",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table},
)
return True
except Exception as exc:
self._log_entity_stage(
"conversations",
"create",
"failed",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table, "error": str(exc)},
)
self._log_local("message_storage.create_conversation_failed", {
"error": str(exc),
"conversation_table": self.conversation_table,
"conversation_id": conversation_id,
})
return False
def get_conversation_by_id(self, conversation_id: str) -> Optional[Dict[str, Any]]:
started_at = self._now_ms()
self._log_entity_stage(
"conversations",
"get_by_id",
"start",
started_at,
{"conversation_id": conversation_id},
)
if not self.enabled:
self._log_entity_stage(
"conversations",
"get_by_id",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled"},
)
return None
self._ensure_table()
if not self.enabled:
self._log_entity_stage(
"conversations",
"get_by_id",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
)
return None
select_sql = f"""
SELECT conversation_id, user, name, status, introduction, created_at, updated_at
FROM {self.conversation_table}
WHERE conversation_id = %s
LIMIT 1
"""
try:
with self._get_conn() as conn:
self._ensure_conversation_schema(conn)
with conn.cursor(pymysql.cursors.DictCursor) as cur:
self._log_entity_stage(
"conversations",
"get_by_id",
"sql_execute",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table},
)
cur.execute(select_sql, (conversation_id,))
result = cur.fetchone()
self._log_entity_stage(
"conversations",
"get_by_id",
"success",
started_at,
{
"conversation_id": conversation_id,
"table": self.conversation_table,
"found": bool(result),
},
)
return dict(result) if result else None
except Exception as exc:
self._log_entity_stage(
"conversations",
"get_by_id",
"failed",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table, "error": str(exc)},
)
self._log_local("message_storage.get_conversation_failed", {
"error": str(exc),
"conversation_table": self.conversation_table,
"conversation_id": conversation_id,
})
return None
def update_conversation_updated_at(self, conversation_id: str, updated_at: int) -> bool:
started_at = self._now_ms()
self._log_entity_stage(
"conversations",
"update_updated_at",
"start",
started_at,
{"conversation_id": conversation_id, "updated_at": updated_at},
)
if not self.enabled:
self._log_entity_stage(
"conversations",
"update_updated_at",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled"},
)
return False
self._ensure_table()
if not self.enabled:
self._log_entity_stage(
"conversations",
"update_updated_at",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
)
return False
update_sql = f"""
UPDATE {self.conversation_table}
SET updated_at = %s,
update_date = %s
WHERE conversation_id = %s
"""
try:
update_ms = self._resolve_epoch_millis(updated_at)
update_bundle = DateTimeGenerator.bundle(update_ms, default_to_now=True)
with self._get_conn() as conn:
self._ensure_conversation_schema(conn)
with conn.cursor() as cur:
self._log_entity_stage(
"conversations",
"update_updated_at",
"sql_execute",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table},
)
affected_rows = cur.execute(
update_sql,
(update_ms, update_bundle.db_datetime, conversation_id),
)
self._log_entity_stage(
"conversations",
"update_updated_at",
"success",
started_at,
{"conversation_id": conversation_id, "affected_rows": int(affected_rows or 0)},
)
return bool(affected_rows)
except Exception as exc:
self._log_entity_stage(
"conversations",
"update_updated_at",
"failed",
started_at,
{"conversation_id": conversation_id, "table": self.conversation_table, "error": str(exc)},
)
self._log_local("message_storage.update_conversation_failed", {
"error": str(exc),
"conversation_table": self.conversation_table,
"conversation_id": conversation_id,
})
return False
def save_message(
self,
conversation_id: str,
message_id: str,
query: str,
answer: Optional[str] = None,
workflow_type: Optional[str] = None,
user: Optional[str] = None,
sql_query: Optional[str] = None,
execution_result: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None,
created_at: Optional[int] = None,
updated_at: Optional[int] = None,
logs: Optional[List[str]] = None,
) -> bool:
started_at = self._now_ms()
self._log_entity_stage(
"messages",
"create",
"start",
started_at,
{
"conversation_id": conversation_id,
"message_id": message_id,
"query_len": len(query or ""),
"answer_len": len(answer or ""),
"workflow_type": workflow_type,
},
)
"""
保存消息记录
Args:
conversation_id: 会话 ID
message_id: 消息 ID
query: 用户查询
answer: AI 回复
workflow_type: 工作流类型 (conversation/tool_using)
user: 用户标识
sql_query: 生成的 SQL
execution_result: SQL 执行结果
metadata: 其他元数据
logs: 过程日志(兼容 Java saveMessageToDB)
Returns:
bool: 是否保存成功
"""
if not self.enabled:
self._log_entity_stage(
"messages",
"create",
"skipped",
started_at,
{"conversation_id": conversation_id, "message_id": message_id, "reason": "storage_disabled"},
)
return False
self._ensure_table()
if not self.enabled:
self._log_entity_stage(
"messages",
"create",
"skipped",
started_at,
{"conversation_id": conversation_id, "message_id": message_id, "reason": "storage_disabled_after_init"},
)
return False
audit = self._build_audit_fields(
random_code=message_id,
user=user,
created_value=created_at,
updated_value=updated_at,
)
log_payload = {
"workflow_type": workflow_type,
"sql_query": sql_query,
"execution_result": execution_result or {},
"metadata": metadata or {},
}
normalized_logs = [str(item) for item in (logs or []) if str(item).strip()]
if normalized_logs:
log_payload["data"] = "\n".join(normalized_logs)
message_record = MessagesDTO(
message_id=message_id,
conversation_id=conversation_id,
user=user,
query=query,
answer=answer,
feedback=None,
feedback_content=None,
created_at=audit["created_at"],
updated_at=audit["updated_at"],
log=log_payload,
)
insert_sql = f"""
INSERT INTO {self.table}(
random_code, create_user, create_date, update_user, update_date,
create_user_name, update_user_name,
message_id, conversation_id, user, query, answer,
feedback, feedback_content, created_at, updated_at, `log`
) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
self._log_entity_stage(
"messages",
"create",
"sql_execute",
started_at,
{"conversation_id": conversation_id, "message_id": message_id, "table": self.table},
)
cur.execute(
insert_sql,
(
audit["random_code"],
audit["create_user"],
audit["create_date"],
audit["update_user"],
audit["update_date"],
audit["create_user_name"],
audit["update_user_name"],
message_record.message_id,
message_record.conversation_id,
message_record.user,
message_record.query,
message_record.answer,
message_record.feedback,
message_record.feedback_content,
message_record.created_at,
message_record.updated_at,
json.dumps(message_record.log, ensure_ascii=False),
),
)
self._log_entity_stage(
"messages",
"create",
"success",
started_at,
{"conversation_id": conversation_id, "message_id": message_id, "table": self.table},
)
return True
except Exception as exc:
self._log_entity_stage(
"messages",
"create",
"failed",
started_at,
{"conversation_id": conversation_id, "message_id": message_id, "table": self.table, "error": str(exc)},
)
self._log_local("message_storage.save_failed", {
"error": str(exc),
"messages_table": self.table,
"message_id": message_id,
"conversation_id": conversation_id,
})
# 开发阶段容错,避免日志失败影响主流程
return False
def get_conversation_history(
self,
conversation_id: str,
limit: int = 20
) -> List[Dict[str, Any]]:
"""
获取会话历史消息
Args:
conversation_id: 会话 ID
limit: 返回消息数量
Returns:
List[Dict[str, Any]]: 消息列表
"""
started_at = self._now_ms()
self._log_entity_stage(
"messages",
"get_history",
"start",
started_at,
{"conversation_id": conversation_id, "limit": limit},
)
if not self.enabled:
self._log_entity_stage(
"messages",
"get_history",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled"},
)
return []
self._ensure_table()
if not self.enabled:
self._log_entity_stage(
"messages",
"get_history",
"skipped",
started_at,
{"conversation_id": conversation_id, "reason": "storage_disabled_after_init"},
)
return []
select_sql = f"""
SELECT * FROM {self.table}
WHERE conversation_id = %s
ORDER BY created_at DESC
LIMIT %s
"""
try:
with self._get_conn() as conn:
with conn.cursor(pymysql.cursors.DictCursor) as cur:
self._log_entity_stage(
"messages",
"get_history",
"sql_execute",
started_at,
{"conversation_id": conversation_id, "limit": limit, "table": self.table},
)
cur.execute(select_sql, (conversation_id, limit))
results = cur.fetchall()
self._log_entity_stage(
"messages",
"get_history",
"success",
started_at,
{"conversation_id": conversation_id, "count": len(results or [])},
)
return list(results)
except Exception as exc:
self._log_entity_stage(
"messages",
"get_history",
"failed",
started_at,
{"conversation_id": conversation_id, "error": str(exc)},
)
return []
def update_feedback_by_message_id(
self,
message_id: str,
feedback: str,
feedback_content: Optional[str] = None,
) -> bool:
"""按 message_id 回写点赞/点踩反馈。"""
started_at = self._now_ms()
self._log_entity_stage(
"messages",
"update_feedback",
"start",
started_at,
{"message_id": message_id, "feedback": feedback},
)
if not self.enabled:
self._log_entity_stage(
"messages",
"update_feedback",
"skipped",
started_at,
{"message_id": message_id, "reason": "storage_disabled"},
)
return False
self._ensure_table()
if not self.enabled:
self._log_entity_stage(
"messages",
"update_feedback",
"skipped",
started_at,
{"message_id": message_id, "reason": "storage_disabled_after_init"},
)
return False
update_sql = f"""
UPDATE {self.table}
SET feedback = %s,
feedback_content = %s,
updated_at = %s,
update_date = %s
WHERE message_id = %s
"""
normalized_feedback_content = (feedback_content or "").strip() or None
now_bundle = DateTimeGenerator.now()
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
self._log_entity_stage(
"messages",
"update_feedback",
"sql_execute",
started_at,
{"message_id": message_id, "table": self.table},
)
affected_rows = cur.execute(
update_sql,
(
feedback,
normalized_feedback_content,
now_bundle.epoch_millis,
now_bundle.db_datetime,
message_id,
),
)
self._log_entity_stage(
"messages",
"update_feedback",
"success",
started_at,
{"message_id": message_id, "affected_rows": int(affected_rows or 0)},
)
return bool(affected_rows)
except Exception as exc:
self._log_entity_stage(
"messages",
"update_feedback",
"failed",
started_at,
{"message_id": message_id, "error": str(exc)},
)
# 开发阶段容错,避免日志失败影响主流程
return False
# 全局单例
_GLOBAL_MESSAGE_STORAGE: Optional[MessageStorage] = None
def get_message_storage() -> MessageStorage:
"""获取消息存储服务实例"""
global _GLOBAL_MESSAGE_STORAGE
if _GLOBAL_MESSAGE_STORAGE is None:
_GLOBAL_MESSAGE_STORAGE = MessageStorage()
return _GLOBAL_MESSAGE_STORAGE
+104
View File
@@ -0,0 +1,104 @@
from __future__ import annotations
import json
from typing import Any, Dict, Optional
import pymysql
from config import Config
from services.common.datetime_utils import DateTimeGenerator
class StructuredLogger:
def __init__(self):
cfg = Config.get_section("logging_mysql")
self.enabled = str(cfg.get("enabled", "false")).lower() in ("1", "true", "yes")
self.host = cfg.get("host", "127.0.0.1")
self.port = int(cfg.get("port", 3306))
self.user = cfg.get("user", "root")
self.password = cfg.get("password", "")
self.database = cfg.get("database", "more_dots")
self.table = cfg.get("table", "structured_logs")
self.connect_timeout = int(cfg.get("connect_timeout", 5))
self._inited = False
def _get_conn(self):
return pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
charset="utf8mb4",
autocommit=True,
connect_timeout=self.connect_timeout,
)
def _ensure_table(self) -> None:
if self._inited or not self.enabled:
return
sql = f"""
CREATE TABLE IF NOT EXISTS {self.table} (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trace_id VARCHAR(64) NOT NULL,
level VARCHAR(16) NOT NULL,
event VARCHAR(128) NOT NULL,
error_code VARCHAR(64) NULL,
payload JSON NULL,
created_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
cur.execute(sql)
self._inited = True
except Exception:
# 开发阶段容错,避免日志失败影响主流程
self.enabled = False
def log(self, level: str, event: str, trace_id: str, payload: Optional[Dict[str, Any]] = None, error_code: Optional[str] = None) -> None:
print(json.dumps({
"trace_id": trace_id,
"level": level,
"event": event,
"error_code": error_code,
"payload": payload or {},
"created_at": DateTimeGenerator.now().iso_str,
}, ensure_ascii=False))
if not self.enabled:
return
self._ensure_table()
if not self.enabled:
return
insert_sql = f"INSERT INTO {self.table}(trace_id, level, event, error_code, payload, created_at) VALUES(%s,%s,%s,%s,%s,%s)"
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
insert_sql,
(
trace_id,
level,
event,
error_code,
json.dumps(payload or {}, ensure_ascii=False),
DateTimeGenerator.now().db_datetime,
),
)
except Exception:
# 开发阶段容错,避免日志失败影响主流程
return
_GLOBAL_STRUCTURED_LOGGER: Optional[StructuredLogger] = None
def get_structured_logger() -> StructuredLogger:
global _GLOBAL_STRUCTURED_LOGGER
if _GLOBAL_STRUCTURED_LOGGER is None:
_GLOBAL_STRUCTURED_LOGGER = StructuredLogger()
return _GLOBAL_STRUCTURED_LOGGER