Logs refactoring after #780

This commit is contained in:
Alex X
2023-12-11 18:07:38 +03:00
parent 1f3a32023f
commit d3bc18c369
4 changed files with 199 additions and 163 deletions

View File

@@ -254,35 +254,15 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
go shell.Restart() go shell.Restart()
} }
// logHandler handles HTTP requests for log buffer operations.
// It supports two HTTP methods:
// - GET: Retrieves the content of in-memory log and sends it back to the client as plain text.
// - DELETE: Clear the in-memory log buffer.
//
// The function expects a valid http.ResponseWriter and an http.Request as parameters.
// For a GET request, it reads the log from in-memory buffer and writes
// the content to the response writer with a "text/plain" content type.
//
// For a DELETE request, it clears the in-memory buffer.
//
// For any other HTTP method, it responds with an HTTP 400 (Bad Request) status.
//
// Parameters:
// - w http.ResponseWriter: The response writer to write the HTTP response to.
// - r *http.Request: The HTTP request object containing the request details.
//
// No return values are provided since the function writes directly to the response writer.
func logHandler(w http.ResponseWriter, r *http.Request) { func logHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case "GET": case "GET":
// Send current state of the log file immediately // Send current state of the log file immediately
data := app.LogCollector.Bytes() w.Header().Set("Content-Type", "application/jsonlines")
Response(w, data, "text/plain") _, _ = app.MemoryLog.WriteTo(w)
case "DELETE": case "DELETE":
app.LogCollector.Reset() app.MemoryLog.Reset()
Response(w, "OK", "text/plain")
Response(w, "Log truncated", "text/plain")
default: default:
http.Error(w, "Method not allowed", http.StatusBadRequest) http.Error(w, "Method not allowed", http.StatusBadRequest)
} }

View File

@@ -1,21 +1,16 @@
package app package app
import ( import (
"bytes"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml" "github.com/AlexxIT/go2rtc/pkg/yaml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -27,8 +22,6 @@ var Info = map[string]any{
"version": Version, "version": Version,
} }
var LogCollector bytes.Buffer
func Init() { func Init() {
var confs Config var confs Config
var version bool var version bool
@@ -90,32 +83,6 @@ func Init() {
migrateStore() migrateStore()
} }
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
NoColor: writer != os.Stdout || format == "text",
}
}
memoryLogger := zerolog.ConsoleWriter{
Out: &LogCollector, TimeFormat: "15:04:05.000",
NoColor: true,
}
writer = zerolog.MultiLevelWriter(writer, memoryLogger)
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func LoadConfig(v any) { func LoadConfig(v any) {
for _, data := range configs { for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil { if err := yaml.Unmarshal(data, v); err != nil {
@@ -124,18 +91,6 @@ func LoadConfig(v any) {
} }
} }
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
}
return log.Logger
}
func PatchConfig(key string, value any, path ...string) error { func PatchConfig(key string, value any, path ...string) error {
if ConfigPath == "" { if ConfigPath == "" {
return errors.New("config file disabled") return errors.New("config file disabled")
@@ -166,6 +121,3 @@ func (c *Config) Set(value string) error {
} }
var configs [][]byte var configs [][]byte
// modules log levels
var modules map[string]string

117
internal/app/log.go Normal file
View File

@@ -0,0 +1,117 @@
package app
import (
"io"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var MemoryLog *circularBuffer
func NewLogger(format string, level string) zerolog.Logger {
var writer io.Writer = os.Stdout
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000", NoColor: format == "text",
}
}
MemoryLog = newBuffer(16)
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
}
func GetLogger(module string) zerolog.Logger {
if s, ok := modules[module]; ok {
lvl, err := zerolog.ParseLevel(s)
if err == nil {
return log.Level(lvl)
}
log.Warn().Err(err).Caller().Send()
}
return log.Logger
}
// modules log levels
var modules map[string]string
const chunkSize = 1 << 16
type circularBuffer struct {
chunks [][]byte
r, w int
}
func newBuffer(chunks int) *circularBuffer {
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
// create first chunk
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
return b
}
func (b *circularBuffer) Write(p []byte) (n int, err error) {
n = len(p)
// check if chunk has size
if len(b.chunks[b.w])+n > chunkSize {
// increase write chunk index
if b.w++; b.w == cap(b.chunks) {
b.w = 0
}
// check overflow
if b.r == b.w {
// increase read chunk index
if b.r++; b.r == cap(b.chunks) {
b.r = 0
}
}
// check if current chunk exists
if b.w == len(b.chunks) {
// allocate new chunk
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
} else {
// reset len of current chunk
b.chunks[b.w] = b.chunks[b.w][:0]
}
}
b.chunks[b.w] = append(b.chunks[b.w], p...)
return
}
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
for i := b.r; ; {
var nn int
if nn, err = w.Write(b.chunks[i]); err != nil {
return
}
n += int64(nn)
if i == b.w {
break
}
if i++; i == cap(b.chunks) {
i = 0
}
}
return
}
func (b *circularBuffer) Reset() {
b.chunks[0] = b.chunks[0][:0]
b.r = 0
b.w = 0
}

View File

@@ -14,49 +14,38 @@
flex-direction: column; flex-direction: column;
} }
html, body, #config { html, body {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.log-viewer { table {
background-color: #f4f4f4; background-color: white;
border: 1px solid #ddd; text-align: left;
padding: 10px; border-collapse: collapse;
} }
.log-entry {
font-family: 'Courier New', monospace;
margin-bottom: 5px;
}
.info { color: #0174DF; }
.debug { color: #585858; }
.error { color: #DF0101; }
/* Button styling */ table td, table th {
#clean, .switch { border: 1px solid black;
background-color: #b89d94; padding: 5px 5px;
border: none; }
color: #695753;
padding: 10px 20px; table tbody td {
font-size: 13px;
vertical-align: top;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center; text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
outline: none;
transition: background-color 0.3s;
}
/* Switch styling to make it look like a button */
.switch {
width: auto;
padding: 10px 20px;
background-color: #f4433644; /* Red */
}
.switch.active {
background-color: #4caf4f4e; /* Green */
} }
</style> </style>
</head> </head>
@@ -64,80 +53,78 @@
<script src="main.js"></script> <script src="main.js"></script>
<div> <div>
<button id="clean">Clean</button> <button id="clean">Clean</button>
<!-- Switch for auto-update --> <button id="update">Auto Update: ON</button>
<button class="switch active" id="autoUpdate">Auto Update: ON</button>
</div> </div>
<br> <br>
<div class="log-viewer" id="log"></div> <table>
<thead>
<tr>
<th style="width: 130px">Time</th>
<th style="width: 40px">Level</th>
<th>Message</th>
</tr>
</thead>
<tbody id="log">
</tbody>
</table>
<script> <script>
const logbody = document.getElementById('log');
document.getElementById('clean').addEventListener('click', async () => { document.getElementById('clean').addEventListener('click', async () => {
let r = await fetch('api/log', {method: 'DELETE'}); const r = await fetch('api/log', {method: 'DELETE'});
if (r.ok) { if (r.ok) reload();
reload(); alert(await r.text());
alert('OK');
} else {
alert(await r.text());
}
}); });
// Sanitizes the input text to prevent XSS when inserting into the DOM // Sanitizes the input text to prevent XSS when inserting into the DOM
function escapeHTML(text) { function escapeHTML(text) {
return text return text
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/"/g, '&quot;') .replace(/"/g, '&quot;')
.replace(/'/g, '&#039;'); .replace(/'/g, '&#039;')
.replace(/\n/g, '<br>');
} }
function applyLogStyling(logText) { function applyLogStyling(jsonlines) {
// Split the log into lines const KEYS = ['time', 'level', 'message'];
const lines = logText.split('\n'); const lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\n', ',') + ']');
// Create HTML content with styled spans return lines.map(line => {
const styledLines = lines.map(line => { const ts = new Date(line['time']);
let className = ''; const msg = Object.keys(line).reduce((msg, key) => {
if (line.includes(' INF ')) className = 'info'; return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg;
if (line.includes(' DBG ')) className = 'debug'; }, line['message']);
if (line.includes(' ERR ')) className = 'error'; return `<tr><td>${ts.toLocaleString()}</td><td>${line['level']}</td><td>${escapeHTML(msg)}</td></tr>`;
return `<div class="log-entry ${className}">${escapeHTML(line)}</div>`; }).join('');
});
// Join the lines back into a single string
return styledLines.join('');
} }
// Handle auto-update switch
const autoUpdateButton = document.getElementById('autoUpdate');
let autoUpdateEnabled = true;
autoUpdateButton.addEventListener('click', () => {
autoUpdateEnabled = !autoUpdateEnabled;
autoUpdateButton.classList.toggle('active');
autoUpdateButton.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
});
function reload() { function reload() {
const url = new URL('api/log', location.href); const url = new URL('api/log', location.href);
fetch(url, {cache: 'no-cache'}) fetch(url, {cache: 'no-cache'})
.then(response => response.text()) .then(response => response.text())
.then(data => { .then(data => {
// Apply styling to the log data // Apply styling to the log data
logbody.innerHTML = applyLogStyling(data); document.getElementById('log').innerHTML = applyLogStyling(data);
}) })
.catch(error => { .catch(error => {
console.error('An error occurred:', error); console.error('An error occurred:', error);
}); });
} }
// Reload the logs every 5 seconds reload();
setInterval(() => {
if (autoUpdateEnabled) {
reload();
}
}, 5000);
reload(); // Handle auto-update switch
let autoUpdateEnabled = true;
const update = document.getElementById('update');
update.addEventListener('click', () => {
autoUpdateEnabled = !autoUpdateEnabled;
update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;
});
// Reload the logs every 5 seconds
setInterval(() => {
if (autoUpdateEnabled) reload();
}, 5000);
</script> </script>
</body> </body>
</html> </html>