sloghttp

package module
v1.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 14, 2026 License: MIT Imports: 18 Imported by: 26

README

slog: net/http middleware

tag Go Version GoDoc Build Status Go report Coverage Contributors License

net/http handler to log HTTP requests using slog.

See also:

HTTP middlewares:

Loggers:

Log sinks:

🚀 Install

go get github.com/samber/slog-http

Compatibility: go >= 1.21

No breaking changes will be made to exported APIs before v2.0.0.

💡 Usage

Handler options
type Config struct {
	DefaultLevel     slog.Level
	ClientErrorLevel slog.Level
	ServerErrorLevel slog.Level

	WithUserAgent      bool
	WithRequestID      bool
	WithRequestBody    bool
	WithRequestHeader  bool
	WithResponseBody   bool
	WithResponseHeader bool
	WithSpanID         bool
	WithTraceID        bool
    WithClientIP       bool
	WithCustomMessage  func(w http.ResponseWriter, r *http.Request) string

	Filters []Filter
}

Attributes will be injected in log payload.

Other global parameters:

sloghttp.TraceIDKey = "trace_id"
sloghttp.SpanIDKey = "span_id"
sloghttp.RequestBodyMaxSize  = 64 * 1024 // 64KB
sloghttp.ResponseBodyMaxSize = 64 * 1024 // 64KB
sloghttp.HiddenRequestHeaders = map[string]struct{}{ ... }
sloghttp.HiddenResponseHeaders = map[string]struct{}{ ... }
sloghttp.RequestIDHeaderKey = "X-Request-Id"
Minimal
import (
	"net/http"
	"os"
	"time"

	sloghttp "github.com/samber/slog-http"
	"log/slog"
)

// Create a slog logger, which:
//   - Logs to stdout.
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

// mux router
mux := http.NewServeMux()

// Routes
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}))
mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	http.Error(w,  "I'm angry" http.StatusInternalServerError)
}))

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.New(logger)(handler)

// Start server
http.ListenAndServe(":4242", handler)

// output:
// time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Success" env=production request.time=2023-10-15T20:32:58.626+02:00 request.method=GET request.path=/ request.route="" request.ip=127.0.0.1:63932 request.length=0 response.time=2023-10-15T20:32:58.926+02:00 response.latency=100ms response.status=200 response.length=7 id=229c7fc8-64f5-4467-bc4a-940700503b0d
OTEL
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

config := sloghttp.Config{
	WithSpanID:  true,
	WithTraceID: true,
}

mux := http.NewServeMux()

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.NewWithConfig(logger, config)(handler)

// Start server
http.ListenAndServe(":4242", handler)
Custom log levels
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

config := sloghttp.Config{
	DefaultLevel:     slog.LevelInfo,
	ClientErrorLevel: slog.LevelWarn,
	ServerErrorLevel: slog.LevelError,
}

mux := http.NewServeMux()

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.NewWithConfig(logger, config)(handler)

// Start server
http.ListenAndServe(":4242", handler)
Verbose
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

config := sloghttp.Config{
	WithRequestBody: true,
	WithResponseBody: true,
	WithRequestHeader: true,
	WithResponseHeader: true,
}

mux := http.NewServeMux()

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.NewWithConfig(logger, config)(handler)

// Start server
http.ListenAndServe(":4242", handler)
Filters
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

mux := http.NewServeMux()

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.NewWithFilters(
	logger,
	sloghttp.Accept(func (w sloghttp.WrapResponseWriter, r *http.Request) bool {
		return xxx
	}),
	sloghttp.IgnoreStatus(401, 404),
)(handler)

// Start server
http.ListenAndServe(":4242", handler)

Available filters:

  • Accept / Ignore
  • AcceptMethod / IgnoreMethod
  • AcceptStatus / IgnoreStatus
  • AcceptStatusGreaterThan / IgnoreStatusGreaterThan
  • AcceptStatusLessThan / IgnoreStatusLessThan
  • AcceptStatusGreaterThanOrEqual / IgnoreStatusGreaterThanOrEqual
  • AcceptStatusLessThanOrEqual / IgnoreStatusLessThanOrEqual
  • AcceptPath / IgnorePath
  • AcceptPathContains / IgnorePathContains
  • AcceptPathPrefix / IgnorePathPrefix
  • AcceptPathSuffix / IgnorePathSuffix
  • AcceptPathMatch / IgnorePathMatch
  • AcceptHost / IgnoreHost
  • AcceptHostContains / IgnoreHostContains
  • AcceptHostPrefix / IgnoreHostPrefix
  • AcceptHostSuffix / IgnoreHostSuffix
  • AcceptHostMatch / IgnoreHostMatch
Using custom time formatters
import (
	"net/http"
	"log/slog"

	sloghttp "github.com/samber/slog-http"
	slogformatter "github.com/samber/slog-formatter"
)

// Create a slog logger, which:
//   - Logs to stdout.
//   - RFC3339 with UTC time format.
logger := slog.New(
	slogformatter.NewFormatterHandler(
		slogformatter.TimezoneConverter(time.UTC),
		slogformatter.TimeFormatter(time.DateTime, nil),
	)(
		slog.NewTextHandler(os.Stdout, nil),
	),
)

// mux router
mux := http.NewServeMux()

// Routes
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}))
mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	http.Error(w, "I'm angry" http.StatusInternalServerError)
}))

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.New(logger)(handler)

// Start server
http.ListenAndServe(":4242", handler)

// output:
// time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Success" env=production request.time=2023-10-15T20:32:58Z request.method=GET request.path=/ request.route="" request.ip=127.0.0.1:63932 request.length=0 response.time=2023-10-15T20:32:58Z response.latency=100ms response.status=200 response.length=7 id=229c7fc8-64f5-4467-bc4a-940700503b0d
Using custom logger sub-group
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

// mux router
mux := http.NewServeMux()

// Routes
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}))
mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	http.Error(w,  "I'm angry" http.StatusInternalServerError)
}))

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.New(logger.WithGroup("http"))(handler)

// Start server
http.ListenAndServe(":4242", handler)

// output:
// time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Success" env=production http.request.time=2023-10-15T20:32:58.626+02:00 http.request.method=GET http.request.path=/ http.request.route="" http.request.ip=127.0.0.1:63932 http.request.length=0 http.response.time=2023-10-15T20:32:58.926+02:00 http.response.latency=100ms http.response.status=200 http.response.length=7 http.id=229c7fc8-64f5-4467-bc4a-940700503b0d
Add logger to a single route
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

// mux router
mux := http.NewServeMux()

// Routes
mux.Handler("/", sloghttp.New(logger)(
	http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			return c.String(http.StatusOK, "Hello, World!")
		},
	),
	sloghttp.New(logger),
))

// Start server
http.ListenAndServe(":4242", handler)
Adding custom attributes
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

// Add an attribute to all log entries made through this logger.
logger = logger.With("env", "production")

// mux router
mux := http.NewServeMux()

// Routes
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	sloghttp.AddCustomAttributes(r, slog.String("foo", "bar"))
	w.Write([]byte("Hello, World!"))
}))

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.New(logger)(handler)

// Start server
http.ListenAndServe(":4242", handler)

// output:
// time=2023-10-15T20:32:58.926+02:00 level=INFO msg="Success" env=production request.time=2023-10-15T20:32:58.626+02:00 request.method=GET request.path=/ request.route="" request.ip=127.0.0.1:63932 request.length=0 response.time=2023-10-15T20:32:58.926+02:00 response.latency=100ms response.status=200 response.length=7 id=229c7fc8-64f5-4467-bc4a-940700503b0d foo=bar
JSON output
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

// mux router
mux := http.NewServeMux()

// Routes
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}))

// Middleware
handler := sloghttp.Recovery(mux)
handler = sloghttp.New(logger)(handler)

// Start server
http.ListenAndServe(":4242", handler)

// output:
// {"time":"2023-10-15T20:32:58.926+02:00","level":"INFO","msg":"Success","env":"production","http":{"request":{"time":"2023-10-15T20:32:58.626+02:00","method":"GET","path":"/","route":"","ip":"127.0.0.1:55296","length":0},"response":{"time":"2023-10-15T20:32:58.926+02:00","latency":100000,"status":200,"length":7},"id":"04201917-d7ba-4b20-a3bb-2fffba5f2bd9"}}

🤝 Contributing

Don't hesitate ;)

# Install some dev dependencies
make tools

# Run tests
make test
# or
make watch-test

👤 Contributors

Contributors

💫 Show your support

Give a ⭐️ if this project helped you!

GitHub Sponsors

📝 License

Copyright © 2023 Samuel Berthe.

This project is MIT licensed.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	TraceIDKey   = "trace_id"
	SpanIDKey    = "span_id"
	RequestIDKey = "id"

	RequestBodyMaxSize  = 64 * 1024 // 64KB
	ResponseBodyMaxSize = 64 * 1024 // 64KB

	HiddenRequestHeaders = map[string]struct{}{
		"authorization": {},
		"cookie":        {},
		"set-cookie":    {},
		"x-auth-token":  {},
		"x-csrf-token":  {},
		"x-xsrf-token":  {},
	}
	HiddenResponseHeaders = map[string]struct{}{
		"set-cookie": {},
	}

	// Formatted with http.CanonicalHeaderKey
	RequestIDHeaderKey = "X-Request-Id"
)

Functions

func AddContextAttributes added in v1.7.0

func AddContextAttributes(ctx context.Context, attrs ...slog.Attr)

AddContextAttributes is the same as AddCustomAttributes, but it doesn't need access to the request struct.

func AddCustomAttributes

func AddCustomAttributes(r *http.Request, attrs ...slog.Attr)

AddCustomAttributes adds custom attributes to the request context. This func can be called from any handler or middleware, as long as the slog-http middleware is already mounted.

func GetRequestID added in v1.2.0

func GetRequestID(r *http.Request) string

GetRequestID returns the request identifier.

func GetRequestIDFromContext added in v1.4.0

func GetRequestIDFromContext(ctx context.Context) string

GetRequestIDFromContext returns the request identifier from the context.

func New

func New(logger *slog.Logger) func(http.Handler) http.Handler

New returns a `func(http.Handler) http.Handler` (middleware) that logs requests using slog.

Requests with errors are logged using slog.Error(). Requests without errors are logged using slog.Info().

func NewWithConfig

func NewWithConfig(logger *slog.Logger, config Config) func(http.Handler) http.Handler

NewWithConfig returns a `func(http.Handler) http.Handler` (middleware) that logs requests using slog.

func NewWithFilters

func NewWithFilters(logger *slog.Logger, filters ...Filter) func(http.Handler) http.Handler

NewWithFilters returns a `func(http.Handler) http.Handler` (middleware) that logs requests using slog.

Requests with errors are logged using slog.Error(). Requests without errors are logged using slog.Info().

func Recovery

func Recovery(next http.Handler) http.Handler

Types

type Config

type Config struct {
	DefaultLevel     slog.Level
	ClientErrorLevel slog.Level
	ServerErrorLevel slog.Level

	WithUserAgent      bool
	WithRequestID      bool
	WithRequestBody    bool
	WithRequestHeader  bool
	WithResponseBody   bool
	WithResponseHeader bool
	WithSpanID         bool
	WithTraceID        bool
	WithClientIP       bool
	WithCustomMessage  func(w http.ResponseWriter, r *http.Request) string

	Filters []Filter
}

func DefaultConfig added in v1.10.0

func DefaultConfig() Config

DefaultConfig returns the default configuration for the request logger.

type Filter

type Filter func(w WrapResponseWriter, r *http.Request) bool

func Accept

func Accept(filter Filter) Filter

Basic

func AcceptHost

func AcceptHost(hosts ...string) Filter

Host

func AcceptHostContains

func AcceptHostContains(parts ...string) Filter

func AcceptHostMatch

func AcceptHostMatch(regs ...regexp.Regexp) Filter

func AcceptHostPrefix

func AcceptHostPrefix(prefixs ...string) Filter

func AcceptHostSuffix

func AcceptHostSuffix(prefixs ...string) Filter

func AcceptMethod

func AcceptMethod(methods ...string) Filter

Method

func AcceptPath

func AcceptPath(urls ...string) Filter

Path

func AcceptPathContains

func AcceptPathContains(parts ...string) Filter

func AcceptPathMatch

func AcceptPathMatch(regs ...regexp.Regexp) Filter

func AcceptPathPrefix

func AcceptPathPrefix(prefixs ...string) Filter

func AcceptPathSuffix

func AcceptPathSuffix(prefixs ...string) Filter

func AcceptStatus

func AcceptStatus(statuses ...int) Filter

Status

func AcceptStatusGreaterThan

func AcceptStatusGreaterThan(status int) Filter

func AcceptStatusGreaterThanOrEqual

func AcceptStatusGreaterThanOrEqual(status int) Filter

func AcceptStatusLessThan added in v1.5.1

func AcceptStatusLessThan(status int) Filter

func AcceptStatusLessThanOrEqual added in v1.5.1

func AcceptStatusLessThanOrEqual(status int) Filter

func Ignore

func Ignore(filter Filter) Filter

func IgnoreHost

func IgnoreHost(hosts ...string) Filter

func IgnoreHostContains

func IgnoreHostContains(parts ...string) Filter

func IgnoreHostMatch

func IgnoreHostMatch(regs ...regexp.Regexp) Filter

func IgnoreHostPrefix

func IgnoreHostPrefix(prefixs ...string) Filter

func IgnoreHostSuffix

func IgnoreHostSuffix(suffixs ...string) Filter

func IgnoreMethod

func IgnoreMethod(methods ...string) Filter

func IgnorePath

func IgnorePath(urls ...string) Filter

func IgnorePathContains

func IgnorePathContains(parts ...string) Filter

func IgnorePathMatch

func IgnorePathMatch(regs ...regexp.Regexp) Filter

func IgnorePathPrefix

func IgnorePathPrefix(prefixs ...string) Filter

func IgnorePathSuffix

func IgnorePathSuffix(suffixs ...string) Filter

func IgnoreStatus

func IgnoreStatus(statuses ...int) Filter

func IgnoreStatusGreaterThan added in v1.5.1

func IgnoreStatusGreaterThan(status int) Filter

func IgnoreStatusGreaterThanOrEqual added in v1.5.1

func IgnoreStatusGreaterThanOrEqual(status int) Filter

func IgnoreStatusLessThan

func IgnoreStatusLessThan(status int) Filter

func IgnoreStatusLessThanOrEqual

func IgnoreStatusLessThanOrEqual(status int) Filter

type WrapResponseWriter

type WrapResponseWriter interface {
	http.ResponseWriter
	http.Flusher
	http.Hijacker

	Status() int
	BytesWritten() int
	Body() []byte
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL