From 8d382afa0f4f2af97a30d9b0c65e7a948e18b752 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Sun, 26 Nov 2023 23:21:57 +0300 Subject: [PATCH 1/5] 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. --- internal/api/api.go | 43 +++++++++++++++++++ internal/app/app.go | 69 +++++++++++++++++++++++++++++- www/log.html | 100 ++++++++++++++++++++++++++++++++++++++++++++ www/main.js | 1 + 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 www/log.html diff --git a/internal/api/api.go b/internal/api/api.go index 9f00a6bc..d08912df 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -52,6 +52,7 @@ func Init() { HandleFunc("api/config", configHandler) HandleFunc("api/exit", exitHandler) HandleFunc("api/restart", restartHandler) + HandleFunc("api/log", logHandler) Handler = http.DefaultServeMux // 4th @@ -246,6 +247,48 @@ func restartHandler(w http.ResponseWriter, r *http.Request) { 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 { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` diff --git a/internal/app/app.go b/internal/app/app.go index 6a432cf1..7b8b46f0 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io" + "io/ioutil" "os" "path/filepath" "runtime" @@ -21,6 +22,7 @@ var Version = "1.8.4" var UserAgent = "go2rtc/" + Version var ConfigPath string +var LogFilePath string var Info = map[string]any{ "version": Version, } @@ -77,16 +79,66 @@ func Init() { LoadConfig(&cfg) - log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"]) + log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"], GetLogFilepath()) modules = cfg.Mod log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH) + log.Debug().Msgf("[log] file: %s", GetLogFilepath()) 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 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 lvl, err := zerolog.ParseLevel(level) diff --git a/www/log.html b/www/log.html new file mode 100644 index 00000000..4d86a7c3 --- /dev/null +++ b/www/log.html @@ -0,0 +1,100 @@ + + + + Logs + + + + + + +
+ +
+
+
+ + + \ No newline at end of file diff --git a/www/main.js b/www/main.js index 9ec2fecf..63dbde32 100644 --- a/www/main.js +++ b/www/main.js @@ -47,6 +47,7 @@ nav li {
  • Streams
  • Add
  • Config
  • +
  • Log
  • ` + document.body.innerHTML; From ab47d5718f667abe70227acd39abf0c218a0619a Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 28 Nov 2023 22:55:50 +0300 Subject: [PATCH 2/5] Refactor log handling and add UI auto-update toggle This commit refactors the log handling in the API to use a switch statement for improved readability and maintainability. It also introduces error messages with more context when reading or truncating the log file fails. On the frontend, a new auto-update toggle button has been added to the log viewer, allowing users to enable or disable automatic log updates. The button's appearance changes based on its state, providing a clear visual indication of whether auto-update is active. Additionally, the button styling has been updated to ensure consistency across the interface. --- internal/api/api.go | 20 ++++++++++--------- www/log.html | 47 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index d08912df..b8a8c753 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -268,25 +268,27 @@ func restartHandler(w http.ResponseWriter, r *http.Request) { // // No return values are provided since the function writes directly to the response writer. func logHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + logFilePath := app.GetLogFilepath() - if r.Method == "GET" { - data, err := os.ReadFile(app.GetLogFilepath()) + // Send current state of the log file immediately + data, err := os.ReadFile(logFilePath) if err != nil { - http.Error(w, "", http.StatusNotFound) + http.Error(w, "Error reading log file", http.StatusInternalServerError) return } Response(w, data, "text/plain") - } else if r.Method == "DELETE" { + case "DELETE": err := os.Truncate(app.GetLogFilepath(), 0) if err != nil { - http.Error(w, "", http.StatusServiceUnavailable) + http.Error(w, "Error truncating log file", http.StatusServiceUnavailable) return } - } else { - http.Error(w, "", http.StatusBadRequest) - return + Response(w, "Log file deleted", "text/plain") + default: + http.Error(w, "Method not allowed", http.StatusBadRequest) } - } type Source struct { diff --git a/www/log.html b/www/log.html index 4d86a7c3..c07994c9 100644 --- a/www/log.html +++ b/www/log.html @@ -31,12 +31,41 @@ .info { color: #0174DF; } .debug { color: #585858; } .error { color: #DF0101; } + + /* Button styling */ + #clean, .switch { + background-color: #b89d94; + border: none; + color: #695753; + padding: 10px 20px; + 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 */ + }
    + +

    @@ -44,7 +73,7 @@ const logbody = document.getElementById('log'); document.getElementById('clean').addEventListener('click', async () => { - r = await fetch('api/log', {method: 'DELETE'}); + let r = await fetch('api/log', {method: 'DELETE'}); if (r.ok) { reload(); alert('OK'); @@ -78,6 +107,16 @@ 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() { const url = new URL('api/log', location.href); fetch(url, {cache: 'no-cache'}) @@ -92,7 +131,11 @@ } // Reload the logs every 5 seconds - setInterval(reload, 5000); + setInterval(() => { + if (autoUpdateEnabled) { + reload(); + } + }, 5000); reload(); From b60000ac34acc8f501ffe0f795ae42b20cc5d35b Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 5 Dec 2023 17:44:32 +0300 Subject: [PATCH 3/5] Refactor log handling to use in-memory Logger --- internal/api/api.go | 16 +++------- internal/app/app.go | 76 +++++++-------------------------------------- 2 files changed, 15 insertions(+), 77 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index b8a8c753..bc7fdfeb 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -270,22 +270,14 @@ func restartHandler(w http.ResponseWriter, r *http.Request) { func logHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - logFilePath := app.GetLogFilepath() // Send current state of the log file immediately - data, err := os.ReadFile(logFilePath) - if err != nil { - http.Error(w, "Error reading log file", http.StatusInternalServerError) - return - } + data := app.LogCollector.Bytes() Response(w, data, "text/plain") case "DELETE": - err := os.Truncate(app.GetLogFilepath(), 0) - if err != nil { - http.Error(w, "Error truncating log file", http.StatusServiceUnavailable) - return - } - Response(w, "Log file deleted", "text/plain") + app.LogCollector.Reset() + + Response(w, "Log truncated", "text/plain") default: http.Error(w, "Method not allowed", http.StatusBadRequest) } diff --git a/internal/app/app.go b/internal/app/app.go index 7b8b46f0..0cddd464 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,11 +1,12 @@ package app import ( + "bytes" "errors" "flag" "fmt" "io" - "io/ioutil" + "os" "path/filepath" "runtime" @@ -27,6 +28,8 @@ var Info = map[string]any{ "version": Version, } +var LogCollector bytes.Buffer + func Init() { var confs Config var version bool @@ -79,66 +82,16 @@ func Init() { LoadConfig(&cfg) - log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"], GetLogFilepath()) + log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"]) modules = cfg.Mod log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH) - log.Debug().Msgf("[log] file: %s", GetLogFilepath()) migrateStore() } -// 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 { +func NewLogger(format string, level string) zerolog.Logger { var writer io.Writer = os.Stdout if format != "json" { @@ -147,20 +100,13 @@ func NewLogger(format string, level string, file string) zerolog.Logger { NoColor: writer != os.Stdout || format == "text", } } - - 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) - } - + 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) From 0a8ab9bbd19601bc8794d3ca3701e24d741baa22 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 5 Dec 2023 17:50:51 +0300 Subject: [PATCH 4/5] Update app.go to remove the unused variable LogFilePath --- internal/app/app.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/app/app.go b/internal/app/app.go index 0cddd464..c0bf19ec 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -23,7 +23,6 @@ var Version = "1.8.4" var UserAgent = "go2rtc/" + Version var ConfigPath string -var LogFilePath string var Info = map[string]any{ "version": Version, } From fc5b36acd3a826eb753271f52df8a9933373e95c Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Tue, 5 Dec 2023 17:54:53 +0300 Subject: [PATCH 5/5] actualise godoc comment for api.logHandler func --- internal/api/api.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index bc7fdfeb..7a467880 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -247,18 +247,16 @@ func restartHandler(w http.ResponseWriter, r *http.Request) { go shell.Restart() } -// logHandler handles HTTP requests for log file operations. +// logHandler handles HTTP requests for log buffer 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. +// - 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 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 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 attempts to delete the log file. If the deletion fails, -// it responds with an HTTP 503 (Service Unavailable) status. +// For a DELETE request, it clears the in-memory buffer. // // For any other HTTP method, it responds with an HTTP 400 (Bad Request) status. //