(meta): Improve logging.

This commit is contained in:
2023-10-08 09:05:20 +03:00
parent 7e82acd9f8
commit 0190982291
5 changed files with 121 additions and 10 deletions

View File

@@ -2,6 +2,7 @@ from typing import Optional
import datetime as dt import datetime as dt
import pytz import pytz
import discord import discord
import logging
from meta import LionBot from meta import LionBot
from utils.lib import Timezoned from utils.lib import Timezoned
@@ -13,6 +14,9 @@ from .lion_user import LionUser
from .lion_guild import LionGuild from .lion_guild import LionGuild
logger = logging.getLogger(__name__)
class MemberConfig(ModelConfig): class MemberConfig(ModelConfig):
settings = SettingDotDict() settings = SettingDotDict()
_model_settings = set() _model_settings = set()
@@ -103,12 +107,16 @@ class LionMember(Timezoned):
async def remove_role(self, role: discord.Role): async def remove_role(self, role: discord.Role):
member = await self.fetch_member() member = await self.fetch_member()
if member is not None and role in member.roles: if member is not None:
try: try:
await member.remove_roles(role) await member.remove_roles(role)
except discord.HTTPException: except discord.HTTPException as e:
# TODO: Logging, audit logging # TODO: Logging, audit logging
pass logger.warning(
"Lion role removal failed for "
f"<uid: {member.id}>, <gid: {member.guild.id}>, <rid: {role.id}>. "
f"Error: {repr(e)}",
)
else: else:
# Remove the role from persistent role storage # Remove the role from persistent role storage
cog = self.bot.get_cog('MemberAdminCog') cog = self.bot.get_cog('MemberAdminCog')

View File

@@ -14,6 +14,7 @@ from meta import LionCog, LionBot, LionContext
from meta.logger import log_wrap from meta.logger import log_wrap
from meta.errors import ResponseTimedOut, UserInputError, UserCancelled, SafeCancellation from meta.errors import ResponseTimedOut, UserInputError, UserCancelled, SafeCancellation
from meta.sharding import THIS_SHARD from meta.sharding import THIS_SHARD
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
from utils.lib import utc_now, error_embed from utils.lib import utc_now, error_embed
from utils.ui import Confirm, ChoicedEnum, Transformed, AButton, AsComponents from utils.ui import Confirm, ChoicedEnum, Transformed, AButton, AsComponents
from utils.transformers import DurationTransformer from utils.transformers import DurationTransformer
@@ -142,6 +143,9 @@ class RoleMenuCog(LionCog):
def __init__(self, bot: LionBot): def __init__(self, bot: LionBot):
self.bot = bot self.bot = bot
self.data = bot.db.load_registry(RoleMenuData()) self.data = bot.db.load_registry(RoleMenuData())
self.monitor = ComponentMonitor('RoleMenus', self._monitor)
self.ready = asyncio.Event()
# Menu caches # Menu caches
self.live_menus = RoleMenu.attached_menus # guildid -> messageid -> menuid self.live_menus = RoleMenu.attached_menus # guildid -> messageid -> menuid
@@ -149,11 +153,42 @@ class RoleMenuCog(LionCog):
# Expiry manage # Expiry manage
self.expiry_monitor = ExpiryMonitor(executor=self._expire) 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 ----- # ----- Initialisation -----
async def cog_load(self): async def cog_load(self):
self.bot.system_monitor.add_component(self.monitor)
await self.data.init() 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(): if self.bot.is_ready():
await self.initialise() await self.initialise()
@@ -164,17 +199,28 @@ class RoleMenuCog(LionCog):
self.live_menus.clear() self.live_menus.clear()
if self.expiry_monitor._monitor_task: if self.expiry_monitor._monitor_task:
self.expiry_monitor._monitor_task.cancel() self.expiry_monitor._monitor_task.cancel()
self.bot.tree.remove_command(rolemenu_ctxcmd)
@LionCog.listener('on_ready') @LionCog.listener('on_ready')
@log_wrap(action="Initialise Role Menus") @log_wrap(action="Initialise Role Menus")
async def initialise(self): 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 = ExpiryMonitor(executor=self._expire)
self.expiry_monitor.start() self.expiry_monitor.start()
# Load guilds
guildids = [guild.id for guild in self.bot.guilds] guildids = [guild.id for guild in self.bot.guilds]
if guildids: if guildids:
await self._initialise_guilds(*guildids) await self._initialise_guilds(*guildids)
self.ready.set()
async def _initialise_guilds(self, *guildids): 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 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. 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) rows = await self.data.RoleMenuHistory.fetch_expiring_where(equipid=equipid)
if rows: if rows:
equip_row = rows[0] equip_row = rows[0]
@@ -277,6 +323,7 @@ class RoleMenuCog(LionCog):
await equip_row.update(removed_at=now) await equip_row.update(removed_at=now)
else: else:
# equipid is no longer valid or is not expiring # 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 pass
# ----- Private Utils ----- # ----- Private Utils -----

View File

@@ -982,7 +982,7 @@ class ScheduleCog(LionCog):
value=partial value=partial
) )
) )
return choices return choices[:25]
@schedule_cmd.autocomplete('cancel') @schedule_cmd.autocomplete('cancel')
async def schedule_cmd_cancel_acmpl(self, interaction: discord.Interaction, partial: str): async def schedule_cmd_cancel_acmpl(self, interaction: discord.Interaction, partial: str):

View File

@@ -13,6 +13,7 @@ from meta.errors import UserInputError
from meta.logger import log_wrap, logging_context from meta.logger import log_wrap, logging_context
from meta.sharding import THIS_SHARD from meta.sharding import THIS_SHARD
from meta.app import appname from meta.app import appname
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
from utils.lib import utc_now, error_embed from utils.lib import utc_now, error_embed
from wards import low_management_ward, sys_admin_ward from wards import low_management_ward, sys_admin_ward
@@ -42,10 +43,14 @@ class TextTrackerCog(LionCog):
self.data = bot.db.load_registry(TextTrackerData()) self.data = bot.db.load_registry(TextTrackerData())
self.settings = TextTrackerSettings() self.settings = TextTrackerSettings()
self.global_settings = TextTrackerGlobalSettings() self.global_settings = TextTrackerGlobalSettings()
self.monitor = ComponentMonitor('TextTracker', self._monitor)
self.babel = babel self.babel = babel
self.sessionq = asyncio.Queue(maxsize=0) self.sessionq = asyncio.Queue(maxsize=0)
self.ready = asyncio.Event()
self.errors = 0
# Map of ongoing text sessions # Map of ongoing text sessions
# guildid -> (userid -> TextSession) # guildid -> (userid -> TextSession)
self.ongoing = defaultdict(dict) self.ongoing = defaultdict(dict)
@@ -54,7 +59,41 @@ class TextTrackerCog(LionCog):
self.untracked_channels = self.settings.UntrackedTextChannels._cache 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): async def cog_load(self):
self.bot.system_monitor.add_component(self.monitor)
await self.data.init() await self.data.init()
self.bot.core.guild_config.register_model_setting(self.settings.XPPerPeriod) self.bot.core.guild_config.register_model_setting(self.settings.XPPerPeriod)
@@ -83,6 +122,7 @@ class TextTrackerCog(LionCog):
await self.initialise() await self.initialise()
async def cog_unload(self): async def cog_unload(self):
self.ready.clear()
if self._consumer_task is not None: if self._consumer_task is not None:
self._consumer_task.cancel() self._consumer_task.cancel()
@@ -104,7 +144,7 @@ class TextTrackerCog(LionCog):
await self.bot.core.lions.fetch_member(session.guildid, session.userid) await self.bot.core.lions.fetch_member(session.guildid, session.userid)
self.sessionq.put_nowait(session) self.sessionq.put_nowait(session)
@log_wrap(stack=['Text Sessions', 'Message Event']) @log_wrap(stack=['Text Sessions', 'Consumer'])
async def _session_consumer(self): async def _session_consumer(self):
""" """
Process completed sessions in batches of length `batchsize`. Process completed sessions in batches of length `batchsize`.
@@ -132,6 +172,7 @@ class TextTrackerCog(LionCog):
logger.exception( logger.exception(
"Unknown exception processing batch of text sessions! Discarding and continuing." "Unknown exception processing batch of text sessions! Discarding and continuing."
) )
self.errors += 1
batch = [] batch = []
counter = 0 counter = 0
last_time = time.monotonic() last_time = time.monotonic()
@@ -202,9 +243,11 @@ class TextTrackerCog(LionCog):
""" """
Launch the session consumer. Launch the session consumer.
""" """
self.ready.clear()
if self._consumer_task and not self._consumer_task.cancelled(): if self._consumer_task and not self._consumer_task.cancelled():
self._consumer_task.cancel() 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.") logger.info("Launched text session consumer.")
@LionCog.listener('on_message') @LionCog.listener('on_message')

View File

@@ -32,7 +32,7 @@ class TaskMonitor(Generic[Taskid]):
self.executor: Optional[Callable[[Taskid], Coroutine[Any, Any, None]]] = executor self.executor: Optional[Callable[[Taskid], Coroutine[Any, Any, None]]] = executor
self._wakeup: asyncio.Event = asyncio.Event() self._wakeup: asyncio.Event = asyncio.Event()
self._monitor_task: Optional[self.Task] = None self._monitor_task: Optional[asyncio.Task] = None
# Task data # Task data
self._tasklist: list[Taskid] = [] self._tasklist: list[Taskid] = []
@@ -42,6 +42,19 @@ class TaskMonitor(Generic[Taskid]):
# And allows simpler external cancellation if required # And allows simpler external cancellation if required
self._running: dict[Taskid, asyncio.Future] = {} 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: def set_tasks(self, *tasks: tuple[Taskid, int]) -> None:
""" """
Similar to `schedule_tasks`, but wipe and reset the tasklist. Similar to `schedule_tasks`, but wipe and reset the tasklist.