obs

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 17 Imported by: 0

README

huma-observability

huma-observability provides request correlation, request-scoped Zap loggers, and structured Zap access logging middleware for Huma v2 APIs. It also provides a small standard net/http request-context middleware for services that have non-Huma routes.

The module path is github.com/janisto/huma-observability; the declared Go package name is obs.

This is not official Huma framework middleware. It is a small, opinionated package for services that want the same production logging contract without copying request middleware into every application.

When To Use It

Use this package when your Huma v2 service needs:

  • Request IDs with validation, generation, response propagation, and context accessors.
  • Request-scoped *zap.Logger values available through obs.Logger(ctx).
  • JSON access logs from Huma middleware, independent of the HTTP router.
  • Router-wide request metadata for health checks, readiness probes, redirects, static handlers, 404/405 handlers, and recovery middleware.
  • W3C traceparent parsing for trace-level log correlation.
  • Cloud-oriented log fields for Google Cloud Logging, AWS CloudWatch/X-Ray query paths, or Azure Monitor/Application Insights ingestion.

Do not use this package as a tracing system. It does not create spans, export metrics, configure OpenTelemetry, create AWS X-Ray segments, or emit generic net/http access logs.

Requirements

  • Go 1.25 or newer.
  • Huma v2.
  • Zap.

The package is currently intended for a v0.x release line. Public API names and log field names are maintained carefully, but this is not a v1 compatibility promise yet.

Install

go get github.com/janisto/huma-observability

Import the module path normally. The package name is obs, so application code uses the obs identifier:

import "github.com/janisto/huma-observability"

Quick Start

More complete runnable examples, including per-cloud deployment notes, are in EXAMPLES.md.

package main

import (
	"github.com/danielgtaylor/huma/v2"

	"github.com/janisto/huma-observability"
)

func setup(api huma.API) error {
	logger, err := obs.NewLogger(obs.LoggerConfig{
		Preset: obs.PresetDefault,
	})
	if err != nil {
		return err
	}

	api.UseMiddleware(obs.RequestContext(obs.RequestContextConfig{
		Logger: logger,
	}))
	api.UseMiddleware(obs.AccessLogger(obs.AccessLoggerConfig{
		Logger: logger,
	}))

	return nil
}

Middleware order is part of the contract: install RequestContext before AccessLogger. RequestContext installs request metadata and the request-scoped logger; AccessLogger writes the Huma operation-aware access log.

HTTP Request Context

For services with both Huma and non-Huma routes, install HTTPRequestContext at the outer router boundary:

handler := obs.HTTPRequestContext(obs.HTTPRequestContextConfig{
	Logger: logger,
	Preset: obs.PresetDefault,
})(router)

HTTPRequestContext installs request IDs, trace correlation metadata, and response request ID headers for every HTTP request. When Logger is configured, it also installs the request-scoped logger. Huma RequestContext reuses that metadata when a request reaches a Huma route, so one inbound request keeps one request ID across both layers.

HTTPRequestContext does not emit access logs and does not wrap http.ResponseWriter. Non-Huma access logs are application-owned or router-owned. Huma routes should use AccessLogger for operation-aware access logs with path_template and operation_id. See EXAMPLES.md for plain net/http handler logging and a Chi route-group access logger.

Handler Logging

Use obs.Logger(ctx) anywhere you have the request context.Context.

func GetRepository(ctx context.Context, owner, repo string) error {
	obs.Logger(ctx).Info("loading github repository",
		zap.String("owner", owner),
		zap.String("repo", repo),
	)
	return nil
}

Logger(ctx) never returns nil. Configure RequestContextConfig.Logger for Huma-only services, or HTTPRequestContextConfig.Logger at the router boundary for mixed Huma and non-Huma services. If a context did not pass through configured request-context middleware, Logger(ctx) returns a no-op logger instead of using a package-global logger or implicit stdout fallback.

Recovery middleware that logs with Logger(r.Context()) must run after HTTPRequestContext if it needs request metadata. Logs emitted before request-context middleware may not have request_id; that means the request did not cross the package boundary yet, the middleware order is wrong, or the log is intentionally outside an HTTP request.

Trace Correlation

W3C traceparent is the only trace context input parsed by this package. When the header is valid, the W3C trace ID becomes the request correlation_id and provider-specific trace field source. When the header is missing or invalid, correlation_id falls back to request_id.

This means every log line gets a stable grouping key:

  • With valid W3C trace context: group by correlation_id=<trace-id>.
  • Without valid trace context: group by correlation_id=<request-id>.

The package also emits common trace fields when a valid trace exists:

  • trace_id
  • parent_id
  • trace_flags
  • trace_sampled

Provider-specific propagation headers such as X-Cloud-Trace-Context, X-Amzn-Trace-Id, and Azure's legacy Request-Id header are intentionally not parsed. If your service must bridge those headers into W3C Trace Context, do that with cloud SDK instrumentation or OpenTelemetry beside this package.

For real Go HTTP tracing, use OpenTelemetry's otelhttp instrumentation in your application. otelhttp.NewHandler wraps HTTP handlers with server spans, and otelhttp.NewTransport instruments HTTP clients and outbound propagation. This package does not configure OpenTelemetry SDKs, exporters, samplers, or global tracer providers.

Cloud Presets

Use the same preset for NewLogger, RequestContext, AccessLogger, and HTTPRequestContext when those pieces are used together.

Google Cloud
logger, err := obs.NewLogger(obs.LoggerConfig{
	Preset: obs.PresetGCP,
})
if err != nil {
	return err
}

api.UseMiddleware(obs.RequestContext(obs.RequestContextConfig{
	Logger: logger,
	Preset: obs.PresetGCP,
}))
api.UseMiddleware(obs.AccessLogger(obs.AccessLoggerConfig{
	Logger: logger,
	Preset: obs.PresetGCP,
}))

The GCP preset emits Cloud Logging-friendly JSON:

  • severity instead of level.
  • httpRequest for Huma access logs.
  • logging.googleapis.com/trace with the raw W3C TRACE_ID.
  • logging.googleapis.com/trace_sampled from the W3C sampled flag.

The middleware does not emit logging.googleapis.com/spanId from a W3C parent-id. A log span ID must come from a real current span; the incoming parent ID is not the same semantic value.

AWS
logger, err := obs.NewLogger(obs.LoggerConfig{
	Preset: obs.PresetAWS,
})
if err != nil {
	return err
}

api.UseMiddleware(obs.RequestContext(obs.RequestContextConfig{
	Logger: logger,
	Preset: obs.PresetAWS,
}))
api.UseMiddleware(obs.AccessLogger(obs.AccessLoggerConfig{
	Logger: logger,
	Preset: obs.PresetAWS,
}))

The AWS preset keeps logs as flat JSON with timestamp, level, and message. With a valid W3C traceparent, it also emits:

  • trace_id
  • parent_id
  • trace_flags
  • trace_sampled
  • xray_trace_id, derived from the W3C trace ID in AWS X-Ray format.

The middleware does not create AWS X-Ray segments and does not emit span_id from an incoming W3C parent ID.

Azure
logger, err := obs.NewLogger(obs.LoggerConfig{
	Preset: obs.PresetAzure,
})
if err != nil {
	return err
}

api.UseMiddleware(obs.RequestContext(obs.RequestContextConfig{
	Logger: logger,
	Preset: obs.PresetAzure,
}))
api.UseMiddleware(obs.AccessLogger(obs.AccessLoggerConfig{
	Logger: logger,
	Preset: obs.PresetAzure,
}))

The Azure preset keeps logs as flat JSON with timestamp, level, and message. With a valid W3C traceparent, it emits:

  • trace_id
  • parent_id
  • trace_flags
  • trace_sampled
  • operation_Id, mapped from the W3C trace ID.
  • operation_ParentId, mapped from the W3C parent ID.

Field Contract

Package-owned fields use snake_case. Provider-required fields keep the names expected by the target platform.

Request metadata fields:

Field Meaning
request_id The local HTTP request ID for this service.
correlation_id The W3C trace ID when valid trace context exists; otherwise the request ID.
trace_id The W3C trace ID from traceparent.
parent_id The W3C parent ID from traceparent.
trace_flags The W3C trace flags value.
trace_sampled Boolean value derived from the sampled flag.

Provider-specific fields:

Preset Fields
GCP logging.googleapis.com/trace, logging.googleapis.com/trace_sampled, httpRequest
AWS xray_trace_id
Azure operation_Id, operation_ParentId

Huma access log fields:

  • method
  • path
  • path_template
  • operation_id
  • status
  • duration_ms
  • remote_ip
  • user_agent
  • httpRequest for GCP

Logger keys:

  • Generic, AWS, Azure: timestamp, level, message, optional logger.
  • GCP: timestamp, severity, message, optional logger.

AccessLoggerConfig.ExtraFields may add application-specific fields to Huma access logs. Fields using package-owned or provider-reserved keys are ignored to avoid duplicate core keys in the JSON output.

Request IDs

Default RequestContext and HTTPRequestContext behavior:

  • Request ID header: X-Request-Id.
  • Trace header: traceparent.
  • Tracestate header: tracestate.
  • Response request ID header: same as the request ID header.
  • Generated request IDs: 16 random bytes encoded as lowercase hex.

Invalid incoming request IDs are ignored and replaced. Invalid traceparent values are ignored for correlation while request processing continues.

CorrelationID(ctx) returns the same value written to correlation_id: the W3C trace ID when a valid traceparent exists, otherwise the request ID.

Set DisableResponseHeader when an upstream gateway owns request ID response headers and the application should not write one.

Use RequestContextConfig for Huma routes and HTTPRequestContextConfig for router-wide HTTP middleware when you need custom request ID or trace header names.

Logger Configuration

NewLogger creates a JSON Zap logger. By default it writes application logs to stdout and Zap internal errors to stderr.

Useful options:

  • Preset: selects generic, GCP, AWS, or Azure field naming.
  • Level: sets the Zap level enabler. Defaults to info.
  • Writer: overrides the application log destination.
  • ErrorWriter: overrides Zap's internal error destination.
  • AddCaller: includes Zap caller fields.
  • Development: enables Zap development behavior.

Panic Behavior

AccessLogger logs a 500 access log when downstream Huma middleware or handlers panic, then re-panics. It does not recover the request or hide the panic from upstream recovery middleware.

Optional Local Wrapper

Applications that want shorter local logging helpers can add them in application code. A complete copyable example is available at examples/local-wrapper/applog/log.go. It intentionally stays small: Log, Debug, Info, Warn, and Error.

applog.Info(ctx, "repository loaded", zap.String("repository", "payments"))
applog.Error(ctx, "github request failed", err, zap.Int("status", status))

The package itself stays Zap-native and does not add application-specific LogWarn or LogError wrappers.

Validation

Use the same checks locally that CI runs:

go test ./...
go test -race ./...
go vet ./...
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
"$(go env GOPATH)/bin/golangci-lint" run ./...
go install golang.org/x/vuln/cmd/govulncheck@v1.5.0
"$(go env GOPATH)/bin/govulncheck" ./...
test -z "$(gofmt -l .)"

References

Documentation

Overview

Package obs provides request correlation, request-scoped Zap loggers, and structured Zap access logging middleware for Huma v2 APIs, plus request context middleware for standard net/http handlers.

The package is intentionally small and opinionated: it uses W3C Trace Context for trace correlation, validates or generates request IDs, stores request-scoped Zap loggers on context, and emits JSON access logs with generic, Google Cloud, AWS, and Azure presets.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AccessLogger

func AccessLogger(config AccessLoggerConfig) func(huma.Context, func(huma.Context))

AccessLogger returns Huma middleware that installs a request-scoped Zap logger and emits one structured access log after the handler completes.

func CorrelationID

func CorrelationID(ctx context.Context) string

CorrelationID returns the trace ID when a W3C trace exists, otherwise the request ID.

func DefaultStatusLevel

func DefaultStatusLevel(status int) zapcore.Level

DefaultStatusLevel maps 5xx responses to error, 4xx responses to warn, and all other responses to info.

func DefaultValidateRequestID

func DefaultValidateRequestID(value string) bool

DefaultValidateRequestID validates incoming request IDs accepted by the default middleware configuration.

func HTTPRequestContext added in v0.2.0

func HTTPRequestContext(config HTTPRequestContextConfig) func(http.Handler) http.Handler

HTTPRequestContext returns net/http middleware that installs request-scoped correlation metadata on the request context.

func Logger

func Logger(ctx context.Context) *zap.Logger

Logger returns the request-scoped logger from ctx, or a no-op logger when no request logger has been installed.

func NewLogger

func NewLogger(config LoggerConfig) (*zap.Logger, error)

NewLogger creates a JSON Zap logger for the selected preset.

func RequestContext

func RequestContext(config RequestContextConfig) func(huma.Context, func(huma.Context))

RequestContext returns Huma middleware that installs request-scoped correlation metadata on the request context.

func RequestID

func RequestID(ctx context.Context) string

RequestID returns the validated or generated request ID for the context.

Types

type AccessLoggerConfig

type AccessLoggerConfig struct {
	Logger      *zap.Logger
	Preset      Preset
	Now         func() time.Time
	StatusLevel StatusLeveler
	ExtraFields func(huma.Context) []zap.Field
}

AccessLoggerConfig configures AccessLogger middleware.

type HTTPRequestContextConfig added in v0.2.0

type HTTPRequestContextConfig struct {
	RequestIDHeader       string
	TraceparentHeader     string
	TracestateHeader      string
	ResponseHeader        string
	DisableResponseHeader bool
	NewRequestID          func() string
	ValidateRequestID     func(string) bool

	Logger *zap.Logger
	Preset Preset
}

HTTPRequestContextConfig configures HTTPRequestContext middleware.

type LoggerConfig

type LoggerConfig struct {
	Preset      Preset
	Level       zapcore.LevelEnabler
	Writer      io.Writer
	ErrorWriter io.Writer
	AddCaller   bool
	Development bool
}

LoggerConfig configures NewLogger.

type Preset

type Preset string

Preset selects a JSON logger field shape.

const (
	// PresetDefault uses flat generic JSON fields.
	PresetDefault Preset = ""
	// PresetGCP uses Google Cloud Logging severity field names and access-log
	// support for Cloud Logging special JSON fields.
	PresetGCP Preset = "gcp"
	// PresetAWS uses flat JSON fields suitable for CloudWatch Logs ingestion.
	PresetAWS Preset = "aws"
	// PresetAzure uses flat JSON fields suitable for Azure Monitor ingestion.
	PresetAzure Preset = "azure"
)

type RequestContextConfig

type RequestContextConfig struct {
	RequestIDHeader       string
	TraceparentHeader     string
	TracestateHeader      string
	ResponseHeader        string
	DisableResponseHeader bool
	NewRequestID          func() string
	ValidateRequestID     func(string) bool

	Logger *zap.Logger
	Preset Preset
}

RequestContextConfig configures RequestContext middleware.

type StatusLeveler

type StatusLeveler func(status int) zapcore.Level

StatusLeveler maps an HTTP response status to a Zap log level.

type TraceContext

type TraceContext struct {
	TraceID     string
	ParentID    string
	Flags       string
	Sampled     bool
	Traceparent string
	Tracestate  string
	Valid       bool
}

TraceContext contains the parsed W3C traceparent value for a request.

func ParseTraceparent

func ParseTraceparent(value string) (TraceContext, bool)

ParseTraceparent parses a W3C traceparent header value.

It accepts version 00 exactly as specified and follows W3C forward compatibility rules for future versions: base fields must be parseable and any extension data must follow the flags field after a dash.

func Trace

func Trace(ctx context.Context) TraceContext

Trace returns the parsed W3C trace context for the context, if one exists.

Directories

Path Synopsis
examples
aws command
azure command
basic command
gcp command

Jump to

Keyboard shortcuts

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