Documentation
¶
Overview ¶
Package httpserver provides an HTTP server that implements app.Component, allowing it to be plugged into an app.App and have its lifecycle managed alongside the logger and tracer.
Consumers bring their own router (standard library http.ServeMux, chi, or any http.Handler) and pass it via [Config.Handler]. LabKit does not own the routing layer; its value is in the built-in middleware that wraps the handler automatically:
- Tracing: extracts incoming W3C trace context (traceparent / tracestate), creates a server span named "METHOD /pattern", and records the response status. Use SetRoutePattern from a router-specific middleware to get low-cardinality span names.
- Access logging: emits a structured log entry per request (message "access") with correlation_id, method, uri, status, duration_s, system, host, proto, remote_addr, remote_ip, referrer, user_agent, written_bytes, content_type, and ttfb_s. Sensitive query parameters and URL userinfo are masked automatically. X-Forwarded-For is honored to surface the real client IP.
AccessLogger is also exported for standalone use -- wrap any http.Handler directly when you need access logging outside of a Server.
Basic usage with standard library ServeMux ¶
mux := http.NewServeMux()
mux.HandleFunc("GET /api/projects/{id}", getProject)
mux.HandleFunc("POST /api/projects", createProject)
a, err := app.New(ctx)
if err != nil { log.Fatal(err) }
srv := httpserver.NewWithConfig(&httpserver.Config{
Addr: ":8080",
Logger: a.Logger(),
Tracer: a.Tracer(),
Handler: mux,
})
a.Register(srv)
if err := a.Start(ctx); err != nil { log.Fatal(err) }
defer a.Shutdown(ctx)
Using chi for advanced routing ¶
For services that need route grouping, scoped middleware, or sub-router mounting, chi is recommended:
r := chi.NewRouter()
r.Use(requireAuth)
r.Get("/api/users", listUsers)
r.Route("/api/v2", func(r chi.Router) {
r.Get("/projects", listProjects)
r.Get("/projects/{id}", getProject)
})
srv := httpserver.NewWithConfig(&httpserver.Config{
Addr: ":8080",
Handler: r,
})
Path parameters ¶
Use URLParam to extract named path parameters. This works with both the standard library ServeMux (Go 1.22+) and chi:
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := httpserver.URLParam(r, "id")
// ...
})
Low-cardinality span names ¶
The tracing middleware names spans "METHOD /path" by default. To get low-cardinality names like "GET /users/{id}" instead of "GET /users/42", set the route pattern via SetRoutePattern in a router-specific middleware:
// Example for chi:
func chiRoutePattern(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
rctx := chi.RouteContext(r.Context())
if rctx != nil {
r = r.WithContext(httpserver.SetRoutePattern(r.Context(), rctx.RoutePattern()))
}
})
}
Standalone access logger ¶
AccessLogger can wrap any http.Handler when the full Server is not needed -- for example to add access logging to a plain http.ServeMux:
logger := log.New()
http.Handle("/api/", httpserver.AccessLogger(apiMux, logger))
Testing ¶
Server implements http.Handler, so tests can call ServeHTTP directly to exercise handlers without binding a real port:
mux := http.NewServeMux()
mux.HandleFunc("GET /ping", pingHandler)
srv := httpserver.NewWithConfig(&httpserver.Config{Handler: mux})
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
Example ¶
Example shows a server wired into an app.App lifecycle with a standard library ServeMux and a readiness check.
package main
import (
"context"
"fmt"
"net/http"
"gitlab.com/gitlab-org/labkit/v2/httpserver"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/projects/{id}", func(w http.ResponseWriter, r *http.Request) {
id := httpserver.URLParam(r, "id")
fmt.Fprintf(w, `{"id": "%s"}`, id)
})
mux.HandleFunc("GET /api/v2/ping", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
srv := httpserver.NewWithConfig(&httpserver.Config{
Addr: ":8080",
Handler: mux,
// Logger: a.Logger(),
// Tracer: a.Tracer(),
})
ctx := context.Background()
if err := srv.Start(ctx); err != nil {
panic(err)
}
defer srv.Shutdown(ctx) //nolint:errcheck
}
Output:
Index ¶
- func AccessLogger(next http.Handler, logger *slog.Logger) http.Handler
- func CorrelationIDMiddleware(next http.Handler) http.Handler
- func PanicRecoveryMiddleware(logger *slog.Logger) func(http.Handler) http.Handler
- func RoutePattern(r *http.Request) string
- func SetRoutePattern(ctx context.Context, pattern string) context.Context
- func URLParam(r *http.Request, key string) string
- type CheckFunc
- type Config
- type Server
- func (s *Server) AddReadinessCheck(name string, fn CheckFunc) *Server
- func (s *Server) Addr() string
- func (s *Server) Name() string
- func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Server) Shutdown(ctx context.Context) error
- func (s *Server) Start(ctx context.Context) error
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AccessLogger ¶
AccessLogger returns HTTP middleware that emits a structured access log entry for each request. If logger is nil, slog.Default() is used.
Example ¶
ExampleAccessLogger shows how to wrap any http.Handler with structured access logging when the full Server is not needed.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"gitlab.com/gitlab-org/labkit/v2/httpserver"
"gitlab.com/gitlab-org/labkit/v2/log"
)
func main() {
logger := log.New()
handler := httpserver.AccessLogger(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
}),
logger,
)
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
fmt.Println(rec.Code)
fmt.Println(rec.Body.String())
}
Output: 200 ok
func CorrelationIDMiddleware ¶
CorrelationIDMiddleware reads the X-Request-ID header from each incoming request and injects it into the request context via correlation.InjectToContext. If the header is absent, empty, longer than 255 characters, or contains characters outside [a-zA-Z0-9_-], a new UUID is generated and used instead. This prevents log injection and downstream poisoning from malicious headers. The injected ID is available to all downstream handlers and middleware via correlation.ExtractFromContext. The resolved ID is also written back as an X-Request-ID response header. This middleware is applied automatically by NewWithConfig. Use it directly only when composing a handler stack outside of the standard Server.
func PanicRecoveryMiddleware ¶
PanicRecoveryMiddleware recovers from panics in downstream handlers, logs a structured error entry with the panic value and stack trace, records the panic on the active OTel span (if any), and writes a 500 Internal Server Error response to the client. If logger is nil, slog.Default() is used.
func RoutePattern ¶
RoutePattern returns the matched route pattern from the request context, or an empty string if no pattern has been set. The tracing middleware uses this to produce low-cardinality span names.
func SetRoutePattern ¶
SetRoutePattern records the matched route pattern in the request context for use by the tracing middleware. Routers that support route patterns should call this in a middleware so that span names use a low-cardinality pattern (e.g. "/users/{id}") instead of the concrete URL path.
When called inside a Server's middleware chain, SetRoutePattern mutates a shared holder that the tracing middleware reads after the handler returns. The returned context is the same as the input (no allocation needed).
Example chi middleware:
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
rctx := chi.RouteContext(r.Context())
if rctx != nil {
httpserver.SetRoutePattern(r.Context(), rctx.RoutePattern())
}
})
})
Types ¶
type CheckFunc ¶
CheckFunc is a health check function. It should return nil when the dependency is healthy and a descriptive error when it is not. The context carries the deadline of the incoming probe request, so CheckFunc implementations should honour it and return promptly.
type Config ¶
type Config struct {
// Name identifies this server in logs and errors. Defaults to "httpserver".
// Use distinct names when running multiple servers (e.g. "api", "admin").
Name string
// Addr is the TCP address to listen on, e.g. ":8080" or "127.0.0.1:9000".
// Use ":0" to let the OS pick a free port (useful in tests).
// Defaults to ":8080".
Addr string
// ReadTimeout is the maximum duration for reading the entire request,
// including the body. Defaults to 5s.
ReadTimeout time.Duration
// WriteTimeout is the maximum duration before timing out the response write.
// Defaults to 10s.
WriteTimeout time.Duration
// IdleTimeout is the maximum duration to wait for the next request on a
// keep-alive connection. Defaults to 60s.
IdleTimeout time.Duration
// ShutdownTimeout is the maximum duration allowed for graceful shutdown
// before in-flight requests are forcibly terminated. Defaults to 30s.
ShutdownTimeout time.Duration
// Logger is used for structured access logs. When nil, access logging is
// disabled.
Logger *slog.Logger
// Tracer is used to create server spans and extract incoming W3C trace
// context. When nil, request tracing is disabled.
Tracer *trace.Tracer
// Metrics exposes the Prometheus registry at /-/metrics alongside the
// built-in liveness and readiness endpoints. When nil, /-/metrics is not
// registered and callers must mount the handler themselves via
// [metrics.Metrics.MountOn].
Metrics *metrics.Metrics
// Handler is the application's HTTP handler (typically an [http.ServeMux]
// or a third-party router such as chi). When nil, all non-health requests
// return 404.
//
// Built-in middleware (tracing, access logging) wraps this handler
// automatically. Consumers do not need to add LabKit middleware manually.
Handler http.Handler
}
Config holds optional configuration for New / NewWithConfig. Zero values produce sensible defaults.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is an HTTP server that implements app.Component. Create routes on your own http.ServeMux or router, pass it as [Config.Handler], then plug the Server into an app.App via Register and call app.App.Start to begin serving.
Built-in middleware (tracing, logging) wraps the handler automatically. Health endpoints (/-/liveness, /-/readiness) are handled by the Server directly, bypassing the application handler.
func NewWithConfig ¶
NewWithConfig returns a Server configured with cfg. A nil cfg is treated identically to an empty Config (all defaults).
func (*Server) AddReadinessCheck ¶
AddReadinessCheck registers a named check that is run on every request to /-/readiness. If the check returns an error the endpoint responds with 503 Service Unavailable and includes the error message in the JSON body.
All checks must be registered before [Start] is called.
srv.AddReadinessCheck("database", func(ctx context.Context) error {
return db.PingContext(ctx)
})
Example ¶
ExampleServer_AddReadinessCheck shows how to register a dependency health check. The /-/readiness endpoint runs all checks concurrently.
package main
import (
"context"
"gitlab.com/gitlab-org/labkit/v2/httpserver"
)
func main() {
srv := httpserver.New()
srv.AddReadinessCheck("database", func(ctx context.Context) error {
// Replace with: return db.PingContext(ctx)
return nil
})
srv.AddReadinessCheck("cache", func(ctx context.Context) error {
// Replace with: return cache.Ping(ctx)
return nil
})
_ = srv
}
Output:
func (*Server) Addr ¶
Addr returns the network address the server is listening on. Returns an empty string if [Start] has not been called yet. Use this after starting with Addr ":0" to discover the bound port.
func (*Server) ServeHTTP ¶
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP dispatches the request through built-in middleware and health endpoints, falling through to the application handler for all other routes. This implements http.Handler so the Server can be used directly in tests without calling [Start].
Example ¶
ExampleServer_ServeHTTP shows how to test handlers without binding a real port. Calling ServeHTTP directly is the fastest way to exercise routes.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"gitlab.com/gitlab-org/labkit/v2/httpserver"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /ping", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "pong")
})
srv := httpserver.NewWithConfig(&httpserver.Config{
Handler: mux,
})
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, req)
fmt.Println(rec.Code)
fmt.Println(rec.Body.String())
}
Output: 200 pong