vital

package module
v0.0.0-...-11faf80 Latest Latest
Warning

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

Go to latest
Published: Feb 27, 2026 License: MIT Imports: 20 Imported by: 0

README

Vital

Production-ready HTTP server utilities for Go with built-in observability, health checks, and middleware.

Features

  • Server Management: Graceful shutdown, TLS support, configurable timeouts
  • Health Checks: Liveness and readiness endpoints with custom checkers
  • Middleware: Timeout, OpenTelemetry, request logging, recovery, basic auth
  • Error Responses: RFC 9457 ProblemDetail for consistent error handling
  • Structured Logging: Context-aware logging with trace correlation

Installation

go get github.com/monkescience/vital

Quick Start

package main

import (
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/monkescience/vital"
)

func main() {
	// Create logger with context support
	logger := slog.New(vital.NewContextHandler(
		slog.NewJSONHandler(os.Stdout, nil),
		vital.WithBuiltinKeys(),
	))
	slog.SetDefault(logger)

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

	// Add health checks
	mux.Handle("/", vital.NewHealthHandler(
		vital.WithVersion("1.0.0"),
		vital.WithEnvironment("production"),
	))

	// Add your routes
	mux.HandleFunc("GET /api/hello", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})

	// Wrap with middleware
	handler := vital.Recovery(logger)(
		vital.RequestLogger(logger)(
			vital.Timeout(30 * time.Second)(mux),
		),
	)

	// Create and run server
	server := vital.NewServer(handler,
		vital.WithPort(8080),
		vital.WithLogger(logger),
	)

	server.Run()
}

Test the server:

curl http://localhost:8080/health/live
curl http://localhost:8080/health/ready
curl http://localhost:8080/api/hello

Server Configuration

Create a server with functional options:

server := vital.NewServer(handler,
	vital.WithPort(8080),
	vital.WithTLS("cert.pem", "key.pem"),
	vital.WithShutdownTimeout(30 * time.Second),
	vital.WithReadTimeout(10 * time.Second),
	vital.WithWriteTimeout(10 * time.Second),
	vital.WithIdleTimeout(120 * time.Second),
	vital.WithLogger(logger),
)

// Start server (blocks until shutdown signal)
server.Run()

// Or manage lifecycle manually
go server.Start()
// ... do other work ...
server.Stop()
Server Options
Option Description Default
WithPort(port) Set server port Required
WithTLS(cert, key) Enable TLS with certificate paths Disabled
WithShutdownTimeout(d) Graceful shutdown timeout 20s
WithReadTimeout(d) Maximum duration for reading request 10s
WithWriteTimeout(d) Maximum duration for writing response 10s
WithIdleTimeout(d) Maximum idle time between requests 120s
WithLogger(logger) Set structured logger slog.Default()

Health Checks

Basic Health Endpoints
// Simple health handler with version and environment
healthHandler := vital.NewHealthHandler(
	vital.WithVersion("1.0.0"),
	vital.WithEnvironment("production"),
)

mux.Handle("/", healthHandler)

This creates two endpoints:

  • GET /health/live - Liveness probe (always returns 200 OK)
  • GET /health/ready - Readiness probe (runs health checks)
Custom Health Checkers

Implement the Checker interface for custom health checks:

type DatabaseChecker struct {
	db *sql.DB
}

func (c *DatabaseChecker) Name() string {
	return "database"
}

func (c *DatabaseChecker) Check(ctx context.Context) (vital.Status, string) {
	if err := c.db.PingContext(ctx); err != nil {
		return vital.StatusError, err.Error()
	}
	return vital.StatusOK, "connected"
}

// Add to health handler
healthHandler := vital.NewHealthHandler(
	vital.WithVersion("1.0.0"),
	vital.WithCheckers(&DatabaseChecker{db: db}),
	vital.WithReadyOptions(
		vital.WithOverallReadyTimeout(5 * time.Second),
	),
)
Health Check Response Format

Liveness response:

{
  "status": "ok"
}

Readiness response:

{
  "status": "ok",
  "version": "1.0.0",
  "environment": "production",
  "checks": [
    {
      "name": "database",
      "status": "ok",
      "message": "connected",
      "duration": "2.5ms"
    }
  ]
}

Middleware

Timeout

Enforce request timeout with automatic error response:

handler := vital.Timeout(30 * time.Second)(mux)

If the handler exceeds the timeout, returns:

{
  "status": 503,
  "title": "Service Unavailable",
  "detail": "request timeout exceeded"
}
OpenTelemetry

Add distributed tracing and metrics:

vital.OTel validates metric instrument setup and returns an error if initialization fails.

import (
	"go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/sdk/metric"
)

// Setup providers
tp := trace.NewTracerProvider(...)
mp := metric.NewMeterProvider(...)

// Apply middleware
otelMiddleware, err := vital.OTel(
	vital.WithTracerProvider(tp),
	vital.WithMeterProvider(mp),
)
if err != nil {
	panic(err)
}

handler := otelMiddleware(mux)

Features:

  • Creates spans for each HTTP request
  • Propagates W3C traceparent headers
  • Records http.server.request.duration histogram
  • Adds trace_id and span_id to request context
Request Logger

Log all HTTP requests with structured logging:

handler := vital.RequestLogger(logger)(mux)

Logs include:

  • HTTP method and path
  • Status code
  • Request duration
  • Remote address and user agent
  • Trace context (if OTel middleware is used)

Example log output:

{
  "time": "2025-01-26T10:30:00Z",
  "level": "INFO",
  "msg": "http request",
  "method": "GET",
  "path": "/api/users",
  "status": 200,
  "duration": "15ms",
  "remote_addr": "192.168.1.1:54321",
  "user_agent": "curl/7.68.0",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7"
}
Recovery

Recover from panics and return 500 error:

handler := vital.Recovery(logger)(mux)

Catches panics, logs the error, and returns:

{
  "status": 500,
  "title": "Internal Server Error",
  "detail": "internal server error"
}
Basic Auth

Protect endpoints with HTTP Basic Authentication:

handler := vital.BasicAuth("admin", "secret", "Admin Area")(mux)

Uses constant-time comparison to prevent timing attacks.

Middleware Chaining

Chain multiple middleware together (applied right-to-left):

otelMiddleware, err := vital.OTel(
	vital.WithTracerProvider(tp),
	vital.WithMeterProvider(mp),
)
if err != nil {
	panic(err)
}

handler := vital.Recovery(logger)(
	vital.RequestLogger(logger)(
		otelMiddleware(vital.Timeout(30 * time.Second)(mux)),
	),
)

Recommended order (innermost to outermost):

  1. Timeout - enforce request deadlines
  2. OTel - trace and metrics
  3. RequestLogger - log requests
  4. Recovery - catch panics

Error Responses

Use RFC 9457 ProblemDetail for consistent error responses:

Standard Errors
// 400 Bad Request
vital.RespondProblem(r.Context(), w, vital.BadRequest("invalid input"))

// 401 Unauthorized
vital.RespondProblem(r.Context(), w, vital.Unauthorized("authentication required"))

// 403 Forbidden
vital.RespondProblem(r.Context(), w, vital.Forbidden("insufficient permissions"))

// 404 Not Found
vital.RespondProblem(r.Context(), w, vital.NotFound("user not found"))

// 409 Conflict
vital.RespondProblem(r.Context(), w, vital.Conflict("email already exists"))

// 422 Unprocessable Entity
vital.RespondProblem(r.Context(), w, vital.UnprocessableEntity("validation failed"))

// 500 Internal Server Error
vital.RespondProblem(r.Context(), w, vital.InternalServerError("database error"))

// 503 Service Unavailable
vital.RespondProblem(r.Context(), w, vital.ServiceUnavailable("service temporarily unavailable"))
Custom ProblemDetail
problem := vital.NewProblemDetail(
	http.StatusTeapot,
	"I'm a teapot",
	vital.WithType("https://example.com/errors/teapot"),
	vital.WithDetail("Cannot brew coffee, I'm a teapot"),
	vital.WithInstance("/api/coffee/123"),
	vital.WithExtension("retry_after", 300),
)

vital.RespondProblem(r.Context(), w, problem)

Response:

{
  "type": "https://example.com/errors/teapot",
  "title": "I'm a teapot",
  "status": 418,
  "detail": "Cannot brew coffee, I'm a teapot",
  "instance": "/api/coffee/123",
  "retry_after": 300
}

Structured Logging

Context-Aware Logger

Create a logger that automatically extracts trace context:

logger := slog.New(vital.NewContextHandler(
	slog.NewJSONHandler(os.Stdout, nil),
	vital.WithBuiltinKeys(), // Adds trace_id, span_id, trace_flags
))

slog.SetDefault(logger)
Custom Context Keys

Add your own context keys:

var UserIDKey = vital.ContextKey{Name: "user_id"}

logger := slog.New(vital.NewContextHandler(
	slog.NewJSONHandler(os.Stdout, nil),
	vital.WithBuiltinKeys(),
	vital.WithContextKeys(UserIDKey),
))

// In your handler
ctx := context.WithValue(r.Context(), UserIDKey, "user-123")
slog.InfoContext(ctx, "processing request") // Includes user_id in log
Logger Configuration

Create logger from configuration:

config := vital.LogConfig{
	Level:     "info",
	Format:    "json",
	AddSource: true,
}

handler, err := vital.NewHandlerFromConfig(config,
	vital.WithBuiltinKeys(),
)
if err != nil {
	log.Fatal(err)
}

logger := slog.New(handler)

Complete Example

package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/monkescience/vital"
	_ "github.com/lib/pq"
)

type DatabaseChecker struct {
	db *sql.DB
}

func (c *DatabaseChecker) Name() string {
	return "database"
}

func (c *DatabaseChecker) Check(ctx context.Context) (vital.Status, string) {
	if err := c.db.PingContext(ctx); err != nil {
		return vital.StatusError, err.Error()
	}
	return vital.StatusOK, "connected"
}

type CreateUserRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

func main() {
	// Setup logger
	logger := slog.New(vital.NewContextHandler(
		slog.NewJSONHandler(os.Stdout, nil),
		vital.WithBuiltinKeys(),
	))
	slog.SetDefault(logger)

	// Setup database
	db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
	if err != nil {
		logger.Error("failed to connect to database", slog.Any("error", err))
		os.Exit(1)
	}
	defer db.Close()

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

	// Health checks
	mux.Handle("/", vital.NewHealthHandler(
		vital.WithVersion("1.0.0"),
		vital.WithEnvironment("production"),
		vital.WithCheckers(&DatabaseChecker{db: db}),
	))

	// API routes
	mux.HandleFunc("POST /api/users", func(w http.ResponseWriter, r *http.Request) {
		var req CreateUserRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			vital.RespondProblem(r.Context(), w, vital.BadRequest(err.Error()))
			return
		}

		// Create user in database
		_, err = db.ExecContext(r.Context(),
			"INSERT INTO users (name, email) VALUES ($1, $2)",
			req.Name, req.Email,
		)
		if err != nil {
			logger.ErrorContext(r.Context(), "failed to create user", slog.Any("error", err))
			vital.RespondProblem(r.Context(), w, vital.InternalServerError("failed to create user"))
			return
		}

		w.WriteHeader(http.StatusCreated)
	})

	// Apply middleware
	handler := vital.Recovery(logger)(
		vital.RequestLogger(logger)(
			vital.Timeout(30 * time.Second)(mux),
		),
	)

	// Create and run server
	server := vital.NewServer(handler,
		vital.WithPort(8080),
		vital.WithLogger(logger),
	)

	logger.Info("starting server", slog.Int("port", 8080))
	server.Run()
}

Configuration Reference

Server Options
Option Type Default Description
WithPort int Required Server port
WithTLS string, string Disabled Certificate and key paths
WithShutdownTimeout time.Duration 20s Graceful shutdown timeout
WithReadTimeout time.Duration 10s Read timeout
WithWriteTimeout time.Duration 10s Write timeout
WithIdleTimeout time.Duration 120s Idle timeout
WithLogger *slog.Logger slog.Default() Structured logger
Health Check Options
Option Type Description
WithVersion string Version string in readiness response
WithEnvironment string Environment string in readiness response
WithCheckers ...Checker Custom health checkers
WithReadyOptions ...ReadyOption Readiness-specific options
Readiness Options
Option Type Default Description
WithOverallReadyTimeout time.Duration 2s Timeout for all checks
OTel Options
Option Type Default Description
WithTracerProvider trace.TracerProvider otel.GetTracerProvider() Custom tracer provider
WithMeterProvider metric.MeterProvider otel.GetMeterProvider() Custom meter provider
WithPropagator propagation.TextMapPropagator propagation.TraceContext{} Custom propagator
Logger Options
Option Type Description
WithBuiltinKeys - Register built-in context keys (trace_id, span_id, trace_flags)
WithContextKeys ...ContextKey Register custom context keys
WithRegistry *Registry Use custom registry instance

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with tests
  4. Run go test ./... and go vet ./...
  5. Submit a pull request

License

MIT License - see LICENSE for details.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidLogLevel is returned when an invalid log level is provided.
	ErrInvalidLogLevel = errors.New("invalid log level")
	// ErrInvalidLogFormat is returned when an invalid log format is provided.
	ErrInvalidLogFormat = errors.New("invalid log format")
)
View Source
var ErrReservedExtensionKey = errors.New("extension key conflicts with reserved RFC 9457 field")

ErrReservedExtensionKey is returned when an extension key conflicts with a reserved RFC 9457 field.

View Source
var SpanIDKey = ContextKey{Name: "span_id"}

SpanIDKey is the context key for W3C span ID.

View Source
var TraceFlagsKey = ContextKey{Name: "trace_flags"}

TraceFlagsKey is the context key for W3C trace flags.

View Source
var TraceIDKey = ContextKey{Name: "trace_id"}

TraceIDKey is the context key for W3C trace ID.

Functions

func GetSpanID

func GetSpanID(ctx context.Context) string

GetSpanID retrieves the span ID from the request context.

func GetTraceFlags

func GetTraceFlags(ctx context.Context) string

GetTraceFlags retrieves the trace flags from the request context.

func GetTraceID

func GetTraceID(ctx context.Context) string

GetTraceID retrieves the trace ID from the request context.

func LiveHandlerFunc

func LiveHandlerFunc() http.HandlerFunc

LiveHandlerFunc returns an HTTP handler function for liveness health checks.

func NewHandlerFromConfig

func NewHandlerFromConfig(cfg LogConfig, opts ...ContextHandlerOption) (slog.Handler, error)

NewHandlerFromConfig creates a new slog.Handler based on the provided configuration. Returns an error if level or format are invalid.

func NewHealthHandler

func NewHealthHandler(opts ...HealthHandlerOption) http.Handler

NewHealthHandler creates an HTTP handler that provides health check endpoints at /health/live and /health/ready.

Example

ExampleNewHealthHandler demonstrates creating health check endpoints.

package main

import (
	"fmt"
	"net/http"

	"github.com/monkescience/vital"
)

func main() {
	// Create health handler with version and environment
	healthHandler := vital.NewHealthHandler(
		vital.WithVersion("1.0.0"),
		vital.WithEnvironment("production"),
	)

	// Mount on router
	mux := http.NewServeMux()
	mux.Handle("/", healthHandler)

	fmt.Println("Health endpoints configured")
	fmt.Println("GET /health/live - liveness probe")
	fmt.Println("GET /health/ready - readiness probe")

	// Cleanup
	_ = mux

}
Output:

Health endpoints configured
GET /health/live - liveness probe
GET /health/ready - readiness probe

func ReadyHandlerFunc

func ReadyHandlerFunc(
	version string,
	environment string,
	checkers []Checker,
	opts ...ReadyOption,
) http.HandlerFunc

ReadyHandlerFunc returns an HTTP handler function for readiness health checks that executes the provided checkers and includes version and environment metadata in the response.

func RespondProblem

func RespondProblem(ctx context.Context, w http.ResponseWriter, problem *ProblemDetail)

RespondProblem writes a ProblemDetail as an HTTP response. It sets the appropriate content type and status code.

Example

ExampleRespondProblem demonstrates returning RFC 9457 problem details.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/monkescience/vital"
)

func main() {
	// Handler that returns a problem detail
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Return 404 Not Found with problem detail
		problem := vital.NotFound("user not found",
			vital.WithType("https://api.example.com/errors/not-found"),
			vital.WithInstance(r.URL.Path),
		)

		vital.RespondProblem(r.Context(), w, problem)
	})

	// Simulate request
	req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)

	fmt.Printf("Status: %d\n", rec.Code)
	fmt.Printf("Content-Type: %s\n", rec.Header().Get("Content-Type"))

}
Output:

Status: 404
Content-Type: application/problem+json

Types

type CheckResponse

type CheckResponse struct {
	Name     string `json:"name"`
	Status   Status `json:"status"`
	Message  string `json:"message,omitempty"`
	Duration string `json:"duration,omitempty"`
}

CheckResponse represents the result of a single health check.

type Checker

type Checker interface {
	Name() string
	Check(ctx context.Context) (Status, string)
}

Checker performs a health check and returns a status and optional message.

type ContextHandler

type ContextHandler struct {
	// contains filtered or unexported fields
}

ContextHandler is a slog.Handler that automatically extracts registered context values and adds them as log attributes.

func NewContextHandler

func NewContextHandler(handler slog.Handler, opts ...ContextHandlerOption) *ContextHandler

NewContextHandler creates a new ContextHandler wrapping the provided handler. If the provided handler is already a ContextHandler, it unwraps it first to avoid nesting. Options can be provided to configure which context keys are extracted.

Example usage:

handler := vital.NewContextHandler(
    slog.NewJSONHandler(os.Stdout, nil),
    vital.WithBuiltinKeys(),              // Include CorrelationIDKey
    vital.WithContextKeys(UserIDKey),     // Add custom keys
)

func (*ContextHandler) Enabled

func (h *ContextHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the handler handles records at the given level.

func (*ContextHandler) Handle

func (h *ContextHandler) Handle(ctx context.Context, record slog.Record) error

Handle processes the log record, extracting registered context values and adding them as attributes.

func (*ContextHandler) Registry

func (h *ContextHandler) Registry() *Registry

Registry returns the handler's registry for inspection.

func (*ContextHandler) Unwrap

func (h *ContextHandler) Unwrap() slog.Handler

Unwrap returns the underlying handler wrapped by this ContextHandler.

func (*ContextHandler) WithAttrs

func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new handler with the given attributes added. The returned handler preserves the same registry as the original.

func (*ContextHandler) WithGroup

func (h *ContextHandler) WithGroup(name string) slog.Handler

WithGroup returns a new handler with the given group name. The returned handler preserves the same registry as the original.

type ContextHandlerOption

type ContextHandlerOption func(*ContextHandler)

ContextHandlerOption is a functional option for configuring a ContextHandler.

func WithBuiltinKeys

func WithBuiltinKeys() ContextHandlerOption

WithBuiltinKeys registers all built-in context keys from the vital library. This includes keys used by vital's middleware (e.g., CorrelationIDKey).

func WithContextKeys

func WithContextKeys(keys ...ContextKey) ContextHandlerOption

WithContextKeys registers specific context keys to be extracted and logged. This is useful for adding custom application-specific keys.

func WithRegistry

func WithRegistry(registry *Registry) ContextHandlerOption

WithRegistry provides a custom registry for the ContextHandler. Use this when you want full control over the registry instance.

type ContextKey

type ContextKey struct {
	Name string
}

ContextKey is a strongly-typed key for storing values in context that should be logged.

func BuiltinKeys

func BuiltinKeys() []ContextKey

BuiltinKeys returns all built-in context keys provided by the vital library. These are keys used by vital's middleware (e.g., TraceIDKey, SpanIDKey, TraceFlagsKey).

type HealthHandlerOption

type HealthHandlerOption func(*handlerConfig)

HealthHandlerOption configures the health check handler.

func WithCheckers

func WithCheckers(checkers ...Checker) HealthHandlerOption

WithCheckers adds health checkers to be executed during readiness checks.

func WithEnvironment

func WithEnvironment(env string) HealthHandlerOption

WithEnvironment sets the environment string to include in readiness responses.

func WithReadyOptions

func WithReadyOptions(opts ...ReadyOption) HealthHandlerOption

WithReadyOptions configures readiness-specific options such as timeouts.

func WithVersion

func WithVersion(v string) HealthHandlerOption

WithVersion sets the version string to include in readiness responses.

type LiveResponse

type LiveResponse struct {
	Status Status `json:"status"`
}

LiveResponse represents the response payload for the liveness health check endpoint.

type LogConfig

type LogConfig struct {
	// Level is the log level (debug, info, warn, error).
	Level string `json:"level" yaml:"level"`
	// Format is the log format (json, text).
	Format string `json:"format" yaml:"format"`
	// AddSource includes the source file and line number in the log.
	AddSource bool `json:"add_source" yaml:"add_source"`
}

LogConfig holds configuration for the logger.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is a function that wraps an http.Handler.

func BasicAuth

func BasicAuth(username, password string, realm string) Middleware

BasicAuth returns a middleware that requires HTTP Basic Authentication. It uses constant-time comparison to prevent timing attacks.

func OTel

func OTel(opts ...OTelOption) (Middleware, error)

OTel returns a middleware that instruments HTTP requests with OpenTelemetry traces and metrics. Returns an error if required metric instruments cannot be created.

Features:

  • Creates a span for each HTTP request with standard HTTP semantic conventions
  • Propagates W3C traceparent headers (incoming and outgoing)
  • Records HTTP metrics: http.server.request.duration histogram
  • Adds trace_id and span_id to request context for log correlation
  • Returns an error if the duration histogram instrument cannot be created

Example:

tp := sdktrace.NewTracerProvider(...)
mp := sdkmetric.NewMeterProvider(...)
middleware, err := vital.OTel(
    vital.WithTracerProvider(tp),
    vital.WithMeterProvider(mp),
)
if err != nil {
    return err
}
handler := middleware(myHandler)
Example

ExampleOTel demonstrates using OpenTelemetry middleware.

package main

import (
	"fmt"
	"net/http"

	"github.com/monkescience/vital"

	sdkmetric "go.opentelemetry.io/otel/sdk/metric"

	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	// Create tracer and meter providers
	tp := sdktrace.NewTracerProvider()
	mp := sdkmetric.NewMeterProvider()

	// Create handler
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	// Wrap with OTel middleware
	otelMiddleware, err := vital.OTel(
		vital.WithTracerProvider(tp),
		vital.WithMeterProvider(mp),
	)
	if err != nil {
		panic(err)
	}

	otelHandler := otelMiddleware(handler)

	fmt.Println("Handler instrumented with OpenTelemetry")

	// Cleanup
	_ = otelHandler

}
Output:

Handler instrumented with OpenTelemetry

func Recovery

func Recovery(logger *slog.Logger) Middleware

Recovery returns a middleware that recovers from panics and returns a 500 error.

func RequestLogger

func RequestLogger(logger *slog.Logger) Middleware

RequestLogger returns a middleware that logs HTTP requests and responses. It logs the method, path, status code, duration, and remote address.

func Timeout

func Timeout(duration time.Duration) Middleware

Timeout returns a middleware that enforces a timeout on request processing. If the handler does not complete within the specified duration, it returns a 503 Service Unavailable response with a ProblemDetail JSON body.

A timeout of 0 or negative duration disables the timeout (passthrough).

The middleware wraps the ResponseWriter to detect if headers have already been sent. If the timeout fires after WriteHeader has been called, the middleware cannot change the response status and will not write the timeout error response.

Example

ExampleTimeout demonstrates using the timeout middleware.

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/monkescience/vital"
)

func main() {
	// Create a handler
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("success"))
	})

	// Wrap with timeout middleware (30 second timeout)
	timeoutHandler := vital.Timeout(30 * time.Second)(handler)

	fmt.Println("Handler wrapped with 30s timeout")

	// Cleanup
	_ = timeoutHandler

}
Output:

Handler wrapped with 30s timeout

type OTelOption

type OTelOption func(*otelConfig)

OTelOption configures the OTel middleware.

func WithMeterProvider

func WithMeterProvider(mp metric.MeterProvider) OTelOption

WithMeterProvider sets a custom meter provider.

func WithPropagator

func WithPropagator(p propagation.TextMapPropagator) OTelOption

WithPropagator sets a custom propagator (default W3C TraceContext).

func WithTracerProvider

func WithTracerProvider(tp trace.TracerProvider) OTelOption

WithTracerProvider sets a custom tracer provider.

type ProblemDetail

type ProblemDetail struct {
	// Type is a URI reference that identifies the problem type.
	// When dereferenced, it should provide human-readable documentation.
	// Defaults to "about:blank" when not specified.
	Type string `json:"type,omitempty"`

	// Title is a short, human-readable summary of the problem type.
	Title string `json:"title"`

	// Status is the HTTP status code for this occurrence of the problem.
	Status int `json:"status"`

	// Detail is a human-readable explanation specific to this occurrence.
	Detail string `json:"detail,omitempty"`

	// Instance is a URI reference identifying the specific occurrence.
	// It may or may not yield further information if dereferenced.
	Instance string `json:"instance,omitempty"`

	// Extensions holds any additional members for extensibility.
	// Use this for problem-type-specific information.
	// Reserved keys (type, title, status, detail, instance) are rejected during marshaling.
	Extensions map[string]any `json:"-"`
}

ProblemDetail represents an RFC 9457 problem details response. See https://datatracker.ietf.org/doc/html/rfc9457 for specification.

func BadRequest

func BadRequest(detail string, opts ...ProblemOption) *ProblemDetail

BadRequest creates a 400 Bad Request problem detail.

func Conflict

func Conflict(detail string, opts ...ProblemOption) *ProblemDetail

Conflict creates a 409 Conflict problem detail.

func Forbidden

func Forbidden(detail string, opts ...ProblemOption) *ProblemDetail

Forbidden creates a 403 Forbidden problem detail.

func Gone

func Gone(detail string, opts ...ProblemOption) *ProblemDetail

Gone creates a 410 Gone problem detail.

func InternalServerError

func InternalServerError(detail string, opts ...ProblemOption) *ProblemDetail

InternalServerError creates a 500 Internal Server Error problem detail.

func MethodNotAllowed

func MethodNotAllowed(detail string, opts ...ProblemOption) *ProblemDetail

MethodNotAllowed creates a 405 Method Not Allowed problem detail.

func NewProblemDetail

func NewProblemDetail(status int, title string, opts ...ProblemOption) *ProblemDetail

NewProblemDetail creates a new ProblemDetail with the specified status, title, and options.

func NotFound

func NotFound(detail string, opts ...ProblemOption) *ProblemDetail

NotFound creates a 404 Not Found problem detail.

func ServiceUnavailable

func ServiceUnavailable(detail string, opts ...ProblemOption) *ProblemDetail

ServiceUnavailable creates a 503 Service Unavailable problem detail.

func TooManyRequests

func TooManyRequests(detail string, opts ...ProblemOption) *ProblemDetail

TooManyRequests creates a 429 Too Many Requests problem detail.

func Unauthorized

func Unauthorized(detail string, opts ...ProblemOption) *ProblemDetail

Unauthorized creates a 401 Unauthorized problem detail.

func UnprocessableEntity

func UnprocessableEntity(detail string, opts ...ProblemOption) *ProblemDetail

UnprocessableEntity creates a 422 Unprocessable Entity problem detail.

func (ProblemDetail) MarshalJSON

func (p ProblemDetail) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON marshaling to include extensions. It returns an error if any extension key conflicts with a reserved RFC 9457 field name.

type ProblemOption

type ProblemOption func(*ProblemDetail)

ProblemOption configures a ProblemDetail.

func WithDetail

func WithDetail(detail string) ProblemOption

WithDetail sets the detail message for the problem detail.

func WithExtension

func WithExtension(key string, value any) ProblemOption

WithExtension adds a custom extension field to the problem detail. Reserved keys (type, title, status, detail, instance) will cause MarshalJSON to return an error.

func WithInstance

func WithInstance(instance string) ProblemOption

WithInstance sets the instance URI for the problem detail.

func WithType

func WithType(typeURI string) ProblemOption

WithType sets the type URI for the problem detail.

type ReadyOption

type ReadyOption func(*readyConfig)

ReadyOption configures the readiness handler behavior.

func WithOverallReadyTimeout

func WithOverallReadyTimeout(d time.Duration) ReadyOption

WithOverallReadyTimeout sets the maximum time allowed for all readiness checks to complete.

type ReadyResponse

type ReadyResponse struct {
	Status      Status          `json:"status"`
	Checks      []CheckResponse `json:"checks"`
	Version     string          `json:"version,omitempty"`
	Environment string          `json:"environment,omitempty"`
}

ReadyResponse represents the response payload for the readiness health check endpoint.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry manages a collection of context keys to extract and log. Each ContextHandler can have its own Registry for isolation.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new empty Registry.

func (*Registry) Keys

func (r *Registry) Keys() []ContextKey

Keys returns all registered keys as a slice for iteration.

func (*Registry) Register

func (r *Registry) Register(key ContextKey)

Register adds a context key to this registry.

type Server

type Server struct {
	*http.Server
	// contains filtered or unexported fields
}

func NewServer

func NewServer(handler http.Handler, opts ...ServerOption) *Server

NewServer creates a new Server with the provided handler and options.

Example

ExampleNewServer demonstrates creating a basic HTTP server with options.

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/monkescience/vital"
)

func main() {
	// Create a simple handler
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("Hello, World!"))
	})

	// Create server with options
	server := vital.NewServer(mux,
		vital.WithPort(8080),
		vital.WithShutdownTimeout(30*time.Second),
	)

	// Server is ready to use
	fmt.Printf("Server configured on port %d\n", 8080)

	// Cleanup
	_ = server

}
Output:

Server configured on port 8080

func (*Server) Run

func (server *Server) Run()

Run starts the server and blocks until a termination signal is received.

func (*Server) Start

func (server *Server) Start() error

Start begins listening and serving HTTP or HTTPS requests. It blocks until the server stops or encounters an error.

func (*Server) Stop

func (server *Server) Stop() error

Stop gracefully shuts down the server with the configured shutdown timeout.

type ServerOption

type ServerOption func(*Server)

ServerOption is a functional option for configuring a Server.

func WithIdleTimeout

func WithIdleTimeout(timeout time.Duration) ServerOption

WithIdleTimeout sets the maximum amount of time to wait for the next request.

func WithLogger

func WithLogger(logger *slog.Logger) ServerOption

WithLogger sets the structured logger for the server.

func WithPort

func WithPort(port int) ServerOption

WithPort sets the server port.

func WithReadTimeout

func WithReadTimeout(timeout time.Duration) ServerOption

WithReadTimeout sets the maximum duration for reading the entire request.

func WithShutdownTimeout

func WithShutdownTimeout(timeout time.Duration) ServerOption

WithShutdownTimeout sets the graceful shutdown timeout.

func WithTLS

func WithTLS(certPath, keyPath string) ServerOption

WithTLS sets the TLS certificate and key paths.

func WithWriteTimeout

func WithWriteTimeout(timeout time.Duration) ServerOption

WithWriteTimeout sets the maximum duration before timing out writes.

type Status

type Status string

Status represents the health status of a service or check.

const (
	// StatusOK indicates the service or check is healthy.
	StatusOK Status = "ok"
	// StatusError indicates the service or check has failed.
	StatusError Status = "error"
)

Jump to

Keyboard shortcuts

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