mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-05 08:16:55 +08:00
Add log file handling and viewing capabilities
This commit introduces the ability to handle log files through the API and provides a new log viewing page. The API now supports GET and DELETE methods for log file operations, allowing retrieval and deletion of log contents. A new log.html page has been added for viewing logs in the browser, with automatic refresh every 5 seconds and styling based on log levels. The app.go file has been updated to include a GetLogFilepath function that retrieves or generates the log file path. The NewLogger function now accepts a file parameter to enable file logging. The main.js file has been updated to include a link to the new log.html page. This enhancement improves the observability and management of the application by providing real-time access to logs and the ability to clear them directly from the web interface.
This commit is contained in:
@@ -52,6 +52,7 @@ func Init() {
|
|||||||
HandleFunc("api/config", configHandler)
|
HandleFunc("api/config", configHandler)
|
||||||
HandleFunc("api/exit", exitHandler)
|
HandleFunc("api/exit", exitHandler)
|
||||||
HandleFunc("api/restart", restartHandler)
|
HandleFunc("api/restart", restartHandler)
|
||||||
|
HandleFunc("api/log", logHandler)
|
||||||
|
|
||||||
Handler = http.DefaultServeMux // 4th
|
Handler = http.DefaultServeMux // 4th
|
||||||
|
|
||||||
@@ -246,6 +247,48 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
go shell.Restart()
|
go shell.Restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logHandler handles HTTP requests for log file operations.
|
||||||
|
// It supports two HTTP methods:
|
||||||
|
// - GET: Retrieves the content of the log file and sends it back to the client as plain text.
|
||||||
|
// - DELETE: Deletes the log file from the server.
|
||||||
|
//
|
||||||
|
// The function expects a valid http.ResponseWriter and an http.Request as parameters.
|
||||||
|
// For a GET request, it reads the log file specified by app.GetLogFilepath() and writes
|
||||||
|
// the content to the response writer with a "text/plain" content type. If the log file
|
||||||
|
// cannot be read, it responds with an HTTP 404 (Not Found) status.
|
||||||
|
//
|
||||||
|
// For a DELETE request, it attempts to delete the log file. If the deletion fails,
|
||||||
|
// it responds with an HTTP 503 (Service Unavailable) status.
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
if r.Method == "GET" {
|
||||||
|
data, err := os.ReadFile(app.GetLogFilepath())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Response(w, data, "text/plain")
|
||||||
|
} else if r.Method == "DELETE" {
|
||||||
|
err := os.Truncate(app.GetLogFilepath(), 0)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
type Source struct {
|
type Source struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -21,6 +22,7 @@ var Version = "1.8.4"
|
|||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
|
var LogFilePath string
|
||||||
var Info = map[string]any{
|
var Info = map[string]any{
|
||||||
"version": Version,
|
"version": Version,
|
||||||
}
|
}
|
||||||
@@ -77,16 +79,66 @@ func Init() {
|
|||||||
|
|
||||||
LoadConfig(&cfg)
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"], GetLogFilepath())
|
||||||
|
|
||||||
modules = cfg.Mod
|
modules = cfg.Mod
|
||||||
|
|
||||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
log.Debug().Msgf("[log] file: %s", GetLogFilepath())
|
||||||
|
|
||||||
migrateStore()
|
migrateStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
// GetLogFilepath retrieves the file path for the log file from the application's configuration.
|
||||||
|
// The configuration is expected to be in YAML format and contain a "log" section with a "file" key.
|
||||||
|
// It uses the LoadConfig function to populate the cfg structure with the configuration data.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// string: The file path of the log file as specified in the configuration.
|
||||||
|
//
|
||||||
|
// Note:
|
||||||
|
//
|
||||||
|
// The function assumes that the LoadConfig function is defined elsewhere and is responsible
|
||||||
|
// for loading and parsing the configuration into the provided struct.
|
||||||
|
// The cfg struct is an anonymous struct with a Mod field, which is a map with string keys and values.
|
||||||
|
// The "log" key within the Mod map is expected to contain a sub-map with the "file" key that holds the log file path.
|
||||||
|
//
|
||||||
|
// Example of expected YAML configuration:
|
||||||
|
//
|
||||||
|
// log:
|
||||||
|
// file: "/path/to/logfile.log"
|
||||||
|
//
|
||||||
|
// If the "file" key is not found within the "log" section of the configuration, the function will return an empty string.
|
||||||
|
func GetLogFilepath() string {
|
||||||
|
var cfg struct {
|
||||||
|
Mod map[string]string `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if LogFilePath != "" {
|
||||||
|
return LogFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
|
if cfg.Mod["file"] == "" {
|
||||||
|
// Generate temporary log file
|
||||||
|
tmpFile, err := ioutil.TempFile("", "go2rtc*.log")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer tmpFile.Close()
|
||||||
|
|
||||||
|
LogFilePath = tmpFile.Name()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
LogFilePath = cfg.Mod["file"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return LogFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(format string, level string, file string) zerolog.Logger {
|
||||||
var writer io.Writer = os.Stdout
|
var writer io.Writer = os.Stdout
|
||||||
|
|
||||||
if format != "json" {
|
if format != "json" {
|
||||||
@@ -96,6 +148,19 @@ func NewLogger(format string, level string) zerolog.Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if file != "" {
|
||||||
|
fileHandler, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||||
|
fileLogger := zerolog.ConsoleWriter{
|
||||||
|
Out: fileHandler, TimeFormat: "15:04:05.000",
|
||||||
|
NoColor: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
writer = zerolog.MultiLevelWriter(writer, fileLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
zerolog.TimeFieldFormat = time.RFC3339Nano
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
lvl, err := zerolog.ParseLevel(level)
|
||||||
|
100
www/log.html
Normal file
100
www/log.html
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Logs</title>
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background-color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #config {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-viewer {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.info { color: #0174DF; }
|
||||||
|
.debug { color: #585858; }
|
||||||
|
.error { color: #DF0101; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
<div>
|
||||||
|
<button id="clean">Clean</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="log-viewer" id="log"></div>
|
||||||
|
<script>
|
||||||
|
const logbody = document.getElementById('log');
|
||||||
|
|
||||||
|
document.getElementById('clean').addEventListener('click', async () => {
|
||||||
|
r = await fetch('api/log', {method: 'DELETE'});
|
||||||
|
if (r.ok) {
|
||||||
|
reload();
|
||||||
|
alert('OK');
|
||||||
|
} else {
|
||||||
|
alert(await r.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sanitizes the input text to prevent XSS when inserting into the DOM
|
||||||
|
function escapeHTML(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLogStyling(logText) {
|
||||||
|
// Split the log into lines
|
||||||
|
const lines = logText.split('\n');
|
||||||
|
// Create HTML content with styled spans
|
||||||
|
const styledLines = lines.map(line => {
|
||||||
|
let className = '';
|
||||||
|
if (line.includes(' INF ')) className = 'info';
|
||||||
|
if (line.includes(' DBG ')) className = 'debug';
|
||||||
|
if (line.includes(' ERR ')) className = 'error';
|
||||||
|
return `<div class="log-entry ${className}">${escapeHTML(line)}</div>`;
|
||||||
|
});
|
||||||
|
// Join the lines back into a single string
|
||||||
|
return styledLines.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
const url = new URL('api/log', location.href);
|
||||||
|
fetch(url, {cache: 'no-cache'})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
// Apply styling to the log data
|
||||||
|
logbody.innerHTML = applyLogStyling(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('An error occurred:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the logs every 5 seconds
|
||||||
|
setInterval(reload, 5000);
|
||||||
|
|
||||||
|
reload();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -47,6 +47,7 @@ nav li {
|
|||||||
<li><a href="index.html">Streams</a></li>
|
<li><a href="index.html">Streams</a></li>
|
||||||
<li><a href="add.html">Add</a></li>
|
<li><a href="add.html">Add</a></li>
|
||||||
<li><a href="editor.html">Config</a></li>
|
<li><a href="editor.html">Config</a></li>
|
||||||
|
<li><a href="log.html">Log</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
` + document.body.innerHTML;
|
` + document.body.innerHTML;
|
||||||
|
Reference in New Issue
Block a user