From 5b809b1b9962248b9e10212ab85a827764c76d3f Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Thu, 17 Dec 2020 23:56:53 +0000 Subject: [PATCH] Add /status endpoint for monitoring --- irclog.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/irclog.py b/irclog.py index 74633a7..dda402a 100644 --- a/irclog.py +++ b/irclog.py @@ -188,8 +188,8 @@ class Config(dict): raise InvalidConfig(f'Invalid channel {key!r} path: not a string') if any(x in channel['path'] for x in itertools.chain(map(chr, range(32)), ('/', '\\', '"', '\x7F'))): raise InvalidConfig(f'Invalid channel {key!r} path: contains invalid characters') - if channel['path'] == 'general': - raise InvalidConfig(f'Invalid channel {key!r} path: cannot be "general"') + if channel['path'] in ('general', 'status'): + raise InvalidConfig(f'Invalid channel {key!r} path: cannot be "general" or "status"') if channel['path'] in seenPaths: raise InvalidConfig(f'Invalid channel {key!r} path: collides with channel {seenPaths[channel["path"]]!r}') seenPaths[channel['path']] = key @@ -274,6 +274,7 @@ class IRCClientProtocol(asyncio.Protocol): self.connectionClosedEvent = connectionClosedEvent self.loop = loop self.config = config + self.lastRecvTime = None self.lastSentTime = None # float timestamp or None; the latter disables the send rate limit self.sendQueue = asyncio.Queue() self.buffer = b'' @@ -410,6 +411,7 @@ class IRCClientProtocol(asyncio.Protocol): def data_received(self, data): time_ = time.time() self.logger.debug(f'Data received: {data!r}') + self.lastRecvTime = time_ # If there's any data left in the buffer, prepend it to the data. Split on CRLF. # Then, process all messages except the last one (since data might not end on a CRLF) and keep the remainder in the buffer. # If data does end with CRLF, all messages will have been processed and the buffer will be empty again. @@ -699,6 +701,8 @@ class IRCClient: if not connectionClosedEvent.is_set(): self.logger.debug('Quitting connection') await self._protocol.quit() + self._transport = None + self._protocol = None except (ConnectionRefusedError, ssl.SSLError, asyncio.TimeoutError, asyncio.CancelledError) as e: self.logger.error(f'{type(e).__module__}.{type(e).__name__}: {e!s}') await asyncio.wait({asyncio.create_task(sigintEvent.wait())}, timeout = 5) @@ -707,6 +711,10 @@ class IRCClient: self.messageQueue.put_nowait(messageEOF) break + @property + def lastRecvTime(self): + return self._protocol.lastRecvTime if self._protocol else None + class Storage: logger = logging.getLogger('irclog.Storage') @@ -829,7 +837,8 @@ class WebServer: '.linkbar a:last-of-type { border-right: none; }', ]) + '' - def __init__(self, config): + def __init__(self, ircClient, config): + self.ircClient = ircClient self.config = config self._paths = {} # '/path' => ('#channel', auth, hidden, extrasearchpaths, description) where auth is either False (no authentication) or the HTTP header value for basic auth @@ -837,6 +846,7 @@ class WebServer: self._app = aiohttp.web.Application() self._app.add_routes([ aiohttp.web.get('/', self.get_homepage), + aiohttp.web.get('/status', self.get_status), aiohttp.web.get(r'/{path:[^/]+}', functools.partial(self._channel_handler, handler = self.get_channel_info)), aiohttp.web.get(r'/{path:[^/]+}/{date:\d{4}-\d{2}-\d{2}}', functools.partial(self._channel_handler, handler = self.get_log)), aiohttp.web.get(r'/{path:[^/]+}/today', functools.partial(self._channel_handler, handler = self.log_redirect_today)), @@ -898,6 +908,10 @@ class WebServer: lines.append(f'{"(PW) " if auth else ""}{html.escape(channel)} (today's log, search)') return aiohttp.web.Response(text = f'IRC logs{"
".join(lines)}', content_type = 'text/html') + async def get_status(self, request): + self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r}') + return (aiohttp.web.Response if (self.ircClient.lastRecvTime or 0) > time.time() - 600 else aiohttp.web.HTTPInternalServerError)() + async def get_channel_info(self, request): self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r}') description = html.escape(self._paths[request.match_info["path"]][4]) if self._paths[request.match_info["path"]][4] else '(not available)' @@ -1177,7 +1191,7 @@ async def main(): # The queue can also contain messageEOF, which signals to the storage layer to stop logging. irc = IRCClient(messageQueue, config) - webserver = WebServer(config) + webserver = WebServer(irc, config) storage = Storage(messageQueue, config) sigintEvent = asyncio.Event()