Skip to content
44 changes: 35 additions & 9 deletions pkg/gofr/logging/remotelogger/dynamic_level_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,16 +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 {
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)
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:
Expand Down
65 changes: 65 additions & 0 deletions pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package remotelogger

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -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")
})
}
}
Loading