Files
life-echo/api/app/core/log_events.py

133 lines
4.1 KiB
Python
Raw Normal View History

"""结构化日志辅助:统一 ``event=`` 行格式与 Celery prerun 可提取的上下文字段。"""
from __future__ import annotations
from typing import Any
def format_log_event(event: str, **fields: Any) -> str:
"""
生成单行 ``event=...`` 日志正文``event`` 固定首位``msg`` 固定末位若提供其余键按字母序
``None`` 与空字符串会跳过浮点数默认保留一位小数适用于 ``duration_ms``
"""
parts: list[str] = [f"event={event}"]
keys = sorted(k for k in fields if k != "msg")
ordered = list(keys)
if "msg" in fields:
ordered.append("msg")
for k in ordered:
v = fields[k]
if v is None:
continue
if isinstance(v, float):
parts.append(f"{k}={v:.1f}")
elif isinstance(v, bool):
parts.append(f"{k}={str(v).lower()}")
else:
s = str(v).strip()
if not s:
continue
parts.append(f"{k}={s}")
return " ".join(parts)
def correlation_bind_kwargs(
*,
user_id: str | None = None,
memoir_correlation_id: str | None = None,
correlation_id: str | None = None,
**more: str | None,
) -> dict[str, str]:
"""供 ``logger.bind(**...)````memoir_correlation_id`` 会以 ``correlation_id`` 写入(统一检索键)。"""
out: dict[str, str] = {}
uid = (user_id or "").strip()
if uid:
out["user_id"] = uid
cid = (correlation_id or memoir_correlation_id or "").strip()
if cid:
out["correlation_id"] = cid
for k, v in more.items():
if v is None:
continue
s = str(v).strip()
if s:
out[str(k)] = s
return out
# bind=True 任务的 positional 与字段名映射kwargs 优先,缺位再填)
_TASK_POSITIONAL_FIELDS: dict[str, tuple[str, ...]] = {
2026-04-30 16:22:55 +08:00
"app.tasks.memory_enrichment_tasks.embed_memory_source": ("user_id", "source_id"),
"app.tasks.memory_enrichment_tasks.enrich_memory_source": ("user_id", "source_id"),
"app.tasks.memory_compaction_tasks.memory_compaction_run": ("user_id",),
"app.tasks.chapter_compose_tasks.recompose_chapter": ("chapter_id",),
"app.tasks.memoir_quality_pass_tasks.memoir_quality_pass": ("user_id",),
"app.tasks.memoir_tasks.process_memoir_phase2": ("user_id", "chapter_category"),
"app.tasks.memoir_tasks.process_memoir_phase1": ("user_id",),
"app.tasks.memoir_tasks.generate_chapter_content": ("user_id", "stage"),
"app.tasks.chapter_cover_tasks.generate_chapter_cover": ("chapter_id",),
"app.tasks.story_image_tasks.generate_story_image": ("story_id",),
"app.tasks.story_title_tasks.generate_story_title_after_create": (
"story_id",
"chapter_category",
"oral_scope",
"user_id",
),
}
_KW_KEYS_COPY: tuple[str, ...] = (
"user_id",
"source_id",
"chapter_id",
"story_id",
"chapter_category",
"stage",
"oral_scope",
)
def celery_prerun_extras(
task_name: str | None,
args: tuple[Any, ...],
kwargs: dict[str, Any] | None,
) -> dict[str, str]:
"""
Celery ``task_prerun`` args/kwargs 提取 ``user_id````correlation_id``
``set_celery_log_extras`` 与任务体内 loguru 记录关联
"""
out: dict[str, str] = {}
kw = dict(kwargs or {})
mcid = kw.get("memoir_correlation_id")
if mcid is not None:
s = str(mcid).strip()
if s:
out["correlation_id"] = s
for key in _KW_KEYS_COPY:
if key not in kw:
continue
val = kw[key]
if val is None:
continue
s = str(val).strip()
if s:
out[key] = s
name = (task_name or "").strip()
fields = _TASK_POSITIONAL_FIELDS.get(name)
if fields and args:
for i, field in enumerate(fields):
if i >= len(args):
break
if field in out:
continue
val = args[i]
if val is None:
continue
s = str(val).strip()
if s:
out[field] = s
return out