From 01909822913852cad21c2b44800a22ef51fb909e Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 8 Oct 2023 09:05:20 +0300 Subject: [PATCH] (meta): Improve logging. --- src/core/lion_member.py | 14 ++++++++-- src/modules/rolemenus/cog.py | 53 ++++++++++++++++++++++++++++++++++-- src/modules/schedule/cog.py | 2 +- src/tracking/text/cog.py | 47 ++++++++++++++++++++++++++++++-- src/utils/monitor.py | 15 +++++++++- 5 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/core/lion_member.py b/src/core/lion_member.py index fb944e76..4a7a5632 100644 --- a/src/core/lion_member.py +++ b/src/core/lion_member.py @@ -2,6 +2,7 @@ from typing import Optional import datetime as dt import pytz import discord +import logging from meta import LionBot from utils.lib import Timezoned @@ -13,6 +14,9 @@ from .lion_user import LionUser from .lion_guild import LionGuild +logger = logging.getLogger(__name__) + + class MemberConfig(ModelConfig): settings = SettingDotDict() _model_settings = set() @@ -103,12 +107,16 @@ class LionMember(Timezoned): async def remove_role(self, role: discord.Role): member = await self.fetch_member() - if member is not None and role in member.roles: + if member is not None: try: await member.remove_roles(role) - except discord.HTTPException: + except discord.HTTPException as e: # TODO: Logging, audit logging - pass + logger.warning( + "Lion role removal failed for " + f", , . " + f"Error: {repr(e)}", + ) else: # Remove the role from persistent role storage cog = self.bot.get_cog('MemberAdminCog') diff --git a/src/modules/rolemenus/cog.py b/src/modules/rolemenus/cog.py index 9efef99a..3bcc6811 100644 --- a/src/modules/rolemenus/cog.py +++ b/src/modules/rolemenus/cog.py @@ -14,6 +14,7 @@ from meta import LionCog, LionBot, LionContext from meta.logger import log_wrap from meta.errors import ResponseTimedOut, UserInputError, UserCancelled, SafeCancellation from meta.sharding import THIS_SHARD +from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel from utils.lib import utc_now, error_embed from utils.ui import Confirm, ChoicedEnum, Transformed, AButton, AsComponents from utils.transformers import DurationTransformer @@ -142,6 +143,9 @@ class RoleMenuCog(LionCog): def __init__(self, bot: LionBot): self.bot = bot self.data = bot.db.load_registry(RoleMenuData()) + self.monitor = ComponentMonitor('RoleMenus', self._monitor) + + self.ready = asyncio.Event() # Menu caches self.live_menus = RoleMenu.attached_menus # guildid -> messageid -> menuid @@ -149,11 +153,42 @@ class RoleMenuCog(LionCog): # Expiry manage self.expiry_monitor = ExpiryMonitor(executor=self._expire) + async def _monitor(self): + state = ( + "<" + "RoleMenus" + " ready={ready}" + " cached={cached}" + " views={views}" + " live={live}" + " expiry={expiry}" + ">" + ) + data = dict( + ready=self.ready.is_set(), + live=sum(len(gmenus) for gmenus in self.live_menus.values()), + expiry=repr(self.expiry_monitor), + cached=len(RoleMenu._menus), + views=len(RoleMenu.menu_views), + ) + if not self.ready.is_set(): + level = StatusLevel.STARTING + info = f"(STARTING) Not initialised. {state}" + elif not self.expiry_monitor._monitor_task: + level = StatusLevel.ERRORED + info = f"(ERRORED) Expiry monitor not running. {state}" + else: + level = StatusLevel.OKAY + info = f"(OK) RoleMenu loaded and listening. {state}" + + return ComponentStatus(level, info, info, data) + # ----- Initialisation ----- async def cog_load(self): + self.bot.system_monitor.add_component(self.monitor) await self.data.init() - self.bot.tree.add_command(rolemenu_ctxcmd) + self.bot.tree.add_command(rolemenu_ctxcmd, override=True) if self.bot.is_ready(): await self.initialise() @@ -164,17 +199,28 @@ class RoleMenuCog(LionCog): self.live_menus.clear() if self.expiry_monitor._monitor_task: self.expiry_monitor._monitor_task.cancel() - self.bot.tree.remove_command(rolemenu_ctxcmd) @LionCog.listener('on_ready') @log_wrap(action="Initialise Role Menus") async def initialise(self): + self.ready.clear() + + # Clean up live menu tasks + for menu in list(RoleMenu._menus.values()): + menu.detach() + self.live_menus.clear() + if self.expiry_monitor._monitor_task: + self.expiry_monitor._monitor_task.cancel() + + # Start monitor self.expiry_monitor = ExpiryMonitor(executor=self._expire) self.expiry_monitor.start() + # Load guilds guildids = [guild.id for guild in self.bot.guilds] if guildids: await self._initialise_guilds(*guildids) + self.ready.set() async def _initialise_guilds(self, *guildids): """ @@ -262,7 +308,7 @@ class RoleMenuCog(LionCog): If the bot is no longer in the server, ignores the expiry. If the member is no longer in the server, removes the role from persisted roles, if applicable. """ - logger.debug(f"Expiring RoleMenu equipped role {equipid}") + logger.info(f"Expiring RoleMenu equipped role {equipid}") rows = await self.data.RoleMenuHistory.fetch_expiring_where(equipid=equipid) if rows: equip_row = rows[0] @@ -277,6 +323,7 @@ class RoleMenuCog(LionCog): await equip_row.update(removed_at=now) else: # equipid is no longer valid or is not expiring + logger.info(f"RoleMenu equipped role {equipid} is no longer valid or is not expiring.") pass # ----- Private Utils ----- diff --git a/src/modules/schedule/cog.py b/src/modules/schedule/cog.py index c655a991..bcdaaff5 100644 --- a/src/modules/schedule/cog.py +++ b/src/modules/schedule/cog.py @@ -982,7 +982,7 @@ class ScheduleCog(LionCog): value=partial ) ) - return choices + return choices[:25] @schedule_cmd.autocomplete('cancel') async def schedule_cmd_cancel_acmpl(self, interaction: discord.Interaction, partial: str): diff --git a/src/tracking/text/cog.py b/src/tracking/text/cog.py index acfb6fe3..d517a118 100644 --- a/src/tracking/text/cog.py +++ b/src/tracking/text/cog.py @@ -13,6 +13,7 @@ from meta.errors import UserInputError from meta.logger import log_wrap, logging_context from meta.sharding import THIS_SHARD from meta.app import appname +from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel from utils.lib import utc_now, error_embed from wards import low_management_ward, sys_admin_ward @@ -42,10 +43,14 @@ class TextTrackerCog(LionCog): self.data = bot.db.load_registry(TextTrackerData()) self.settings = TextTrackerSettings() self.global_settings = TextTrackerGlobalSettings() + self.monitor = ComponentMonitor('TextTracker', self._monitor) self.babel = babel self.sessionq = asyncio.Queue(maxsize=0) + self.ready = asyncio.Event() + self.errors = 0 + # Map of ongoing text sessions # guildid -> (userid -> TextSession) self.ongoing = defaultdict(dict) @@ -54,7 +59,41 @@ class TextTrackerCog(LionCog): self.untracked_channels = self.settings.UntrackedTextChannels._cache + async def _monitor(self): + state = ( + "<" + "TextTracker" + " ready={ready}" + " queued={queued}" + " errors={errors}" + " running={running}" + " consumer={consumer}" + ">" + ) + data = dict( + ready=self.ready.is_set(), + queued=self.sessionq.qsize(), + errors=self.errors, + running=sum(len(usessions) for usessions in self.ongoing.values()), + consumer="'Running'" if (self._consumer_task and not self._consumer_task.done()) else "'Not Running'", + ) + if not self.ready.is_set(): + level = StatusLevel.STARTING + info = f"(STARTING) Not initialised. {state}" + elif not self._consumer_task: + level = StatusLevel.ERRORED + info = f"(ERROR) Consumer task not running. {state}" + elif self.errors > 1: + level = StatusLevel.UNSURE + info = f"(UNSURE) Errors occurred while consuming. {state}" + else: + level = StatusLevel.OKAY + info = f"(OK) Message tracking operational. {state}" + + return ComponentStatus(level, info, info, data) + async def cog_load(self): + self.bot.system_monitor.add_component(self.monitor) await self.data.init() self.bot.core.guild_config.register_model_setting(self.settings.XPPerPeriod) @@ -83,6 +122,7 @@ class TextTrackerCog(LionCog): await self.initialise() async def cog_unload(self): + self.ready.clear() if self._consumer_task is not None: self._consumer_task.cancel() @@ -104,7 +144,7 @@ class TextTrackerCog(LionCog): await self.bot.core.lions.fetch_member(session.guildid, session.userid) self.sessionq.put_nowait(session) - @log_wrap(stack=['Text Sessions', 'Message Event']) + @log_wrap(stack=['Text Sessions', 'Consumer']) async def _session_consumer(self): """ Process completed sessions in batches of length `batchsize`. @@ -132,6 +172,7 @@ class TextTrackerCog(LionCog): logger.exception( "Unknown exception processing batch of text sessions! Discarding and continuing." ) + self.errors += 1 batch = [] counter = 0 last_time = time.monotonic() @@ -202,9 +243,11 @@ class TextTrackerCog(LionCog): """ Launch the session consumer. """ + self.ready.clear() if self._consumer_task and not self._consumer_task.cancelled(): self._consumer_task.cancel() - self._consumer_task = asyncio.create_task(self._session_consumer()) + self._consumer_task = asyncio.create_task(self._session_consumer(), name='text-session-consumer') + self.ready.set() logger.info("Launched text session consumer.") @LionCog.listener('on_message') diff --git a/src/utils/monitor.py b/src/utils/monitor.py index 79ed8209..96aedeb7 100644 --- a/src/utils/monitor.py +++ b/src/utils/monitor.py @@ -32,7 +32,7 @@ class TaskMonitor(Generic[Taskid]): self.executor: Optional[Callable[[Taskid], Coroutine[Any, Any, None]]] = executor self._wakeup: asyncio.Event = asyncio.Event() - self._monitor_task: Optional[self.Task] = None + self._monitor_task: Optional[asyncio.Task] = None # Task data self._tasklist: list[Taskid] = [] @@ -42,6 +42,19 @@ class TaskMonitor(Generic[Taskid]): # And allows simpler external cancellation if required self._running: dict[Taskid, asyncio.Future] = {} + def __repr__(self): + return ( + "<" + f"{self.__class__.__name__}" + f" tasklist={len(self._tasklist)}" + f" taskmap={len(self._taskmap)}" + f" wakeup={self._wakeup.is_set()}" + f" bucket={self._bucket}" + f" running={len(self._running)}" + f" task={self._monitor_task}" + f">" + ) + def set_tasks(self, *tasks: tuple[Taskid, int]) -> None: """ Similar to `schedule_tasks`, but wipe and reset the tasklist.