mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-08 09:40:14 +08:00
Logs refactoring after #780
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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
117
internal/app/log.go
Normal 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
|
||||||
|
}
|
169
www/log.html
169
www/log.html
@@ -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, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''')
|
||||||
|
.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>
|
Reference in New Issue
Block a user