Add log viewer to mbc

This commit is contained in:
Tulir Asokan 2018-12-14 00:33:33 +02:00
parent ecc1843119
commit 6f06eec3cc
5 changed files with 141 additions and 17 deletions

View File

@ -1 +1 @@
from . import upload, build, login, init from . import upload, build, login, init, logs

View File

@ -27,7 +27,7 @@ import click
from ...loader import PluginMeta from ...loader import PluginMeta
from ..cliq.validators import PathValidator from ..cliq.validators import PathValidator
from ..base import app from ..base import app
from ..config import config from ..config import get_default_server
from .upload import upload_file from .upload import upload_file
yaml = YAML() yaml = YAML()
@ -98,11 +98,8 @@ def write_plugin(meta: PluginMeta, output: Union[str, IO]) -> None:
def upload_plugin(output: Union[str, IO]) -> None: def upload_plugin(output: Union[str, IO]) -> None:
try: server, token = get_default_server()
server = config["default_server"] if not token:
token = config["servers"][server]
except KeyError:
print(Fore.RED + "Default server not configured." + Fore.RESET)
return return
if isinstance(output, str): if isinstance(output, str):
with open(output, "rb") as file: with open(output, "rb") as file:

110
maubot/cli/commands/logs.py Normal file
View File

@ -0,0 +1,110 @@
# maubot - A plugin-based Matrix bot system.
# Copyright (C) 2018 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from datetime import datetime
import asyncio
from colorama import Fore
from aiohttp import WSMsgType, WSMessage, ClientSession
from mautrix.client.api.types.util import Obj
import click
from ..config import get_token, get_default_server
from ..base import app
history_count: int = 10
@app.command(help="View the logs of a server")
@click.argument("server", required=False)
@click.option("-t", "--tail", default=10, help="Maximum number of old log lines to display")
def logs(server: str, tail: int) -> None:
if not server:
server, token = get_default_server()
else:
token = get_token(server)
if not token:
return
global history_count
history_count = tail
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(view_logs(server, token), loop=loop)
try:
loop.run_until_complete(future)
except KeyboardInterrupt:
future.cancel()
loop.run_until_complete(future)
loop.close()
def parsedate(entry: Obj) -> None:
i = entry.time.index("+")
i = entry.time.index(":", i)
entry.time = entry.time[:i] + entry.time[i + 1:]
entry.time = datetime.strptime(entry.time, "%Y-%m-%dT%H:%M:%S.%f%z")
levelcolors = {
"DEBUG": "",
"INFO": Fore.CYAN,
"WARNING": Fore.YELLOW,
"ERROR": Fore.RED,
"FATAL": Fore.MAGENTA,
}
def print_entry(entry: dict) -> None:
entry = Obj(**entry)
parsedate(entry)
print("{levelcolor}[{date}] [{level}@{logger}] {message}{resetcolor}"
.format(date=entry.time.strftime("%Y-%m-%d %H:%M:%S"),
level=entry.levelname,
levelcolor=levelcolors.get(entry.levelname, ""),
resetcolor=Fore.RESET,
logger=entry.name,
message=entry.msg))
def handle_msg(data: dict) -> bool:
if "auth_success" in data:
if data["auth_success"]:
print(Fore.GREEN + "Connected to log websocket" + Fore.RESET)
else:
print(Fore.RED + "Failed to authenticate to log websocket" + Fore.RESET)
return False
elif "history" in data:
for entry in data["history"][-history_count:]:
print_entry(entry)
else:
print_entry(data)
return True
async def view_logs(server: str, token: str) -> None:
async with ClientSession() as session:
async with session.ws_connect(f"{server}/_matrix/maubot/v1/logs") as ws:
await ws.send_str(token)
try:
msg: WSMessage
async for msg in ws:
if msg.type == WSMsgType.TEXT:
if not handle_msg(msg.json()):
break
elif msg.type == WSMsgType.ERROR:
print(Fore.YELLOW + "Connection error: " + msg.data + Fore.RESET)
elif msg.type == WSMsgType.CLOSE:
print(Fore.YELLOW + "Server closed connection" + Fore.RESET)
except asyncio.CancelledError:
pass

View File

@ -22,7 +22,7 @@ from colorama import Fore
import click import click
from ..base import app from ..base import app
from ..config import config from ..config import get_default_server, get_token
class UploadError(Exception): class UploadError(Exception):
@ -34,15 +34,10 @@ class UploadError(Exception):
@click.option("-s", "--server", help="The maubot instance to upload the plugin to") @click.option("-s", "--server", help="The maubot instance to upload the plugin to")
def upload(path: str, server: str) -> None: def upload(path: str, server: str) -> None:
if not server: if not server:
try: server, token = get_default_server()
server = config["default_server"] else:
except KeyError: token = get_token(server)
print(Fore.RED + "Default server not configured" + Fore.RESET) if not token:
return
try:
token = config["servers"][server]
except KeyError:
print(Fore.RED + "Server not found" + Fore.RESET)
return return
with open(path, "rb") as file: with open(path, "rb") as file:
upload_file(file, server, token) upload_file(file, server, token)

View File

@ -13,9 +13,12 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Tuple, Optional
import json import json
import os import os
from colorama import Fore
config = { config = {
"servers": {}, "servers": {},
"default_server": None, "default_server": None,
@ -23,6 +26,25 @@ config = {
configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config")) configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config"))
def get_default_server() -> Tuple[Optional[str], Optional[str]]:
try:
server: str = config["default_server"]
except KeyError:
server = None
if server is None:
print(f"{Fore.RED}Default server not configured.{Fore.RESET}")
return None, None
return server, get_token(server)
def get_token(server: str) -> Optional[str]:
try:
return config["servers"][server]
except KeyError:
print(f"{Fore.RED}No access token saved for {server}.{Fore.RESET}")
return None
def save_config() -> None: def save_config() -> None:
with open(f"{configdir}/maubot-cli.json", "w") as file: with open(f"{configdir}/maubot-cli.json", "w") as file:
json.dump(config, file) json.dump(config, file)