mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-02 06:33:38 +01:00
Add a log viewer to Spinner.
This is more of a proof-of-concept/demo for using a websocket with Spinner. Gives an example of how we could push live updates to the webapp. Also, the logger is actually nice. Guaranteed to never get cluttered with system logs. Looks basically identical to our other log viewers. Filtering is basic but fast. And we could build much better tooling on top of this.
This commit is contained in:
committed by
Cody Henthorne
parent
c314918c6b
commit
6a974c48ef
@@ -5,7 +5,7 @@ html, body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select, input {
|
||||
select, input, button {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-variant-ligatures: none;
|
||||
font-size: 1rem;
|
||||
|
||||
290
spinner/lib/src/main/assets/logs.hbs
Normal file
290
spinner/lib/src/main/assets/logs.hbs
Normal file
@@ -0,0 +1,290 @@
|
||||
<html>
|
||||
{{> partials/head title="Home" }}
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet">
|
||||
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1.collapse-header {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
h2.collapse-header {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
#log-container {
|
||||
width: calc(100% - 32px);
|
||||
height: calc(100% - 325px);
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto;
|
||||
border: 1px solid black;
|
||||
resize: vertical;
|
||||
white-space: pre;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background-color: #2b2b2b;
|
||||
margin-top: 8px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid black;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#logs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.log-verbose {
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
.log-debug {
|
||||
color: #5ca72b;
|
||||
}
|
||||
|
||||
.log-info{
|
||||
color: #46bbb9;
|
||||
}
|
||||
|
||||
.log-warn{
|
||||
color: #d6cb37;
|
||||
}
|
||||
|
||||
.log-error{
|
||||
color: #ff6b68;
|
||||
}
|
||||
|
||||
#follow-button.enabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
width: calc(100% - 14px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#toolbar #filter-text {
|
||||
flex-grow: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#socket-status {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 16px;
|
||||
margin: 3px 0 3px 3px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#socket-status.connected {
|
||||
background-color: #5ca72b;
|
||||
}
|
||||
|
||||
#socket-status.connecting {
|
||||
background-color: #d6cb37;
|
||||
}
|
||||
|
||||
#socket-status.disconnected {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<body>
|
||||
{{> partials/prefix isLogs=true}}
|
||||
|
||||
<div id="toolbar">
|
||||
<button onclick="onFollowClicked()" id="follow-button">Follow</button>
|
||||
<input type="text" id="filter-text" placeholder="Filter..." />
|
||||
<div id="socket-status"></div>
|
||||
</div>
|
||||
|
||||
<div id="log-container"></div>
|
||||
|
||||
|
||||
{{> partials/suffix }}
|
||||
|
||||
<script>
|
||||
const logs = []
|
||||
const logTable = document.getElementById('logs')
|
||||
const logContainer = document.getElementById('log-container')
|
||||
const followButton = document.getElementById('follow-button')
|
||||
const filterText = document.getElementById('filter-text')
|
||||
const statusOrb = document.getElementById('socket-status')
|
||||
|
||||
let followLogs = false
|
||||
let programaticScroll = false
|
||||
|
||||
let filter = null
|
||||
|
||||
function main() {
|
||||
initWebSocket()
|
||||
setFollowState(true)
|
||||
|
||||
logContainer.innerHTML = ''
|
||||
|
||||
filterText.addEventListener('input', onFilterChanged)
|
||||
|
||||
logContainer.addEventListener('scroll', () => {
|
||||
if (programaticScroll) {
|
||||
programaticScroll = false
|
||||
} else {
|
||||
setFollowState(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onFollowClicked() {
|
||||
setFollowState(true)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
function onFilterChanged(event) {
|
||||
filter = event.target.value
|
||||
|
||||
logContainer.innerHTML = ''
|
||||
|
||||
logs
|
||||
.filter(it => logMatches(it, filter))
|
||||
.map(it => logToDiv(it))
|
||||
.forEach(it => logContainer.appendChild(it))
|
||||
|
||||
setFollowState(true)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
function initWebSocket() {
|
||||
const websocket = new WebSocket(`ws://${window.location.host}/logs/websocket`)
|
||||
let keepAliveTimer = null
|
||||
statusOrb.className = 'connecting'
|
||||
|
||||
websocket.onopen = () => {
|
||||
console.log("[open] Connection established");
|
||||
console.log("Sending to server");
|
||||
|
||||
statusOrb.className = 'connected'
|
||||
|
||||
keepAliveTimer = setInterval(() => websocket.send('keepalive'), 1000)
|
||||
}
|
||||
|
||||
websocket.onclose = () => {
|
||||
console.log('[close] Closed!')
|
||||
statusOrb.className = 'disconnected'
|
||||
|
||||
if (keepAliveTimer != null) {
|
||||
clearInterval(keepAliveTimer)
|
||||
keepAliveTimer = null
|
||||
}
|
||||
|
||||
setTimeout(() => initWebSocket(), 1000)
|
||||
}
|
||||
|
||||
websocket.onmessage = onWebSocketMessage
|
||||
}
|
||||
|
||||
function onWebSocketMessage(event) {
|
||||
const log = JSON.parse(event.data)
|
||||
logs.push(log)
|
||||
if (logs.length > 5_000) {
|
||||
logs.shift()
|
||||
}
|
||||
|
||||
if (filter == null || logMatches(log, filter)) {
|
||||
logContainer.appendChild(logToDiv(log))
|
||||
}
|
||||
|
||||
if (followLogs) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function logToDiv(log) {
|
||||
const div = document.createElement('div')
|
||||
|
||||
const linePrefix = `[${log.thread}] ${log.time} ${log.tag} ${log.level} `
|
||||
|
||||
let stackTraceString = log.stackTrace
|
||||
if (stackTraceString != null) {
|
||||
stackTraceString = ' \n' + stackTraceString
|
||||
}
|
||||
|
||||
let textContent = `${linePrefix}${log.message || ''}${stackTraceString || ''}`
|
||||
textContent = indentOverflowLines(textContent, linePrefix.length)
|
||||
|
||||
div.textContent = textContent
|
||||
div.classList.add(levelToClass(log.level))
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
function levelToClass(level) {
|
||||
switch (level) {
|
||||
case 'V': return 'log-verbose'
|
||||
case 'D': return 'log-debug'
|
||||
case 'I': return 'log-info'
|
||||
case 'W': return 'log-warn'
|
||||
case 'E': return 'log-error'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
function setFollowState(value) {
|
||||
if (followLogs === value) {
|
||||
return
|
||||
}
|
||||
|
||||
followLogs = value
|
||||
|
||||
if (followLogs) {
|
||||
followButton.classList.add('enabled')
|
||||
followButton.disabled = true
|
||||
} else {
|
||||
followButton.classList.remove('enabled')
|
||||
followButton.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
programaticScroll = true
|
||||
logContainer.scrollTop = logContainer.scrollHeight
|
||||
}
|
||||
|
||||
function indentOverflowLines(text, indent) {
|
||||
const lines = text.split('\n')
|
||||
|
||||
if (lines.length > 1) {
|
||||
const spaces = ' '.repeat(indent)
|
||||
const overflow = lines.slice(1)
|
||||
const indented = overflow.map(it => spaces + it).join('\n')
|
||||
return lines[0] + '\n' + indented
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
function logMatches(log, filter) {
|
||||
if (log.tag != null && log.tag.indexOf(filter) >= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (log.message != null && log.message.indexOf(filter) >= 0) {
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
if (log.stackTrace != null && log.stackTrace.indexOf(filter) >= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
main()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,6 +23,7 @@
|
||||
<li {{#if isBrowse}}class="active"{{/if}}><a href="/browse?db={{database}}">Browse</a></li>
|
||||
<li {{#if isQuery}}class="active"{{/if}}><a href="/query?db={{database}}">Query</a></li>
|
||||
<li {{#if isRecent}}class="active"{{/if}}><a href="/recent?db={{database}}">Recent</a></li>
|
||||
<li {{#if isLogs}}class="active"{{/if}}><a href="/logs?db={{database}}">Logs</a></li>
|
||||
{{#each plugins}}
|
||||
<li {{#if (eq name activePlugin.name)}}class="active"{{/if}}><a href="{{path}}">{{name}}</a></li>
|
||||
{{/each}}
|
||||
|
||||
Reference in New Issue
Block a user