Skip to content

Commit

Permalink
Add new 'Logs' page to the UI to view stdout logs
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Oct 10, 2020
1 parent f81d75a commit 8dbe30c
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 8 deletions.
1 change: 1 addition & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func registerHTTPHandlers(e *echo.Echo) {
g.GET("/api/settings", handleGetSettings)
g.PUT("/api/settings", handleUpdateSettings)
g.POST("/api/admin/reload", handleReloadApp)
g.GET("/api/logs", handleGetLogs)

g.GET("/api/subscribers/:id", handleGetSubscriber)
g.GET("/api/subscribers/:id/export", handleExportSubscriberData)
Expand Down
12 changes: 9 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"html/template"
"io"
"log"
"os"
"os/signal"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/jmoiron/sqlx"
"github.com/knadh/koanf"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/listmonk/internal/buflog"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/internal/media"
"github.com/knadh/listmonk/internal/messenger"
Expand All @@ -39,6 +41,7 @@ type App struct {
media media.Store
notifTpls *template.Template
log *log.Logger
bufLog *buflog.BufLog

// Channel for passing reload signals.
sigChan chan os.Signal
Expand All @@ -53,9 +56,12 @@ type App struct {
}

var (
lo = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
ko = koanf.New(".")
// Buffered log writer for storing N lines of log entries for the UI.
bufLog = buflog.New(5000)
lo = log.New(io.MultiWriter(os.Stdout, bufLog), "",
log.Ldate|log.Ltime|log.Lshortfile)

ko = koanf.New(".")
fs stuffbin.FileSystem
db *sqlx.DB
queries *Queries
Expand Down Expand Up @@ -119,7 +125,6 @@ func init() {

// Load settings from DB.
initSettings(queries)

}

func main() {
Expand All @@ -132,6 +137,7 @@ func main() {
media: initMediaStore(),
messengers: make(map[string]messenger.Messenger),
log: lo,
bufLog: bufLog,
}
_, app.queries = initQueries(queryFilePath, db, fs, true)
app.manager = initCampaignManager(app.queries, app.constants, app)
Expand Down
6 changes: 6 additions & 0 deletions cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ func handleUpdateSettings(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{true})
}

// handleGetLogs returns the log entries stored in the log buffer.
func handleGetLogs(c echo.Context) error {
app := c.Get("app").(*App)
return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
}

func getSettings(app *App) (settings, error) {
var (
b types.JSONText
Expand Down
16 changes: 13 additions & 3 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,19 @@
icon="file-image-outline" label="Templates"></b-menu-item>
</b-menu-item><!-- campaigns -->

<b-menu-item :to="{name: 'settings'}" tag="router-link"
:active="activeItem.settings"
icon="cog-outline" label="Settings"></b-menu-item>
<b-menu-item :expanded="activeGroup.settings"
:active="activeGroup.settings"
v-on:update:active="(state) => toggleGroup('settings', state)"
icon="cog-outline" label="Settings">

<b-menu-item :to="{name: 'settings'}" tag="router-link"
:active="activeItem.settings"
icon="cog-outline" label="Settings"></b-menu-item>

<b-menu-item :to="{name: 'logs'}" tag="router-link"
:active="activeItem.logs"
icon="newspaper-variant-outline" label="Logs"></b-menu-item>
</b-menu-item><!-- settings -->
</b-menu-list>
</b-menu>
</div>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,6 @@ export const getSettings = async () => http.get('/api/settings',

export const updateSettings = async (data) => http.put('/api/settings', data,
{ loading: models.settings });

export const getLogs = async () => http.get('/api/logs',
{ loading: models.logs });
26 changes: 25 additions & 1 deletion frontend/src/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ $turquoise: $green;
$red: #ff5722;
$link: $primary;
$input-placeholder-color: $grey-light;
$grey-lightest: #eaeaea;

$colors: map-merge($colors, (
"turquoise": ($green, $green-invert),
Expand Down Expand Up @@ -41,6 +42,11 @@ code {
color: $grey;
}

pre {
background: none;
border: 1px solid $grey-lightest;
}

ul.no {
list-style-type: none;
padding: 0;
Expand Down Expand Up @@ -226,7 +232,7 @@ section {
}
thead th, tbody td {
padding: 15px 10px;
border-color: #eaeaea;
border-color: $grey-lightest;
}
.actions a {
margin: 0 10px;
Expand Down Expand Up @@ -600,6 +606,24 @@ section.campaign {
}
}

/* Logs */
.logs {
.lines {
height: 70vh;
overflow-y: scroll;

.stamp {
color: $primary;
display: inline-block;
min-width: 160px;
}

.line:hover {
background: $white-bis;
}
}
}

/* C3 charting lib */
.c3 {
.c3-chart-lines .c3-line {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const models = Object.freeze({
templates: 'templates',
media: 'media',
settings: 'settings',
logs: 'logs',
});

// Ad-hoc URIs that are used outside of vuex requests.
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ const routes = [
meta: { title: 'Settings', group: 'settings' },
component: () => import(/* webpackChunkName: "main" */ '../views/Settings.vue'),
},
{
path: '/settings/logs',
name: 'logs',
meta: { title: 'Logs', group: 'settings' },
component: () => import(/* webpackChunkName: "main" */ '../views/Logs.vue'),
},
];

const router = new VueRouter({
Expand Down
1 change: 1 addition & 0 deletions frontend/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default new Vuex.Store({
[models.templates]: (state) => state[models.templates],
[models.settings]: (state) => state[models.settings],
[models.serverConfig]: (state) => state[models.serverConfig],
[models.logs]: (state) => state[models.logs],
},

modules: {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/Import.vue
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export default Vue.extend({
this.logs = data;
Vue.nextTick(() => {
// vue.$refs doesn't work as the logs textarea is rendered dynamiaclly.
// vue.$refs doesn't work as the logs textarea is rendered dynamically.
const ref = document.getElementById('import-log');
if (ref) {
ref.scrollTop = ref.scrollHeight;
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/views/Logs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<section class="logs content relative">
<h1 class="title is-4">Logs</h1>
<hr />
<b-loading :active="loading.logs" :is-full-page="false" />
<pre class="lines" ref="lines">
<template v-for="(l, i) in lines"><span v-html="formatLine(l)" :key="i" class="line"></span>
</template>
</pre>
</section>
</template>

<script>
import Vue from 'vue';
import { mapState } from 'vuex';
const reFormatLine = new RegExp(/^(.*) (.+?)\.go:[0-9]+:\s/g);
export default Vue.extend({
data() {
return {
lines: '',
pollId: null,
};
},
methods: {
formatLine: (l) => l.replace(reFormatLine, '<span class="stamp">$1</span> '),
getLogs() {
this.$api.getLogs().then((data) => {
this.lines = data;
this.$nextTick(() => {
this.$refs.lines.scrollTop = this.$refs.lines.scrollHeight;
});
});
},
},
computed: {
...mapState(['logs', 'loading']),
},
mounted() {
this.getLogs();
// Update the logs every 10 seconds.
this.pollId = setInterval(() => this.getLogs(), 10000);
},
destroyed() {
clearInterval(this.pollId);
},
});
</script>
50 changes: 50 additions & 0 deletions internal/buflog/buflog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package buflog

import (
"bytes"
"strings"
"sync"
)

// BufLog implements a simple log buffer that can be supplied to a std
// log instance. It stores logs up to N lines.
type BufLog struct {
maxLines int
buf *bytes.Buffer
lines []string

sync.RWMutex
}

// New returns a new log buffer that stores up to maxLines lines.
func New(maxLines int) *BufLog {
return &BufLog{
maxLines: maxLines,
buf: &bytes.Buffer{},
lines: make([]string, 0, maxLines),
}
}

// Write writes a log item to the buffer maintaining maxLines capacity
// using LIFO.
func (bu *BufLog) Write(b []byte) (n int, err error) {
bu.Lock()
if len(bu.lines) >= bu.maxLines {
bu.lines[0] = ""
bu.lines = bu.lines[1:len(bu.lines)]
}

bu.lines = append(bu.lines, strings.TrimSpace(string(b)))
bu.Unlock()
return len(b), nil
}

// Lines returns the log lines.
func (bu *BufLog) Lines() []string {
bu.RLock()
defer bu.RUnlock()

out := make([]string, len(bu.lines))
copy(out[:], bu.lines[:])
return out
}

0 comments on commit 8dbe30c

Please sign in to comment.