Merge pull request #149 from xPMo/bot-logs

Print recent logs with !bot logs
This commit is contained in:
xPMo 2021-05-08 19:18:49 -05:00 committed by GitHub
commit 6b9bd1620d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 69 additions and 16 deletions

View File

@ -43,6 +43,7 @@ Bot management commands.
* !bot import [module] [json object] - Update a module's settings from json (Must be done as bot owner) * !bot import [module] [json object] - Update a module's settings from json (Must be done as bot owner)
* !bot import [module] [key ...] [json object] - Update a sub-object in a module from json (Must be done as bot owner) * !bot import [module] [key ...] [json object] - Update a sub-object in a module from json (Must be done as bot owner)
* Example: !bot import alias aliases {"osm": "loc", "sh": "cmd"} * Example: !bot import alias aliases {"osm": "loc", "sh": "cmd"}
* !bot logs [module] ([count]) - Print the [count] most recent messages the given module has reported (Must be done as bot owner)
* !bot stats - show statistics on matrix users seen by bot * !bot stats - show statistics on matrix users seen by bot
* !bot leave - ask bot to leave this room (Must be done as admin in room) * !bot leave - ask bot to leave this room (Must be done as admin in room)
* !bot modules - list all modules including enabled status * !bot modules - list all modules including enabled status

34
bot.py
View File

@ -236,7 +236,16 @@ class Bot:
:return bool: Success upon sending the message :return bool: Success upon sending the message
""" """
# Sends private message to user. Returns true on success. # Sends private message to user. Returns true on success.
msg_room = await self.find_or_create_private_msg(mxid, roomname)
if not msg_room or (type(msg_room) is RoomCreateError):
self.logger.error(f'Unable to create room when trying to message {mxid}')
return False
# Send message to the room
await self.send_text(msg_room, message)
return True
async def find_or_create_private_msg(self, mxid, roomname):
# Find if we already have a common room with user: # Find if we already have a common room with user:
msg_room = None msg_room = None
for croomid in self.client.rooms: for croomid in self.client.rooms:
@ -253,15 +262,9 @@ class Bot:
is_direct=True, is_direct=True,
preset=RoomPreset.private_chat, preset=RoomPreset.private_chat,
invite={mxid}, invite={mxid},
) )
return msg_room
if not msg_room or (type(msg_room) is RoomCreateError):
self.logger.error(f'Unable to create room when trying to message {mxid}')
return False
# Send message to the room
await self.send_text(msg_room, message)
return True
def remove_callback(self, callback): def remove_callback(self, callback):
for cb_object in self.client.event_callbacks: for cb_object in self.client.event_callbacks:
@ -314,7 +317,7 @@ class Bot:
try: try:
module_settings[modulename] = moduleobject.get_settings() module_settings[modulename] = moduleobject.get_settings()
except Exception: except Exception:
traceback.print_exc(file=sys.stderr) self.logger.exception(f'unhandled exception {modulename}.get_settings')
data = {self.appid: self.version, 'module_settings': module_settings} data = {self.appid: self.version, 'module_settings': module_settings}
self.set_account_data(data) self.set_account_data(data)
@ -329,7 +332,7 @@ class Bot:
moduleobject.set_settings( moduleobject.set_settings(
data['module_settings'][modulename]) data['module_settings'][modulename])
except Exception: except Exception:
traceback.print_exc(file=sys.stderr) self.logger.exception(f'unhandled exception {modulename}.set_settings')
async def message_cb(self, room, event): async def message_cb(self, room, event):
# Ignore if asked to ignore # Ignore if asked to ignore
@ -373,7 +376,7 @@ class Bot:
await self.send_text(room, f'Sorry, only bot owner can run that command.') await self.send_text(room, f'Sorry, only bot owner can run that command.')
except Exception: except Exception:
await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details') await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
traceback.print_exc(file=sys.stderr) self.logger.exception(f'unhandled exception in !{command}')
else: else:
self.logger.error(f"Unknown command: {command}") self.logger.error(f"Unknown command: {command}")
# TODO Make this configurable # TODO Make this configurable
@ -415,8 +418,7 @@ class Bot:
cls = getattr(module, 'MatrixModule') cls = getattr(module, 'MatrixModule')
return cls(modulename) return cls(modulename)
except Exception: except Exception:
self.logger.error(f'Module {modulename} failed to load!') self.logger.exception(f'Module {modulename} failed to load')
traceback.print_exc(file=sys.stderr)
return None return None
def reload_modules(self): def reload_modules(self):
@ -446,7 +448,7 @@ class Bot:
try: try:
await moduleobject.matrix_poll(self, self.pollcount) await moduleobject.matrix_poll(self, self.pollcount)
except Exception: except Exception:
traceback.print_exc(file=sys.stderr) self.logger.exception(f'unhandled exception from {modulename}.matrix_poll')
await asyncio.sleep(10) await asyncio.sleep(10)
def set_account_data(self, data): def set_account_data(self, data):
@ -511,7 +513,7 @@ class Bot:
try: try:
moduleobject.matrix_start(self) moduleobject.matrix_start(self)
except Exception: except Exception:
traceback.print_exc(file=sys.stderr) self.logger.exception(f'unhandled exception from {modulename}.matrix_start')
self.logger.info(f'All modules started.') self.logger.info(f'All modules started.')
def stop(self): def stop(self):
@ -520,7 +522,7 @@ class Bot:
try: try:
moduleobject.matrix_stop(self) moduleobject.matrix_stop(self)
except Exception: except Exception:
traceback.print_exc(file=sys.stderr) self.logger.exception(f'unhandled exception from {modulename}.matrix_stop')
self.logger.info(f'All modules stopped.') self.logger.info(f'All modules stopped.')
async def run(self): async def run(self):

View File

@ -1,11 +1,26 @@
import collections import collections
import logging
import json import json
import requests import requests
from html import escape
from datetime import timedelta from datetime import timedelta
import time import time
from nio import RoomCreateError
from modules.common.module import BotModule, ModuleCannotBeDisabled from modules.common.module import BotModule, ModuleCannotBeDisabled
class LogDequeHandler(logging.Handler):
def __init__(self, count):
super().__init__(level = logging.NOTSET)
self.logs = dict()
self.level = logging.INFO
def emit(self, record):
try:
self.logs[str(record.module)].append(record)
except:
self.logs[str(record.module)] = collections.deque([record], maxlen=15)
class MatrixModule(BotModule): class MatrixModule(BotModule):
def __init__(self, name): def __init__(self, name):
@ -16,6 +31,9 @@ class MatrixModule(BotModule):
def matrix_start(self, bot): def matrix_start(self, bot):
super().matrix_start(bot) super().matrix_start(bot)
self.starttime = time.time() self.starttime = time.time()
self.loghandler = LogDequeHandler(10)
self.loghandler.setFormatter(logging.Formatter('%(levelname)s - %(name)s - %(message)s'))
logging.root.addHandler(self.loghandler)
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split(None, 2) args = event.body.split(None, 2)
@ -49,6 +67,8 @@ class MatrixModule(BotModule):
await self.export_settings(bot, event, module_name=args[2]) await self.export_settings(bot, event, module_name=args[2])
elif args[1] == 'import': elif args[1] == 'import':
await self.import_settings(bot, event) await self.import_settings(bot, event)
elif args[1] == 'logs':
await self.last_logs(bot, room, event, args[2])
else: else:
pass pass
@ -219,6 +239,36 @@ class MatrixModule(BotModule):
bot.save_settings() bot.save_settings()
await bot.send_msg(event.sender, f'Private message from {bot.matrix_user}', 'Updated bot settings') await bot.send_msg(event.sender, f'Private message from {bot.matrix_user}', 'Updated bot settings')
async def last_logs(self, bot, room, event, target):
bot.must_be_owner(event)
self.logger.info(f'{event.sender} asked for recent log messages.')
msg_room = await bot.find_or_create_private_msg(event.sender, f'Private message from {bot.matrix_user}')
if not msg_room or (type(msg_room) is RoomCreateError):
# fallback to current room if we can't create one
msg_room = room
try:
target, count = target.split()
count = -abs(int(count))
except ValueError:
count = 0
keys = list(self.loghandler.logs)
for key in [target, f'module {target}']:
try:
logs = list(self.loghandler.logs[key])
break
except (KeyError, TypeError):
pass
else:
return await bot.send_text(msg_room, f'Unknown module {target}, or no logs yet')
if count:
logs = logs[count:]
logs = '\n'.join([self.loghandler.format(record) for record in logs])
return await bot.send_html(msg_room, f'<strong>Logs for {key}:</strong>\n<pre><code class="language-txt">{escape(logs)}</code></pre>', f'Logs for {key}:\n' + logs)
def disable(self): def disable(self):
raise ModuleCannotBeDisabled raise ModuleCannotBeDisabled