feat: improve log filtering and console workspace (#10514) · AstrBotDevs/AstrBot@ffd77c9 · GitHub
Skip to content

Commit ffd77c9

Browse files
authored
feat: improve log filtering and console workspace (#10514)
1 parent 44def64 commit ffd77c9

21 files changed

Lines changed: 906 additions & 320 deletions

File tree

‎astrbot/core/event_bus.py‎

Lines changed: 7 additions & 2 deletions

‎astrbot/core/log.py‎

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import time
1010
from asyncio import Queue
1111
from collections import deque
12+
from collections.abc import Iterator
13+
from contextlib import contextmanager
14+
from contextvars import ContextVar
15+
from dataclasses import dataclass
1216
from pathlib import Path
1317
from typing import TYPE_CHECKING
1418

@@ -28,6 +32,54 @@
2832
PLUGIN_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+
3183
if 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

‎astrbot/core/pipeline/scheduler.py‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import cast
33

44
from astrbot.core import logger
5+
from astrbot.core.log import log_context
56
from astrbot.core.platform import AstrMessageEvent
67
from astrbot.core.platform.sources.webchat.webchat_event import WebChatMessageEvent
78
from astrbot.core.platform.sources.wecom_ai_bot.wecomai_event import (
@@ -87,14 +88,15 @@ async def execute(self, event: AstrMessageEvent) -> None:
8788
8889
"""
8990
active_event_registry.register(event)
90-
try:
91-
await self._process_stages(event)
92-
93-
# 发送一个空消息, 以便于后续的处理
94-
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
95-
await event.send(None)
96-
97-
logger.debug("pipeline execution completed.")
98-
finally:
99-
event.cleanup_temporary_local_files()
100-
active_event_registry.unregister(event)
91+
with log_context(event.get_platform_id(), event.get_platform_name()):
92+
try:
93+
await self._process_stages(event)
94+
95+
# Send an empty message for platform-specific completion handling.
96+
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
97+
await event.send(None)
98+
99+
logger.debug("pipeline execution completed.")
100+
finally:
101+
event.cleanup_temporary_local_files()
102+
active_event_registry.unregister(event)

‎astrbot/core/platform/manager.py‎

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from astrbot.core import logger
77
from astrbot.core.config.astrbot_config import AstrBotConfig
8+
from astrbot.core.log import log_context
89
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
910
from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
1011

@@ -49,7 +50,10 @@ def _sanitize_platform_id(self, platform_id: str | None) -> tuple[str | None, bo
4950
return sanitized, sanitized != platform_id
5051

5152
def _start_platform_task(self, task_name: str, inst: Platform) -> None:
52-
run_task = asyncio.create_task(inst.run(), name=task_name)
53+
run_task = asyncio.create_task(
54+
self._run_platform(inst),
55+
name=task_name,
56+
)
5357
wrapper_task = asyncio.create_task(
5458
self._task_wrapper(run_task, platform=inst),
5559
name=f"{task_name}_wrapper",
@@ -59,6 +63,17 @@ def _start_platform_task(self, task_name: str, inst: Platform) -> None:
5963
wrapper=wrapper_task,
6064
)
6165

66+
async def _run_platform(self, inst: Platform) -> None:
67+
"""Run an adapter with its platform metadata bound to emitted logs.
68+
69+
Args:
70+
inst: Platform adapter instance to run.
71+
"""
72+
meta = inst.meta()
73+
platform_id = meta.id or inst.config.get("id")
74+
with log_context(platform_id, meta.name):
75+
await inst.run()
76+
6277
async def _stop_platform_task(self, client_id: str) -> None:
6378
tasks = self._platform_tasks.pop(client_id, None)
6479
if not tasks:
@@ -130,6 +145,10 @@ async def load_platform(self, platform_config: dict) -> None:
130145
"Loading IM platform adapter %s(%s) ...",
131146
platform_config["type"],
132147
platform_config["id"],
148+
extra={
149+
"platform_id": platform_config["id"],
150+
"platform_type": platform_config["type"],
151+
},
133152
)
134153
match platform_config["type"]:
135154
case "aiocqhttp":
@@ -240,26 +259,30 @@ async def load_platform(self, platform_config: dict) -> None:
240259
async def _task_wrapper(
241260
self, task: asyncio.Task, platform: Platform | None = None
242261
) -> None:
243-
# 设置平台状态为运行中
244-
if platform:
245-
platform.status = PlatformStatus.RUNNING
246-
247-
try:
248-
await task
249-
except asyncio.CancelledError:
262+
meta = platform.meta() if platform else None
263+
platform_id = (meta.id or platform.config.get("id")) if meta else None
264+
platform_type = meta.name if meta else None
265+
with log_context(platform_id, platform_type):
266+
# 设置平台状态为运行中
250267
if platform:
251-
platform.status = PlatformStatus.STOPPED
252-
except Exception as e:
253-
error_msg = str(e)
254-
tb_str = traceback.format_exc()
255-
logger.error(f"------- Task {task.get_name()} failed: {e}")
256-
for line in tb_str.split("\n"):
257-
logger.error(f"| {line}")
258-
logger.error("-------")
259-
260-
# 记录错误到平台实例
261-
if platform:
262-
platform.record_error(error_msg, tb_str)
268+
platform.status = PlatformStatus.RUNNING
269+
270+
try:
271+
await task
272+
except asyncio.CancelledError:
273+
if platform:
274+
platform.status = PlatformStatus.STOPPED
275+
except Exception as e:
276+
error_msg = str(e)
277+
tb_str = traceback.format_exc()
278+
logger.error(f"------- Task {task.get_name()} failed: {e}")
279+
for line in tb_str.split("\n"):
280+
logger.error(f"| {line}")
281+
logger.error("-------")
282+
283+
# 记录错误到平台实例
284+
if platform:
285+
platform.record_error(error_msg, tb_str)
263286

264287
async def reload(self, platform_config: dict) -> None:
265288
await self.terminate_platform(platform_config["id"])

‎astrbot/dashboard/services/platform_service.py‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from astrbot.core import logger
77
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
8+
from astrbot.core.log import log_context
89
from astrbot.core.platform import Platform
910
from astrbot.core.platform.sources.dingtalk.app_registration import (
1011
poll_dingtalk_app_registration_once,
@@ -46,16 +47,17 @@ async def handle_webhook_callback(self, webhook_uuid: str, request_obj):
4647
logger.warning(f"未找到 webhook_uuid 为 {webhook_uuid} 的平台")
4748
raise PlatformServiceError("未找到对应平台", 404)
4849

49-
try:
50-
return await platform_adapter.webhook_callback(request_obj)
51-
except NotImplementedError as exc:
52-
logger.error(
53-
f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法"
54-
)
55-
raise PlatformServiceError("平台未支持统一 Webhook 模式", 500) from exc
56-
except Exception as exc:
57-
logger.error(f"处理 webhook 回调时发生错误: {exc}", exc_info=True)
58-
raise PlatformServiceError("处理回调失败", 500) from exc
50+
meta = platform_adapter.meta()
51+
platform_id = meta.id or platform_adapter.config.get("id")
52+
with log_context(platform_id, meta.name):
53+
try:
54+
return await platform_adapter.webhook_callback(request_obj)
55+
except NotImplementedError as exc:
56+
logger.error(f"平台 {meta.name} 未实现 webhook_callback 方法")
57+
raise PlatformServiceError("平台未支持统一 Webhook 模式", 500) from exc
58+
except Exception as exc:
59+
logger.error(f"处理 webhook 回调时发生错误: {exc}", exc_info=True)
60+
raise PlatformServiceError("处理回调失败", 500) from exc
5961

6062
def find_platform_by_uuid(self, webhook_uuid: str) -> Platform | None:
6163
for platform in self.platform_manager.platform_insts:

‎changelogs/unreleased.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
# Unreleased
2+
3+
## Logs
4+
5+
- Add structured source, plugin, platform, and category metadata for in-memory log filtering.
6+
- Logs: top-row filters → the Filters sidebar, with combined level/source/plugin/platform/keyword filtering and an option to hide user-chat logs.
7+
- Core source label → System; plugin selectors prefer display names and show IDs as subtitles.
8+
- Auto-scroll switch → automatic bottom following, preserving the reading position when viewing older logs.
9+
- Add 8–16px font controls (12px default, 2px steps) alongside fullscreen and Pip installation.

‎dashboard/src/assets/mdi-subset/materialdesignicons-subset.css‎

Lines changed: 5 additions & 1 deletion
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)