Finish basic log viewer

This commit is contained in:
Tulir Asokan 2018-11-12 00:00:37 +02:00
parent 6f2162d5f3
commit c2a5451401
9 changed files with 124 additions and 17 deletions

View File

@ -38,6 +38,12 @@ class WebSocketHandler(logging.Handler):
self.formatter = logging.Formatter() self.formatter = logging.Formatter()
def emit(self, record: logging.LogRecord) -> None: def emit(self, record: logging.LogRecord) -> None:
try:
self._emit(record)
except Exception as e:
print("Logging error:", e)
def _emit(self, record: logging.LogRecord) -> None:
# JSON conversion based on Marsel Mavletkulov's json-log-formatter (MIT license) # JSON conversion based on Marsel Mavletkulov's json-log-formatter (MIT license)
# https://github.com/marselester/json-log-formatter # https://github.com/marselester/json-log-formatter
content = { content = {
@ -45,6 +51,7 @@ class WebSocketHandler(logging.Handler):
for name, value in record.__dict__.items() for name, value in record.__dict__.items()
if name not in EXCLUDE_ATTRS if name not in EXCLUDE_ATTRS
} }
content["id"] = record.relativeCreated
content["msg"] = record.getMessage() content["msg"] = record.getMessage()
content["time"] = datetime.utcnow() content["time"] = datetime.utcnow()
@ -61,7 +68,7 @@ class WebSocketHandler(logging.Handler):
try: try:
await self.ws.send_json(record) await self.ws.send_json(record)
except Exception as e: except Exception as e:
pass print("Log sending error:", e)
log_root = logging.getLogger("maubot") log_root = logging.getLogger("maubot")

View File

@ -82,6 +82,7 @@ export async function openLogSocket() {
socket: null, socket: null,
connected: false, connected: false,
authenticated: false, authenticated: false,
onLog: data => {},
fails: -1, fails: -1,
} }
const openHandler = () => { const openHandler = () => {
@ -100,7 +101,9 @@ export async function openLogSocket() {
console.info("Websocket connection authentication failed") console.info("Websocket connection authentication failed")
} }
} else { } else {
data.time = new Date(data.time)
console.log("SERVLOG", data) console.log("SERVLOG", data)
wrapper.onLog(data)
} }
} }
const closeHandler = evt => { const closeHandler = evt => {

View File

@ -21,6 +21,7 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab
import Spinner from "../../components/Spinner" import Spinner from "../../components/Spinner"
import api from "../../api" import api from "../../api"
import BaseMainView from "./BaseMainView" import BaseMainView from "./BaseMainView"
import Log from "./Log"
const ClientListEntry = ({ entry }) => { const ClientListEntry = ({ entry }) => {
const classes = ["client", "entry"] const classes = ["client", "entry"]
@ -200,7 +201,8 @@ class Client extends BaseMainView {
</> </>
render() { render() {
return <div className="client"> return <>
<div className="client">
{this.renderSidebar()} {this.renderSidebar()}
<div className="info"> <div className="info">
{this.renderPreferences()} {this.renderPreferences()}
@ -208,6 +210,8 @@ class Client extends BaseMainView {
{this.renderInstances()} {this.renderInstances()}
</div> </div>
</div> </div>
<Log showName={false} lines={this.props.log}/>
</>
} }
} }

View File

@ -0,0 +1,30 @@
// 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/>.
import React, { Component } from "react"
import Log from "./Log"
class Home extends Component {
render() {
return <>
<div className="home">
See sidebar to get started
</div>
<Log lines={this.props.log}/>
</>
}
}
export default Home

View File

@ -23,6 +23,7 @@ import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/P
import api from "../../api" import api from "../../api"
import Spinner from "../../components/Spinner" import Spinner from "../../components/Spinner"
import BaseMainView from "./BaseMainView" import BaseMainView from "./BaseMainView"
import Log from "./Log"
const InstanceListEntry = ({ entry }) => ( const InstanceListEntry = ({ entry }) => (
<NavLink className="instance entry" to={`/instance/${entry.id}`}> <NavLink className="instance entry" to={`/instance/${entry.id}`}>
@ -167,6 +168,7 @@ class Instance extends BaseMainView {
</button> </button>
</div> </div>
<div className="error">{this.state.error}</div> <div className="error">{this.state.error}</div>
<Log showName={false} lines={this.props.log}/>
</div> </div>
} }
} }

View File

@ -0,0 +1,29 @@
// 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/>.
import React from "react"
const Log = ({ lines, showName = true }) => <div className="log">
{lines.map(data =>
<div className="row" key={data.id}>
<span className="time">{data.time.toLocaleTimeString()}</span>
<span className="level">{data.levelname}</span>
{showName && <span className="logger">{data.name}</span>}
<span className="text">{data.msg}</span>
</div>,
)}
</div>
export default Log

View File

@ -21,6 +21,7 @@ import PrefTable, { PrefInput } from "../../components/PreferenceTable"
import Spinner from "../../components/Spinner" import Spinner from "../../components/Spinner"
import api from "../../api" import api from "../../api"
import BaseMainView from "./BaseMainView" import BaseMainView from "./BaseMainView"
import Log from "./Log"
const PluginListEntry = ({ entry }) => ( const PluginListEntry = ({ entry }) => (
<NavLink className="plugin entry" to={`/plugin/${entry.id}`}> <NavLink className="plugin entry" to={`/plugin/${entry.id}`}>
@ -90,6 +91,7 @@ class Plugin extends BaseMainView {
</div>} </div>}
<div className="error">{this.state.error}</div> <div className="error">{this.state.error}</div>
{!this.isNew && this.renderInstances()} {!this.isNew && this.renderInstances()}
<Log showName={false} lines={this.props.log}/>
</div> </div>
} }
} }

View File

@ -20,6 +20,7 @@ import { ReactComponent as Plus } from "../../res/plus.svg"
import Instance from "./Instance" import Instance from "./Instance"
import Client from "./Client" import Client from "./Client"
import Plugin from "./Plugin" import Plugin from "./Plugin"
import Home from "./Home"
class Dashboard extends Component { class Dashboard extends Component {
constructor(props) { constructor(props) {
@ -30,6 +31,8 @@ class Dashboard extends Component {
plugins: {}, plugins: {},
sidebarOpen: false, sidebarOpen: false,
} }
this.logLines = []
this.logMap = {}
window.maubot = this window.maubot = this
} }
@ -55,9 +58,13 @@ class Dashboard extends Component {
plugins[plugin.id] = plugin plugins[plugin.id] = plugin
} }
this.setState({ instances, clients, plugins }) this.setState({ instances, clients, plugins })
const logs = await api.openLogSocket() const logs = await api.openLogSocket()
console.log("WebSocket opened:", logs) logs.onLog = data => {
window.logs = logs this.logLines.push(data)
;(this.logMap[data.name] || (this.logMap[data.name] = [])).push(data)
this.setState({})
}
} }
renderList(field, type) { renderList(field, type) {
@ -85,11 +92,13 @@ class Dashboard extends Component {
if (!entry) { if (!entry) {
return this.renderNotFound(field.slice(0, -1)) return this.renderNotFound(field.slice(0, -1))
} }
console.log(`maubot.${field.slice(0, -1)}.${id}`)
return React.createElement(type, { return React.createElement(type, {
entry, entry,
onDelete: () => this.delete(field, id), onDelete: () => this.delete(field, id),
onChange: newEntry => this.add(field, newEntry, id), onChange: newEntry => this.add(field, newEntry, id),
ctx: this.state, ctx: this.state,
log: this.logMap[`maubot.${field.slice(0, -1)}.${id}`] || [],
}) })
} }
@ -142,7 +151,7 @@ class Dashboard extends Component {
<main className="view"> <main className="view">
<Switch> <Switch>
<Route path="/" exact render={() => "Hello, World!"}/> <Route path="/" exact render={() => <Home log={this.logLines}/>}/>
<Route path="/new/instance" render={() => <Route path="/new/instance" render={() =>
<Instance onChange={newEntry => this.add("instances", newEntry)} <Instance onChange={newEntry => this.add("instances", newEntry)}
ctx={this.state}/>}/> ctx={this.state}/>}/>

View File

@ -78,17 +78,38 @@
@import instance @import instance
@import plugin @import plugin
> .not-found > div
text-align: center
margin-top: 5rem
font-size: 1.5rem
> div:not(.not-found)
margin: 2rem 4rem margin: 2rem 4rem
@media screen and (max-width: 50rem) @media screen and (max-width: 50rem)
margin: 2rem 1rem margin: 2rem 1rem
div.log > div.row
span.level, span.logger
display: none
> div.not-found, > div.home
text-align: center
margin-top: 5rem
font-size: 1.5rem
div.log
text-align: left
font-size: 12px
max-height: 20rem
font-family: "Fira Code", monospace
overflow: auto
> div.row
white-space: nowrap
> span.level:before
content: " ["
> span.logger:before
content: "@"
> span.text:before
content: "] "
div.buttons div.buttons
+button-group +button-group
display: flex display: flex