From bf241e2a5d9f582f6aad32a8f8a55f7b7356c00a Mon Sep 17 00:00:00 2001 From: HugoW5 Date: Thu, 23 Oct 2025 14:19:52 +0200 Subject: [PATCH 1/5] Add terminal logging support to httpLogFilter --- pkg/gofr/logging/logger.go | 4 ++ .../remotelogger/dynamic_level_logger.go | 37 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/pkg/gofr/logging/logger.go b/pkg/gofr/logging/logger.go index 76ed18595..8ccbae6cc 100644 --- a/pkg/gofr/logging/logger.go +++ b/pkg/gofr/logging/logger.go @@ -245,6 +245,10 @@ func checkIfTerminal(w io.Writer) bool { } } +func (l *logger) IsTerminal() bool { + return l.isTerminal +} + // ChangeLevel changes the log level of the logger. // This allows dynamic adjustment of the logging verbosity. func (l *logger) ChangeLevel(level Level) { diff --git a/pkg/gofr/logging/remotelogger/dynamic_level_logger.go b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go index eaef2d3c2..43aa19d14 100644 --- a/pkg/gofr/logging/remotelogger/dynamic_level_logger.go +++ b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go @@ -29,6 +29,18 @@ type httpLogFilter struct { initLogged bool } +func (f *httpLogFilter) isTerminalLogger() bool { + type terminalChecker interface { + IsTerminal() bool + } + + if l, ok := f.Logger.(terminalChecker); ok { + return l.IsTerminal() + } + + return false +} + // Log implements a simplified filtering strategy with consistent formatting. func (f *httpLogFilter) Log(args ...any) { if len(args) == 0 || args[0] == nil { @@ -78,14 +90,23 @@ func (f *httpLogFilter) handleHTTPLog(httpLog *service.Log, args []any) { // Subsequent successful hits - log at DEBUG level with consistent format case isSuccessful: if debugLogger, ok := f.Logger.(interface{ Debugf(string, ...any) }); ok { - colorCode := colorForResponseCode(httpLog.ResponseCode) - debugLogger.Debugf("\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s", - httpLog.CorrelationID, - colorCode, - httpLog.ResponseCode, - httpLog.ResponseTime, - httpLog.HTTPMethod, - httpLog.URI) + if f.isTerminalLogger() { + colorCode := colorForResponseCode(httpLog.ResponseCode) + debugLogger.Debugf("\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8dμs\u001B[0m %s %s", + httpLog.CorrelationID, + colorCode, + httpLog.ResponseCode, + httpLog.ResponseTime, + httpLog.HTTPMethod, + httpLog.URI) + } else { + debugLogger.Debugf("%s %d %dμs %s %s", + httpLog.CorrelationID, + httpLog.ResponseCode, + httpLog.ResponseTime, + httpLog.HTTPMethod, + httpLog.URI) + } } // Error responses - pass through to original logger From 502212219b3ce108e87d4183c46b47007cc2643a Mon Sep 17 00:00:00 2001 From: HugoW5 Date: Thu, 23 Oct 2025 14:21:55 +0200 Subject: [PATCH 2/5] Added descriptive comments for the new functions --- pkg/gofr/logging/logger.go | 2 ++ pkg/gofr/logging/remotelogger/dynamic_level_logger.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pkg/gofr/logging/logger.go b/pkg/gofr/logging/logger.go index 8ccbae6cc..291508617 100644 --- a/pkg/gofr/logging/logger.go +++ b/pkg/gofr/logging/logger.go @@ -245,6 +245,8 @@ func checkIfTerminal(w io.Writer) bool { } } +// IsTerminal returns true if the logger's output is a terminal (TTY). +// This helps decide whether to include ANSI color codes for pretty printing. func (l *logger) IsTerminal() bool { return l.isTerminal } diff --git a/pkg/gofr/logging/remotelogger/dynamic_level_logger.go b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go index 43aa19d14..dd3c7a712 100644 --- a/pkg/gofr/logging/remotelogger/dynamic_level_logger.go +++ b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go @@ -29,6 +29,8 @@ type httpLogFilter struct { initLogged bool } +// isTerminalLogger checks if the underlying logger supports terminal output (TTY). +// Returns true if colors and pretty-printing can be applied safely. func (f *httpLogFilter) isTerminalLogger() bool { type terminalChecker interface { IsTerminal() bool From ede28a536832224dee16a49ce9c808206cc9da44 Mon Sep 17 00:00:00 2001 From: HugoW5 Date: Thu, 30 Oct 2025 09:33:31 +0100 Subject: [PATCH 3/5] feat(logging): enhance HTTP logging with structured output and pretty print --- .../remotelogger/dynamic_level_logger.go | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/pkg/gofr/logging/remotelogger/dynamic_level_logger.go b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go index dd3c7a712..e044c2b13 100644 --- a/pkg/gofr/logging/remotelogger/dynamic_level_logger.go +++ b/pkg/gofr/logging/remotelogger/dynamic_level_logger.go @@ -21,6 +21,34 @@ const ( colorRed = 202 // For server errors (5xx) ) +// httpDebugMsg represents a structured HTTP debug log entry. +// It implements PrettyPrint for colored output and json.Marshaler for JSON logs. +type httpDebugMsg struct { + CorrelationID string `json:"correlation_id"` + ResponseCode int `json:"response_code"` + ResponseTime int64 `json:"response_time_us"` + HTTPMethod string `json:"http_method"` + URI string `json:"uri"` +} + +func (m httpDebugMsg) PrettyPrint(w io.Writer) { + colorCode := colorForResponseCode(m.ResponseCode) + fmt.Fprintf(w, + "\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8dμs\u001B[0m %s %s\n", + m.CorrelationID, + colorCode, + m.ResponseCode, + m.ResponseTime, + m.HTTPMethod, + m.URI, + ) +} + +func (m httpDebugMsg) MarshalJSON() ([]byte, error) { + type alias httpDebugMsg + return json.Marshal(alias(m)) +} + // httpLogFilter filters HTTP logs from remote logger to reduce noise. type httpLogFilter struct { logging.Logger @@ -29,20 +57,6 @@ type httpLogFilter struct { initLogged bool } -// isTerminalLogger checks if the underlying logger supports terminal output (TTY). -// Returns true if colors and pretty-printing can be applied safely. -func (f *httpLogFilter) isTerminalLogger() bool { - type terminalChecker interface { - IsTerminal() bool - } - - if l, ok := f.Logger.(terminalChecker); ok { - return l.IsTerminal() - } - - return false -} - // Log implements a simplified filtering strategy with consistent formatting. func (f *httpLogFilter) Log(args ...any) { if len(args) == 0 || args[0] == nil { @@ -91,25 +105,14 @@ func (f *httpLogFilter) handleHTTPLog(httpLog *service.Log, args []any) { // Subsequent successful hits - log at DEBUG level with consistent format case isSuccessful: - if debugLogger, ok := f.Logger.(interface{ Debugf(string, ...any) }); ok { - if f.isTerminalLogger() { - colorCode := colorForResponseCode(httpLog.ResponseCode) - debugLogger.Debugf("\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8dμs\u001B[0m %s %s", - httpLog.CorrelationID, - colorCode, - httpLog.ResponseCode, - httpLog.ResponseTime, - httpLog.HTTPMethod, - httpLog.URI) - } else { - debugLogger.Debugf("%s %d %dμs %s %s", - httpLog.CorrelationID, - httpLog.ResponseCode, - httpLog.ResponseTime, - httpLog.HTTPMethod, - httpLog.URI) - } + msg := httpDebugMsg{ + CorrelationID: httpLog.CorrelationID, + ResponseCode: httpLog.ResponseCode, + ResponseTime: httpLog.ResponseTime, + HTTPMethod: httpLog.HTTPMethod, + URI: httpLog.URI, } + f.Logger.Debug(msg) // Error responses - pass through to original logger default: From 02991fd77308e8a4e869ada68e2df209f9540a4b Mon Sep 17 00:00:00 2001 From: HugoW5 Date: Fri, 31 Oct 2025 08:34:09 +0100 Subject: [PATCH 4/5] refactor(logging): remove IsTerminal method from logger --- pkg/gofr/logging/logger.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/gofr/logging/logger.go b/pkg/gofr/logging/logger.go index 291508617..76ed18595 100644 --- a/pkg/gofr/logging/logger.go +++ b/pkg/gofr/logging/logger.go @@ -245,12 +245,6 @@ func checkIfTerminal(w io.Writer) bool { } } -// IsTerminal returns true if the logger's output is a terminal (TTY). -// This helps decide whether to include ANSI color codes for pretty printing. -func (l *logger) IsTerminal() bool { - return l.isTerminal -} - // ChangeLevel changes the log level of the logger. // This allows dynamic adjustment of the logging verbosity. func (l *logger) ChangeLevel(level Level) { From 074cc7152aa2f3fa467decdf24c8676f23e6ff19 Mon Sep 17 00:00:00 2001 From: Umang01-hash Date: Fri, 7 Nov 2025 13:23:15 +0530 Subject: [PATCH 5/5] add test for Prettyprint method --- .../remotelogger/dynamic_level_logger_test.go | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go b/pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go index d34585c16..633f31dfb 100644 --- a/pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go +++ b/pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go @@ -1,6 +1,7 @@ package remotelogger import ( + "bytes" "fmt" "net/http" "net/http/httptest" @@ -357,3 +358,67 @@ func TestLogLevelChangeToFatal_NoExit(t *testing.T) { // Verify the log contains a warning about the level change assert.Contains(t, log, "LOG_LEVEL updated from INFO to FATAL") } + +func TestHTTPDebugMsg_PrettyPrint(t *testing.T) { + cases := []struct { + name string + msg httpDebugMsg + wantColorSeq string + }{ + { + name: "2xx uses blue", + msg: httpDebugMsg{ + CorrelationID: "corr-200", + ResponseCode: 200, + ResponseTime: 123, + HTTPMethod: "GET", + URI: "/ok", + }, + wantColorSeq: fmt.Sprintf("\u001B[38;5;%dm", colorBlue), + }, + { + name: "4xx uses yellow", + msg: httpDebugMsg{ + CorrelationID: "corr-404", + ResponseCode: 404, + ResponseTime: 456, + HTTPMethod: "POST", + URI: "/not-found", + }, + wantColorSeq: fmt.Sprintf("\u001B[38;5;%dm", colorYellow), + }, + { + name: "5xx uses red", + msg: httpDebugMsg{ + CorrelationID: "corr-500", + ResponseCode: 500, + ResponseTime: 789, + HTTPMethod: "PUT", + URI: "/err", + }, + wantColorSeq: fmt.Sprintf("\u001B[38;5;%dm", colorRed), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + tc.msg.PrettyPrint(&buf) + out := buf.String() + + // basic content checks + assert.Contains(t, out, tc.msg.CorrelationID) + assert.Contains(t, out, tc.msg.HTTPMethod) + assert.Contains(t, out, tc.msg.URI) + assert.Contains(t, out, fmt.Sprintf("%d", tc.msg.ResponseCode)) + // response time should include the microsecond suffix + assert.Contains(t, out, fmt.Sprintf("%dμs", tc.msg.ResponseTime)) + + // color sequence must be present + assert.Contains(t, out, tc.wantColorSeq, "expected color sequence %q in output: %q", tc.wantColorSeq, out) + + // ensure reset code present + assert.Contains(t, out, "\u001B[0m") + }) + } +}