99import time
1010from asyncio import Queue
1111from collections import deque
12+ from collections .abc import Iterator
13+ from contextlib import contextmanager
14+ from contextvars import ContextVar
15+ from dataclasses import dataclass
1216from pathlib import Path
1317from typing import TYPE_CHECKING
1418
2832PLUGIN_LOG_LEVELS = ("DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" )
2933"""Allowed per-plugin log levels."""
3034
35+
36+ @dataclass
37+ class LogContext :
38+ """Platform metadata attached to logs emitted inside an async operation.
39+
40+ Attributes:
41+ platform_id: Unique configured platform instance ID.
42+ platform_type: Platform adapter type, such as ``telegram``.
43+ active: Whether copied async contexts may still use this metadata.
44+ """
45+
46+ platform_id : str | None
47+ platform_type : str | None
48+ active : bool = True
49+
50+
51+ _log_context : ContextVar [LogContext | None ] = ContextVar (
52+ "astrbot_log_context" ,
53+ default = None ,
54+ )
55+
56+
57+ @contextmanager
58+ def log_context (
59+ platform_id : str | None ,
60+ platform_type : str | None ,
61+ ) -> Iterator [None ]:
62+ """Bind platform metadata to logs in the current execution context.
63+
64+ Copied contexts share the same mutable object. Marking it inactive on exit
65+ prevents detached tasks from retaining stale event platform metadata.
66+
67+ Args:
68+ platform_id: Unique configured platform instance ID.
69+ platform_type: Platform adapter type.
70+
71+ Yields:
72+ Control while the platform metadata is active.
73+ """
74+ context = LogContext (platform_id = platform_id , platform_type = platform_type )
75+ token = _log_context .set (context )
76+ try :
77+ yield
78+ finally :
79+ context .active = False
80+ _log_context .reset (token )
81+
82+
3183if TYPE_CHECKING :
3284 from loguru import Record
3385
@@ -38,11 +90,15 @@ class _RecordEnricherFilter(logging.Filter):
3890 def filter (self , record : logging .LogRecord ) -> bool :
3991 if record .name .startswith (PLUGIN_LOGGER_PREFIX ):
4092 # Records from a per-plugin logger are tagged with the plugin name.
41- record .plugin_tag = f"[{ record .name [len (PLUGIN_LOGGER_PREFIX ) :]} ]"
93+ plugin_name = record .name [len (PLUGIN_LOGGER_PREFIX ) :]
94+ record .plugin_tag = f"[{ plugin_name } ]"
95+ record .source = "plugin"
96+ record .plugin_name = plugin_name
4297 else :
43- record .plugin_tag = (
44- "[Plug]" if _is_plugin_path (record .pathname ) else "[Core]"
45- )
98+ is_plugin = _is_plugin_path (record .pathname )
99+ record .plugin_tag = "[Plug]" if is_plugin else "[Core]"
100+ record .source = "plugin" if is_plugin else "core"
101+ record .plugin_name = None
46102 record .short_levelname = _get_short_level_name (record .levelname )
47103 record .astrbot_version_tag = (
48104 f" [v{ VERSION } ]" if record .levelno >= logging .WARNING else ""
@@ -51,6 +107,14 @@ def filter(self, record: logging.LogRecord) -> bool:
51107 record .source_line = record .lineno
52108 record .is_trace = record .name == "astrbot.trace"
53109 record .category = getattr (record , "category" , None ) or "system"
110+ context = _log_context .get ()
111+ context_is_active = context is not None and context .active
112+ record .platform_id = getattr (record , "platform_id" , None ) or (
113+ context .platform_id if context_is_active else None
114+ )
115+ record .platform_type = getattr (record , "platform_type" , None ) or (
116+ context .platform_type if context_is_active else None
117+ )
54118 return True
55119
56120
@@ -108,6 +172,10 @@ def _patch_record(record: "Record") -> None:
108172 extra .setdefault ("source_line" , record ["line" ])
109173 extra .setdefault ("is_trace" , False )
110174 extra .setdefault ("category" , "system" )
175+ extra .setdefault ("source" , "core" )
176+ extra .setdefault ("plugin_name" , None )
177+ extra .setdefault ("platform_id" , None )
178+ extra .setdefault ("platform_type" , None )
111179
112180
113181_loguru = _raw_loguru_logger .patch (_patch_record )
@@ -135,6 +203,11 @@ def emit(self, record: logging.LogRecord) -> None:
135203 ),
136204 "source_line" : getattr (record , "source_line" , record .lineno ),
137205 "is_trace" : getattr (record , "is_trace" , record .name == "astrbot.trace" ),
206+ "category" : getattr (record , "category" , None ) or "system" ,
207+ "source" : getattr (record , "source" , "core" ),
208+ "plugin_name" : getattr (record , "plugin_name" , None ),
209+ "platform_id" : getattr (record , "platform_id" , None ),
210+ "platform_type" : getattr (record , "platform_type" , None ),
138211 }
139212
140213 _loguru .bind (** payload ).opt (exception = record .exc_info ).log (
@@ -182,6 +255,10 @@ def emit(self, record: logging.LogRecord) -> None:
182255 "time" : time .time (),
183256 "data" : log_entry ,
184257 "category" : getattr (record , "category" , None ) or "system" ,
258+ "source" : getattr (record , "source" , "core" ),
259+ "plugin_name" : getattr (record , "plugin_name" , None ),
260+ "platform_id" : getattr (record , "platform_id" , None ),
261+ "platform_type" : getattr (record , "platform_type" , None ),
185262 },
186263 )
187264
0 commit comments