diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 21016895..4b2fba58 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -7,8 +7,14 @@ on: - master - main pull_request: + permissions: + # Required: allow read access to the content for analysis. contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + pull-requests: read + # Optional: Allow write access to checks to allow the action to annotate code in the PR. + checks: write jobs: golangci: @@ -21,6 +27,7 @@ jobs: with: # NOTE: Keep this in sync with the version from go.mod go-version: "1.20.x" + cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/ctx.go b/ctx.go index 9257825e..d5d847f9 100644 --- a/ctx.go +++ b/ctx.go @@ -1605,14 +1605,39 @@ func (c *DefaultCtx) Status(status int) Ctx { // // The returned value may be useful for logging. func (c *DefaultCtx) String() string { - return fmt.Sprintf( - "#%016X - %s <-> %s - %s %s", - c.fasthttp.ID(), - c.fasthttp.LocalAddr(), - c.fasthttp.RemoteAddr(), - c.fasthttp.Request.Header.Method(), - c.fasthttp.URI().FullURI(), - ) + // Get buffer from pool + buf := bytebufferpool.Get() + + // Start with the ID, converting it to a hex string without fmt.Sprintf + buf.WriteByte('#') //nolint:errcheck // It is fine to ignore the error + // Convert ID to hexadecimal + id := strconv.FormatUint(c.fasthttp.ID(), 16) + // Pad with leading zeros to ensure 16 characters + for i := 0; i < (16 - len(id)); i++ { + buf.WriteByte('0') //nolint:errcheck // It is fine to ignore the error + } + buf.WriteString(id) //nolint:errcheck // It is fine to ignore the error + buf.WriteString(" - ") //nolint:errcheck // It is fine to ignore the error + + // Add local and remote addresses directly + buf.WriteString(c.fasthttp.LocalAddr().String()) //nolint:errcheck // It is fine to ignore the error + buf.WriteString(" <-> ") //nolint:errcheck // It is fine to ignore the error + buf.WriteString(c.fasthttp.RemoteAddr().String()) //nolint:errcheck // It is fine to ignore the error + buf.WriteString(" - ") //nolint:errcheck // It is fine to ignore the error + + // Add method and URI + buf.Write(c.fasthttp.Request.Header.Method()) //nolint:errcheck // It is fine to ignore the error + buf.WriteByte(' ') //nolint:errcheck // It is fine to ignore the error + buf.Write(c.fasthttp.URI().FullURI()) //nolint:errcheck // It is fine to ignore the error + + // Allocate string + str := buf.String() + + // Reset buffer + buf.Reset() + bytebufferpool.Put(buf) + + return str } // Type sets the Content-Type HTTP header to the MIME type specified by the file extension. diff --git a/ctx_test.go b/ctx_test.go index df896920..93b1a1d4 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -4631,6 +4631,20 @@ func Test_Ctx_String(t *testing.T) { require.Equal(t, "#0000000000000000 - 0.0.0.0:0 <-> 0.0.0.0:0 - GET http:///", c.String()) } +// go test -v -run=^$ -bench=Benchmark_Ctx_String -benchmem -count=4 +func Benchmark_Ctx_String(b *testing.B) { + var str string + app := New() + ctx := app.NewCtx(&fasthttp.RequestCtx{}) + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + str = ctx.String() + } + require.Equal(b, "#0000000000000000 - 0.0.0.0:0 <-> 0.0.0.0:0 - GET http:///", str) +} + func TestCtx_ParamsInt(t *testing.T) { // Create a test context and set some strings (or params) // create a fake app to be used within this test diff --git a/log/default.go b/log/default.go index abc9c8f4..96c751a9 100644 --- a/log/default.go +++ b/log/default.go @@ -6,8 +6,8 @@ import ( "io" "log" "os" - "sync" + "github.com/gofiber/utils/v2" "github.com/valyala/bytebufferpool" ) @@ -75,8 +75,6 @@ func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []any if format != "" { _, _ = buf.WriteString(format) //nolint:errcheck // It is fine to ignore the error } - var once sync.Once - isFirst := true // Write keys and values privateLog buffer if len(keysAndValues) > 0 { if (len(keysAndValues) & 1) == 1 { @@ -84,14 +82,12 @@ func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []any } for i := 0; i < len(keysAndValues); i += 2 { - if format == "" && isFirst { - once.Do(func() { - _, _ = fmt.Fprintf(buf, "%s=%v", keysAndValues[i], keysAndValues[i+1]) - isFirst = false - }) - continue + if i > 0 || format != "" { + _ = buf.WriteByte(' ') //nolint:errcheck // It is fine to ignore the error } - _, _ = fmt.Fprintf(buf, " %s=%v", keysAndValues[i], keysAndValues[i+1]) + _, _ = buf.WriteString(keysAndValues[i].(string)) //nolint:errcheck // It is fine to ignore the error + _ = buf.WriteByte('=') //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(utils.ToString(keysAndValues[i+1])) //nolint:errcheck // It is fine to ignore the error } } diff --git a/log/default_test.go b/log/default_test.go index 78ef0204..3b1eb6f0 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -156,6 +156,60 @@ func Test_LogfKeyAndValues(t *testing.T) { } } +func BenchmarkLogfKeyAndValues(b *testing.B) { + tests := []struct { + name string + level Level + format string + keysAndValues []any + }{ + { + name: "test logf with debug level and key-values", + level: LevelDebug, + format: "", + keysAndValues: []any{"name", "Bob", "age", 30}, + }, + { + name: "test logf with info level and key-values", + level: LevelInfo, + format: "", + keysAndValues: []any{"status", "ok", "code", 200}, + }, + { + name: "test logf with warn level and key-values", + level: LevelWarn, + format: "", + keysAndValues: []any{"error", "not found", "id", 123}, + }, + { + name: "test logf with format and key-values", + level: LevelWarn, + format: "test", + keysAndValues: []any{"error", "not found", "id", 123}, + }, + { + name: "test logf with one key", + level: LevelWarn, + format: "", + keysAndValues: []any{"error"}, + }, + } + + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: tt.level, + depth: 4, + } + for i := 0; i < b.N; i++ { + l.privateLogw(tt.level, tt.format, tt.keysAndValues) + } + }) + } +} + func Test_WithContextCaller(t *testing.T) { logger = &defaultLogger{ stdlog: log.New(os.Stderr, "", log.Lshortfile), @@ -169,7 +223,7 @@ func Test_WithContextCaller(t *testing.T) { WithContext(ctx).Info("") Info("") - require.Equal(t, "default_test.go:169: [Info] \ndefault_test.go:170: [Info] \n", string(w.b)) + require.Equal(t, "default_test.go:223: [Info] \ndefault_test.go:224: [Info] \n", string(w.b)) } func Test_SetLevel(t *testing.T) { diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index 26669e3c..49a9202d 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "strconv" "sync" "github.com/gofiber/fiber/v3" @@ -47,17 +48,50 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { if data.ChainErr != nil { formatErr = " | " + data.ChainErr.Error() } - _, _ = buf.WriteString( //nolint:errcheck // This will never fail - fmt.Sprintf("%s | %3d | %13v | %15s | %-7s | %-"+data.ErrPaddingStr+"s %s\n", - data.Timestamp.Load().(string), - c.Response().StatusCode(), - data.Stop.Sub(data.Start), - c.IP(), - c.Method(), - c.Path(), - formatErr, - ), - ) + + // Helper function to append fixed-width string with padding + fixedWidth := func(s string, width int, rightAlign bool) { + if rightAlign { + for i := len(s); i < width; i++ { + _ = buf.WriteByte(' ') //nolint:errcheck // It is fine to ignore the error + } + _, _ = buf.WriteString(s) //nolint:errcheck // It is fine to ignore the error + } else { + _, _ = buf.WriteString(s) //nolint:errcheck // It is fine to ignore the error + for i := len(s); i < width; i++ { + _ = buf.WriteByte(' ') //nolint:errcheck // It is fine to ignore the error + } + } + } + + // Timestamp + _, _ = buf.WriteString(data.Timestamp.Load().(string)) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(" | ") //nolint:errcheck // It is fine to ignore the error + + // Status Code with 3 fixed width, right aligned + fixedWidth(strconv.Itoa(c.Response().StatusCode()), 3, true) + _, _ = buf.WriteString(" | ") //nolint:errcheck // It is fine to ignore the error + + // Duration with 13 fixed width, right aligned + fixedWidth(data.Stop.Sub(data.Start).String(), 13, true) + _, _ = buf.WriteString(" | ") //nolint:errcheck // It is fine to ignore the error + + // Client IP with 15 fixed width, right aligned + fixedWidth(c.IP(), 15, true) + _, _ = buf.WriteString(" | ") //nolint:errcheck // It is fine to ignore the error + + // HTTP Method with 7 fixed width, left aligned + fixedWidth(c.Method(), 7, false) + _, _ = buf.WriteString(" | ") //nolint:errcheck // It is fine to ignore the error + + // Path with dynamic padding for error message, left aligned + errPadding, _ := strconv.Atoi(data.ErrPaddingStr) //nolint:errcheck // It is fine to ignore the error + fixedWidth(c.Path(), errPadding, false) + + // Error message + _, _ = buf.WriteString(" ") //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(formatErr) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString("\n") //nolint:errcheck // It is fine to ignore the error } // Write buffer to output