From d3bc18c3694d0e6d3e12b69d0445f3a313c88371 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 11 Dec 2023 18:07:38 +0300 Subject: [PATCH] Logs refactoring after #780 --- internal/api/api.go | 28 ++------ internal/app/app.go | 48 ------------- internal/app/log.go | 117 ++++++++++++++++++++++++++++++++ www/log.html | 161 ++++++++++++++++++++------------------------ 4 files changed, 195 insertions(+), 159 deletions(-) create mode 100644 internal/app/log.go diff --git a/internal/api/api.go b/internal/api/api.go index 7c6cf1ed..db23360a 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -254,35 +254,15 @@ func restartHandler(w http.ResponseWriter, r *http.Request) { 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) { switch r.Method { case "GET": - // Send current state of the log file immediately - data := app.LogCollector.Bytes() - Response(w, data, "text/plain") + w.Header().Set("Content-Type", "application/jsonlines") + _, _ = app.MemoryLog.WriteTo(w) case "DELETE": - app.LogCollector.Reset() - - Response(w, "Log truncated", "text/plain") + app.MemoryLog.Reset() + Response(w, "OK", "text/plain") default: http.Error(w, "Method not allowed", http.StatusBadRequest) } diff --git a/internal/app/app.go b/internal/app/app.go index c0bf19ec..d5e0e75a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,21 +1,16 @@ package app import ( - "bytes" "errors" "flag" "fmt" - "io" - "os" "path/filepath" "runtime" "strings" - "time" "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/yaml" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -27,8 +22,6 @@ var Info = map[string]any{ "version": Version, } -var LogCollector bytes.Buffer - func Init() { var confs Config var version bool @@ -90,32 +83,6 @@ func Init() { 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) { for _, data := range configs { 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 { if ConfigPath == "" { return errors.New("config file disabled") @@ -166,6 +121,3 @@ func (c *Config) Set(value string) error { } var configs [][]byte - -// modules log levels -var modules map[string]string diff --git a/internal/app/log.go b/internal/app/log.go new file mode 100644 index 00000000..e8d4bc88 --- /dev/null +++ b/internal/app/log.go @@ -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 +} diff --git a/www/log.html b/www/log.html index c07994c9..138b03f4 100644 --- a/www/log.html +++ b/www/log.html @@ -14,49 +14,38 @@ flex-direction: column; } - html, body, #config { + html, body { width: 100%; height: 100%; } - .log-viewer { - background-color: #f4f4f4; - border: 1px solid #ddd; - padding: 10px; + table { + background-color: white; + text-align: left; + border-collapse: collapse; } - .log-entry { - font-family: 'Courier New', monospace; - margin-bottom: 5px; + + table td, table th { + border: 1px solid black; + padding: 5px 5px; } - .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; + table tbody td { + font-size: 13px; + vertical-align: top; } - /* Switch styling to make it look like a button */ - .switch { - width: auto; - padding: 10px 20px; - background-color: #f4433644; /* Red */ + table thead { + background: #CFCFCF; + background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%); + border-bottom: 3px solid black; } - .switch.active { - background-color: #4caf4f4e; /* Green */ + table thead th { + font-size: 15px; + font-weight: bold; + color: black; + text-align: center; } @@ -64,80 +53,78 @@
- - +

-
+ + + + + + + + + + +
TimeLevelMessage
\ No newline at end of file