Skip to content

Commit

Permalink
feat: 使用 structlog 作为日志库
Browse files Browse the repository at this point in the history
  • Loading branch information
st1020 committed Mar 12, 2024
1 parent 28cff73 commit 8018936
Show file tree
Hide file tree
Showing 18 changed files with 350 additions and 235 deletions.
10 changes: 6 additions & 4 deletions alicebot/adapter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
overload,
)

import structlog

from alicebot.event import Event
from alicebot.typing import ConfigT, EventT
from alicebot.utils import is_config_class
Expand All @@ -28,6 +30,8 @@

__all__ = ["Adapter"]

logger = structlog.stdlib.get_logger()

if os.getenv("ALICEBOT_DEV") == "1": # pragma: no cover
# 当处于开发环境时,使用 pkg_resources 风格的命名空间包
__import__("pkg_resources").declare_namespace(__name__)
Expand Down Expand Up @@ -77,10 +81,8 @@ async def safe_run(self) -> None:
"""附带有异常处理地安全运行适配器。"""
try:
await self.run()
except Exception as e:
self.bot.error_or_exception(
f"Run adapter {self.__class__.__name__} failed:", e
)
except Exception:
logger.exception("Run adapter failed", adapter_name=self.__class__.__name__)

@abstractmethod
async def run(self) -> None:
Expand Down
12 changes: 7 additions & 5 deletions alicebot/adapter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from typing import Literal, Optional, Union

import aiohttp
import structlog
from aiohttp import web

from alicebot.adapter import Adapter
from alicebot.log import logger
from alicebot.typing import ConfigT, EventT

__all__ = [
Expand All @@ -23,6 +23,8 @@
"WebSocketAdapter",
]

logger = structlog.stdlib.get_logger()


class PollingAdapter(Adapter[EventT, ConfigT], metaclass=ABCMeta):
"""轮询式适配器示例。"""
Expand Down Expand Up @@ -206,8 +208,8 @@ async def startup(self) -> None:
self.app.add_routes([web.get(self.url, self.handle_reverse_ws_response)])
else:
logger.error(
'Config "adapter_type" must be "ws" or "reverse-ws", not '
+ self.adapter_type
'Config "adapter_type" must be "ws" or "reverse-ws"',
adapter_type=self.adapter_type,
)

async def run(self) -> None:
Expand All @@ -216,8 +218,8 @@ async def run(self) -> None:
while True:
try:
await self.websocket_connect()
except aiohttp.ClientError as e:
self.bot.error_or_exception("WebSocket connection error:", e)
except aiohttp.ClientError:
logger.exception("WebSocket connection error")
if self.bot.should_exit.is_set():
break
await asyncio.sleep(self.reconnect_interval)
Expand Down
138 changes: 75 additions & 63 deletions alicebot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
overload,
)

import structlog
from pydantic import (
ValidationError,
create_model, # pyright: ignore[reportUnknownVariableType]
Expand All @@ -43,7 +44,6 @@
SkipException,
StopException,
)
from alicebot.log import logger
from alicebot.plugin import Plugin, PluginLoadType
from alicebot.typing import AdapterHook, AdapterT, BotHook, EventHook, EventT
from alicebot.utils import (
Expand All @@ -67,6 +67,8 @@
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
)

logger = structlog.stdlib.get_logger()


class Bot:
"""AliceBot 机器人对象,定义了机器人的基本方法。
Expand Down Expand Up @@ -235,8 +237,8 @@ async def _run(self) -> None:
await adapter_startup_hook_func(_adapter)
try:
await _adapter.startup()
except Exception as e:
self.error_or_exception(f"Startup adapter {_adapter!r} failed:", e)
except Exception:
logger.exception("Startup adapter failed", adapter=_adapter)

for _adapter in self.adapters:
for adapter_run_hook_func in self._adapter_run_hooks:
Expand Down Expand Up @@ -283,8 +285,7 @@ def _remove_plugin_by_path(
for plugin_ in _removed_plugins:
plugins.remove(plugin_)
logger.info(
"Succeeded to remove plugin "
f'"{plugin_.__name__}" from file "{file}"'
"Succeeded to remove plugin from file", plugin=plugin_, file=file
)
return removed_plugins

Expand Down Expand Up @@ -322,7 +323,7 @@ async def _run_hot_reload(self) -> None: # pragma: no cover
and samefile(self._config_file, file)
and change_type == change_type.modified
):
logger.info(f'Reload config file "{self._config_file}"')
logger.info("Reload config file", file=self._config_file)
old_config = self.config
self._reload_config_dict()
if (
Expand All @@ -346,18 +347,18 @@ async def _run_hot_reload(self) -> None: # pragma: no cover
continue

if change_type == Change.added:
logger.info(f"Hot reload: Added file: {file}")
logger.info("Hot reload: Added file", file=file)
self._load_plugins(
Path(file), plugin_load_type=PluginLoadType.DIR, reload=True
)
self._update_config()
continue
if change_type == Change.deleted:
logger.info(f"Hot reload: Deleted file: {file}")
logger.info("Hot reload: Deleted file", file=file)
self._remove_plugin_by_path(file)
self._update_config()
elif change_type == Change.modified:
logger.info(f"Hot reload: Modified file: {file}")
logger.info("Hot reload: Modified file", file=file)
self._remove_plugin_by_path(file)
self._load_plugins(
Path(file), plugin_load_type=PluginLoadType.DIR, reload=True
Expand Down Expand Up @@ -394,9 +395,27 @@ def update_config(
adapter=update_config(self.adapters, "AdapterConfig", AdapterConfig),
__base__=MainConfig,
)(**self._raw_config_dict)
# 更新 log 级别
logger.remove()
logger.add(sys.stderr, level=self.config.bot.log.level)

if self.config.bot.log is not None:
log_level = 0
if isinstance(self.config.bot.log.level, int):
log_level = self.config.bot.log.level
elif isinstance(self.config.bot.log.level, str):
log_level = structlog.processors.NAME_TO_LEVEL[
self.config.bot.log.level.lower()
]

wrapper_class = structlog.make_filtering_bound_logger(log_level)

if not self.config.bot.log.verbose_exception:

class BoundLoggerWithoutException(wrapper_class):
exception = wrapper_class.error
aexception = wrapper_class.aerror

wrapper_class = BoundLoggerWithoutException

structlog.configure(wrapper_class=wrapper_class)

def _reload_config_dict(self) -> None:
"""重新加载配置文件。"""
Expand All @@ -412,20 +431,20 @@ def _reload_config_dict(self) -> None:
elif self._config_file.endswith(".toml"):
self._raw_config_dict = tomllib.load(f)
else:
self.error_or_exception(
"Read config file failed:",
OSError("Unable to determine config file type"),
logger.error(
"Read config file failed: "
"Unable to determine config file type"
)
except OSError as e:
self.error_or_exception("Can not open config file:", e)
except (ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError) as e:
self.error_or_exception("Read config file failed:", e)
except OSError:
logger.exception("Can not open config file:")
except (ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError):
logger.exception("Read config file failed:")

try:
self.config = MainConfig(**self._raw_config_dict)
except ValidationError as e:
except ValidationError:
self.config = MainConfig()
self.error_or_exception("Config dict parse error:", e)
logger.exception("Config dict parse error")
self._update_config()

def reload_plugins(self) -> None:
Expand Down Expand Up @@ -464,7 +483,9 @@ async def handle_event(
"""
if show_log:
logger.info(
f"Adapter {current_event.adapter.name} received: {current_event!r}"
"Event received from adapter",
adapter_name=current_event.adapter.name,
current_event=current_event,
)

if handle_get:
Expand Down Expand Up @@ -493,9 +514,7 @@ async def _handle_event(self, current_event: Optional[Event[Any]] = None) -> Non
await _hook_func(current_event)

for plugin_priority in sorted(self.plugins_priority_dict.keys()):
logger.debug(
f"Checking for matching plugins with priority {plugin_priority!r}"
)
logger.debug("Checking for matching plugins", priority=plugin_priority)
stop = False
for plugin in self.plugins_priority_dict[plugin_priority]:
try:
Expand All @@ -514,7 +533,9 @@ async def _handle_event(self, current_event: Optional[Event[Any]] = None) -> Non
if plugin_state is not None:
self.plugin_state[_plugin.name] = plugin_state
if await _plugin.rule():
logger.info(f"Event will be handled by {_plugin!r}")
logger.info(
"Event will be handled by plugin", plugin=_plugin
)
try:
await _plugin.handle()
finally:
Expand All @@ -526,8 +547,8 @@ async def _handle_event(self, current_event: Optional[Event[Any]] = None) -> Non
except StopException:
# 插件要求停止当前事件传播
stop = True
except Exception as e:
self.error_or_exception(f'Exception in plugin "{plugin}":', e)
except Exception:
logger.exception("Exception in plugin", plugin=plugin)
if stop:
break

Expand Down Expand Up @@ -649,21 +670,20 @@ def _load_plugin_class(
for _plugin in self.plugins:
if _plugin.__name__ == plugin_class.__name__:
logger.warning(
f'Already have a same name plugin "{_plugin.__name__}"'
"Already have a same name plugin", name=_plugin.__name__
)
plugin_class.__plugin_load_type__ = plugin_load_type
plugin_class.__plugin_file_path__ = plugin_file_path
self.plugins_priority_dict[priority].append(plugin_class)
logger.info(
f'Succeeded to load plugin "{plugin_class.__name__}" '
f'from class "{plugin_class!r}"'
"Succeeded to load plugin from class",
name=plugin_class.__name__,
plugin_class=plugin_class,
)
else:
self.error_or_exception(
f'Load plugin from class "{plugin_class!r}" failed:',
LoadModuleError(
f'Plugin priority incorrect in the class "{plugin_class!r}"'
),
logger.error(
"Load plugin from class failed: Plugin priority incorrect in the class",
plugin_class=plugin_class,
)

def _load_plugins_from_module_name(
Expand All @@ -678,8 +698,8 @@ def _load_plugins_from_module_name(
plugin_classes = get_classes_from_module_name(
module_name, Plugin, reload=reload
)
except ImportError as e:
self.error_or_exception(f'Import module "{module_name}" failed:', e)
except ImportError:
logger.exception("Import module failed", module_name=module_name)
else:
for plugin_class, module in plugin_classes:
self._load_plugin_class(
Expand Down Expand Up @@ -713,14 +733,14 @@ def _load_plugins(
plugin_, plugin_load_type or PluginLoadType.CLASS, None
)
elif isinstance(plugin_, str):
logger.info(f'Loading plugins from module "{plugin_}"')
logger.info("Loading plugins from module", module_name=plugin_)
self._load_plugins_from_module_name(
plugin_,
plugin_load_type=plugin_load_type or PluginLoadType.NAME,
reload=reload,
)
elif isinstance(plugin_, Path):
logger.info(f'Loading plugins from path "{plugin_}"')
logger.info("Loading plugins from path", path=plugin_)
if not plugin_.is_file():
raise LoadModuleError( # noqa: TRY301
f'The plugin path "{plugin_}" must be a file'
Expand Down Expand Up @@ -761,8 +781,8 @@ def _load_plugins(
raise TypeError( # noqa: TRY301
f"{plugin_} can not be loaded as plugin"
)
except Exception as e:
self.error_or_exception(f'Load plugin "{plugin_}" failed:', e)
except Exception:
logger.exception("Load plugin failed:", plugin=plugin_)

def load_plugins(
self, *plugins: Union[Type[Plugin[Any, Any, Any]], str, Path]
Expand All @@ -789,7 +809,7 @@ def _load_plugins_from_dirs(self, *dirs: Path) -> None:
例如:`pathlib.Path("path/of/plugins/")` 。
"""
dir_list = [str(x.resolve()) for x in dirs]
logger.info(f'Loading plugins from dirs "{", ".join(map(str, dir_list))}"')
logger.info("Loading plugins from dirs", dirs=", ".join(map(str, dir_list)))
self._module_path_finder.path.extend(dir_list)
for module_info in pkgutil.iter_modules(dir_list):
if not module_info.name.startswith("_"):
Expand Down Expand Up @@ -821,6 +841,11 @@ def _load_adapters(self, *adapters: Union[Type[Adapter[Any, Any]], str]) -> None
try:
if isinstance(adapter_, type) and issubclass(adapter_, Adapter):
adapter_object = adapter_(self)
logger.info(
"Succeeded to load adapter from class",
name=adapter_object.__class__.__name__,
adapter_class=adapter_,
)
elif isinstance(adapter_, str):
adapter_classes = get_classes_from_module_name(adapter_, Adapter)
if not adapter_classes:
Expand All @@ -832,18 +857,19 @@ def _load_adapters(self, *adapters: Union[Type[Adapter[Any, Any]], str]) -> None
f"More then one Adapter class in the {adapter_} module"
)
adapter_object = adapter_classes[0][0](self) # type: ignore
logger.info(
"Succeeded to load adapter from module",
name=adapter_object.__class__.__name__,
module_name=adapter_,
)
else:
raise TypeError( # noqa: TRY301
f"{adapter_} can not be loaded as adapter"
)
except Exception as e:
self.error_or_exception(f'Load adapter "{adapter_}" failed:', e)
except Exception:
logger.exception("Load adapter failed", adapter=adapter_)
else:
self.adapters.append(adapter_object)
logger.info(
f'Succeeded to load adapter "{adapter_object.__class__.__name__}" '
f'from "{adapter_}"'
)

def load_adapters(self, *adapters: Union[Type[Adapter[Any, Any]], str]) -> None:
"""加载适配器。
Expand Down Expand Up @@ -902,20 +928,6 @@ def get_plugin(self, name: str) -> Type[Plugin[Any, Any, Any]]:
return _plugin
raise LookupError(f'Can not find plugin named "{name}"')

def error_or_exception(
self, message: str, exception: Exception
) -> None: # pragma: no cover
"""根据当前 Bot 的配置输出 error 或者 exception 日志。
Args:
message: 消息。
exception: 异常。
"""
if self.config.bot.log.verbose_exception:
logger.exception(message)
else:
logger.error(f"{message} {exception!r}")

def bot_run_hook(self, func: BotHook) -> BotHook:
"""注册一个 Bot 启动时的函数。
Expand Down
Loading

0 comments on commit 8018936

Please sign in to comment.