#3886·kratos

middleware/logging: duplicate error string recorded in `error` & `stack`, and all non-nil errors hardcoded to LevelError

Author: formal-youCreated Sep 11, 2026Updated Sep 11, 2026
Labelsbug

What happened:

In github.com/go-kratos/kratos/v3/middleware/logging, there are two significant issues regarding error handling and structured logging:

  1. Duplicate error string recorded in both error and stack attributes: In logging.go, extractError(err) attempts to extract stack traces via fmt.Sprintf("%+v", err):

    go
    func extractError(err error) (slog.Level, string) {
        if err != nil {
            return slog.LevelError, fmt.Sprintf("%+v", err)
        }
        return slog.LevelInfo, ""
    }

    However, Kratos's own *errors.Error (as well as Go standard library errors created via errors.New or fmt.Errorf) does not implement fmt.Formatter with stack frame capture. As a result, fmt.Sprintf("%+v", err) evaluates to the exact same string as err.Error(). Consequently, the middleware logs the exact same error message twice:

    go
    attrs = append(attrs, slog.Any("error", err))
    if stack != "" {
        attrs = append(attrs, slog.String("stack", stack)) // Identical to err.Error()!
    }
  2. All non-nil errors are hardcoded to slog.LevelError: Any non-nil error—including client/business expected errors (e.g. 400 Bad Request, 404 Not Found, 401 Unauthorized) and transient rate-limiting signals (e.g. 503 Service Unavailable, 429 Too Many Requests)—is unconditionally logged at slog.LevelError. In production SRE environments, this causes severe Alert Fatigue (alert storms) on monitoring platforms (e.g., Prometheus / Grafana / Datadog) that trigger on error log rates.

What you expected to happen:

  1. No duplicate error logging: If an error does not contain a real stack trace (or when fmt.Sprintf("%+v", err) == err.Error()), the stack attribute should either be omitted or deduplicated. Furthermore, runtime panic stacks are already captured by middleware/recovery via runtime.Stack(), so middleware/logging should not duplicate pseudo-stacks.
  2. Dynamic Log Level Mapping: Status codes should map to appropriate log levels:
    • 2xx / 3xx -> slog.LevelInfo
    • 4xx (and optionally 503/429) -> slog.LevelWarn (not paging SRE alerts)
    • 500 / internal server errors -> slog.LevelError Support an optional WithLevelFunc(fn func(code int32, err error) slog.Level) option for custom mapping.

How to reproduce it (as minimally and precisely as possible):

Run a handler with middleware/logging that returns a business 400 error:

go
package main

import (
	"context"
	"log/slog"
	"os"

	"github.com/go-kratos/kratos/v3/errors"
	"github.com/go-kratos/kratos/v3/middleware/logging"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	mw := logging.Server(logger)

	handler := mw(func(ctx context.Context, req any) (any, error) {
		return nil, errors.BadRequest("USER_NOT_FOUND", "user does not exist")
	})

	_, _ = handler(context.Background(), "test-request")
}

Actual Log Output:

json
{
  "time": "2026-09-12T01:30:00Z",
  "level": "ERROR",
  "msg": "server request",
  "kind": "server",
  "component": "",
  "operation": "",
  "code": 400,
  "reason": "USER_NOT_FOUND",
  "error": "error: code = 400 reason = USER_NOT_FOUND message = user does not exist metadata = map[] cause = <nil>",
  "stack": "error: code = 400 reason = USER_NOT_FOUND message = user does not exist metadata = map[] cause = <nil>"
}

Notice that:

  • "level" is ERROR for a simple 400 Bad Request;
  • "stack" and "error" contain identical text.

Anything else we need to know?:

  • In Kratos v2, logging only had a "stack" key (which formatted with %+v). In v3, when migrating to log/slog, "error" was added without removing or cleaning up "stack".
  • We have tested a clean fix:
    1. Omit the stack attribute when no stack frames are present or remove stack in favor of middleware/recovery handling.
    2. Provide a default level mapper where code >= 400 && code < 500 outputs slog.LevelWarn.
  • Happy to submit a Pull Request if the maintainers agree with this direction!

Environment:

  • Kratos version (use kratos -v): v3.0.0
  • Go version (use go version): go version go1.24.0 (or go1.27)
  • OS (e.g: cat /etc/os-release): Windows 11 / Linux (Ubuntu 24.04)
  • Others: log/slog with JSONHandler