httplog

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Jun 4, 2025 License: MIT Imports: 10 Imported by: 1

README

httplog

Structured HTTP request logging middleware for Go, built on the standard library log/slog package

Go Reference Go Report Card MIT License

httplog is a lightweight, high-performance HTTP request logging middleware for Go web applications. Built on Go 1.21+'s standard log/slog package, it provides structured logging with zero external dependencies.

Features

  • 🚀 High Performance: Minimal overhead
  • 📋 Structured Logging: Built on Go's standard log/slog package
  • 🎯 Smart Log Levels: Auto-assigns levels by status code (5xx = error, 4xx = warn)
  • 📊 Schema Support: Compatible with ECS, OTEL, and GCP logging formats
  • 🛡️ Panic Recovery: Recovers panics with stack traces and HTTP 500 responses
  • 🔍 Body Logging: Conditional request/response body capture with content-type filtering
  • 📝 Custom Attributes: Add log attributes from handlers and middlewares
  • 🎨 Developer Friendly: Concise mode and curl command generation
  • 🔗 Router Agnostic: Works with Chi, Gin, Echo, and standard http.ServeMux

Usage

go get github.com/golang-cz/httplog@latest

package main

import (
	"errors"
	"fmt"
	"log"
	"log/slog"
	"net/http"
	"os"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/golang-cz/httplog"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With(
		slog.String("app", "example-app"),
		slog.String("version", "v1.0.0-a1fa420"),
		slog.String("env", "production"),
	)

	r := chi.NewRouter()

	// Request logger
	r.Use(httplog.RequestLogger(logger, &httplog.Options{
		// Level defines the verbosity of the request logs:
		// slog.LevelDebug - log all responses (incl. OPTIONS)
		// slog.LevelInfo  - log responses (excl. OPTIONS)
		// slog.LevelWarn  - log 4xx and 5xx responses only (except for 429)
		// slog.LevelError - log 5xx responses only
		Level: slog.LevelInfo,

		// Set log output to Elastic Common Schema (ECS) format.
		Schema: httplog.SchemaECS,

		// RecoverPanics recovers from panics occurring in the underlying HTTP handlers
		// and middlewares. It returns HTTP 500 unless response status was already set.
		//
		// NOTE: Panics are logged as errors automatically, regardless of this setting.
		RecoverPanics: true,

		// Optionally, filter out some request logs.
		Skip: func(req *http.Request, respStatus int) bool {
			return respStatus == 404 || respStatus == 405 || respStatus == 429
		},

		// Optionally, log selected request/response headers explicitly.
		LogRequestHeaders:  []string{"Origin"},
		LogResponseHeaders: []string{},

		// Optionally, enable logging of request/response body based on custom conditions.
		// Useful for debugging payload issues in development.
		LogRequestBody:  isDebugHeaderSet,
		LogResponseBody: isDebugHeaderSet,
	}))

	// Set request log attribute from within middleware.
	r.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := r.Context()

			httplog.SetAttrs(ctx, slog.String("user", "user1"))

			next.ServeHTTP(w, r.WithContext(ctx))
		})
	})

	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello world \n"))
	})

	http.ListenAndServe("localhost:8000", r)
}

func isDebugHeaderSet(r *http.Request) bool {
	return r.Header.Get("Debug") == "reveal-body-logs"
}

Example

See _example/main.go and try it locally:

$ cd _example

# JSON logger (production)
$ go run .

# Pretty logger (localhost)
$ ENV=localhost go run .

License

MIT license

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// SchemaECS represents the Elastic Common Schema (SchemaECS) version 9.0.0.
	// This schema is widely used with Elasticsearch and the Elastic Stack.
	//
	// Reference: https://www.elastic.co/guide/en/ecs/current/ecs-http.html
	SchemaECS = Schema{
		Timestamp:          "@timestamp",
		Level:              "log.level",
		Message:            "message",
		Error:              "error.message",
		ErrorStackTrace:    "error.stack_trace",
		RequestURL:         "url.full",
		RequestMethod:      "http.request.method",
		RequestPath:        "url.path",
		RequestRemoteIP:    "client.ip",
		RequestHost:        "url.domain",
		RequestScheme:      "url.scheme",
		RequestProto:       "http.version",
		RequestHeaders:     "http.request.headers",
		RequestBody:        "http.request.body.content",
		RequestBytes:       "http.request.body.bytes",
		RequestBytesUnread: "http.request.body.unread.bytes",
		RequestUserAgent:   "user_agent.original",
		RequestReferer:     "http.request.referrer",
		ResponseHeaders:    "http.response.headers",
		ResponseBody:       "http.response.body.content",
		ResponseStatus:     "http.response.status_code",
		ResponseDuration:   "event.duration",
		ResponseBytes:      "http.response.body.bytes",
	}

	// SchemaOTEL represents OpenTelemetry (SchemaOTEL) semantic conventions version 1.34.0.
	// This schema follows OpenTelemetry standards for observability data.
	//
	// Reference: https://opentelemetry.io/docs/specs/semconv/http/http-metrics
	SchemaOTEL = Schema{
		Timestamp:          "timestamp",
		Level:              "severity_text",
		Message:            "body",
		Error:              "exception.message",
		ErrorStackTrace:    "exception.stacktrace",
		RequestURL:         "url.full",
		RequestMethod:      "http.request.method",
		RequestPath:        "url.path",
		RequestRemoteIP:    "client.address",
		RequestHost:        "server.address",
		RequestScheme:      "url.scheme",
		RequestProto:       "network.protocol.version",
		RequestHeaders:     "http.request.header",
		RequestBody:        "http.request.body.content",
		RequestBytes:       "http.request.body.size",
		RequestBytesUnread: "http.request.body.unread.size",
		RequestUserAgent:   "user_agent.original",
		RequestReferer:     "http.request.header.referer",
		ResponseHeaders:    "http.response.header",
		ResponseBody:       "http.response.body.content",
		ResponseStatus:     "http.response.status_code",
		ResponseDuration:   "http.server.request.duration",
		ResponseBytes:      "http.response.body.size",
	}

	// SchemaGCP represents Google Cloud Platform's structured logging format.
	// This schema is optimized for Google Cloud Logging service.
	//
	// References:
	//   - https://cloud.google.com/logging/docs/structured-logging
	//   - https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest
	SchemaGCP = Schema{
		Timestamp:          "timestamp",
		Level:              "severity",
		Message:            "message",
		Error:              "error",
		ErrorStackTrace:    "stack_trace",
		RequestURL:         "httpRequest.requestUrl",
		RequestMethod:      "httpRequest.requestMethod",
		RequestPath:        "httpRequest.requestPath",
		RequestRemoteIP:    "httpRequest.remoteIp",
		RequestHost:        "httpRequest.host",
		RequestScheme:      "httpRequest.scheme",
		RequestProto:       "httpRequest.protocol",
		RequestHeaders:     "httpRequest.requestHeaders",
		RequestBody:        "httpRequest.requestBody",
		RequestBytes:       "httpRequest.requestSize",
		RequestBytesUnread: "httpRequest.requestUnreadSize",
		RequestUserAgent:   "httpRequest.userAgent",
		RequestReferer:     "httpRequest.referer",
		ResponseHeaders:    "httpRequest.responseHeaders",
		ResponseBody:       "httpRequest.responseBody",
		ResponseStatus:     "httpRequest.status",
		ResponseDuration:   "httpRequest.latency",
		ResponseBytes:      "httpRequest.responseSize",
		GroupDelimiter:     ".",
	}
)

Functions

func CURL

func CURL(req *http.Request, reqBody string) string

CURL returns a curl command for the given request and body.

func RequestLogger

func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Handler

func SetAttrs

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

Types

type Options

type Options struct {
	// Level defines the verbosity of the request logs:
	// slog.LevelDebug - log both request starts & responses (incl. OPTIONS)
	// slog.LevelInfo  - log responses (excl. OPTIONS)
	// slog.LevelWarn  - log 4xx and 5xx responses only (except for 429)
	// slog.LevelError - log 5xx responses only
	//
	// You can override the level with a custom slog.Handler, e.g. on per-request basis.
	Level slog.Level

	// Schema defines the mapping of semantic log fields to their corresponding
	// field names in different logging systems and standards.
	//
	// This enables log output in different formats compatible with various logging
	// platforms and standards (ECS, OTEL, GCP, etc.) by providing the schema.
	//
	// httplog.SchemaECS (Elastic Common Schema)
	// httplog.SchemaOTEL (OpenTelemetry)
	// httplog.SchemaGCP (Google Cloud Platform)
	//
	// Append .Concise(true) to reduce log verbosity, e.g. for localhost development.
	Schema Schema

	// RecoverPanics recovers from panics occurring in the underlying HTTP handlers
	// and middlewares and returns HTTP 500 unless response status was already set.
	//
	// NOTE: Panics are logged as errors automatically, regardless of this setting.
	RecoverPanics bool

	// Skip is an optional predicate function that determines whether to skip
	// recording logs for a given request.
	//
	// If nil, all requests are recorded.
	// If provided, requests where Skip returns true will not be recorded.
	Skip func(req *http.Request, respStatus int) bool

	// LogRequestHeaders is a list of headers to be logged as attributes.
	// If not provided, the default is ["Content-Type", "Origin"].
	//
	// WARNING: Do not leak any request headers with sensitive information.
	LogRequestHeaders []string

	// LogRequestBody is an optional predicate function that controls logging of request body.
	//
	// If the function returns true, the request body will be logged.
	// If false, no request body will be logged.
	//
	// WARNING: Do not leak any request bodies with sensitive information.
	LogRequestBody func(req *http.Request) bool

	// LogResponseHeaders controls a list of headers to be logged as attributes.
	//
	// If not provided, there are no default headers.
	LogResponseHeaders []string

	// LogRequestBody is an optional predicate function that controls logging of request body.
	//
	// If the function returns true, the request body will be logged.
	// If false, no request body will be logged.
	//
	// WARNING: Do not leak any response bodies with sensitive information.
	LogResponseBody func(req *http.Request) bool

	// LogBodyContentTypes defines a list of body Content-Types that are safe to be logged
	// with LogRequestBody or LogResponseBody options.
	//
	// If not provided, the default is ["application/json", "application/xml", "text/plain", "text/csv", "application/x-www-form-urlencoded", ""].
	LogBodyContentTypes []string

	// LogBodyMaxLen defines the maximum length of the body to be logged.
	//
	// If not provided, the default is 1024 bytes. Set to -1 to log the full body.
	LogBodyMaxLen int

	// LogExtraAttrs is an optional function that lets you add extra attributes to the
	// request log.
	//
	// Example:
	//
	// // Log all requests with invalid payload as curl command.
	// func(req *http.Request, reqBody string, respStatus int) []slog.Attr {
	//     if respStatus == 400 || respStatus == 422 {
	// 	       req.Header.Del("Authorization")
	//         return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))}
	// 	   }
	// 	   return nil
	// }
	//
	// WARNING: Be careful not to leak any sensitive information in the logs.
	LogExtraAttrs func(req *http.Request, reqBody string, respStatus int) []slog.Attr
}

type Schema

type Schema struct {
	// Base attributes for core logging information.
	Timestamp       string // Timestamp of the log entry
	Level           string // Log level (info, warn, error, etc.)
	Message         string // Primary log message
	Error           string // Error message when an error occurs
	ErrorStackTrace string // Stack trace for errors

	// Request attributes for the incoming HTTP request.
	// NOTE: RequestQuery is intentionally not supported as it would likely leak sensitive data.
	RequestURL         string // Full request URL
	RequestMethod      string // HTTP method (GET, POST, etc.)
	RequestPath        string // URL path component
	RequestRemoteIP    string // Client IP address
	RequestHost        string // Host header value
	RequestScheme      string // URL scheme (http, https)
	RequestProto       string // HTTP protocol version (HTTP/1.1, HTTP/2, etc.)
	RequestHeaders     string // Selected request headers
	RequestBody        string // Request body content, if logged.
	RequestBytes       string // Size of request body in bytes
	RequestBytesUnread string // Unread bytes in request body
	RequestUserAgent   string // User-Agent header value
	RequestReferer     string // Referer header value

	// Response attributes for the HTTP response.
	ResponseHeaders  string // Selected response headers
	ResponseBody     string // Response body content, if logged.
	ResponseStatus   string // HTTP status code
	ResponseDuration string // Request processing duration
	ResponseBytes    string // Size of response body in bytes

	// GroupDelimiter is an optional delimiter for nested objects in some formats.
	// For example, GCP uses nested JSON objects like "httpRequest": {}.
	GroupDelimiter string
}

Schema defines the mapping of semantic log fields to their corresponding field names in different logging systems and standards.

This enables log output in different formats compatible with various logging platforms and standards (ECS, OTEL, GCP, etc.) by providing the schema.

func (Schema) Concise

func (s Schema) Concise(concise bool) Schema

Concise returns a simplified schema with essential fields only. When concise is true, it reduces log output by keeping only error, request, and response details.

This is useful for localhost development to reduce log verbosity.

Jump to

Keyboard shortcuts

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