obs

package module
v1.0.1 Latest Latest
Warning

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

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

README

huma-observability

Latest release Go Reference Go version CI Socket Badge

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.

Why this package exists

Managed platforms such as Cloud Run already collect container output. Applications should only need to write structured JSON to standard output (stdout); the platform can handle ingestion and delivery.

Compared with sending logs through an in-process cloud logging client, this reduces container CPU, memory, and network use by removing logging API calls, authentication, buffering, batching, and retry work from the application. Under sustained logging load, that reduction can provide a noticeable performance improvement. It also avoids the dependency and maintenance cost of a cloud logging SDK, including its configuration, credentials, and upgrades.

This package turns that simple pipeline into useful production observability. It provides validated request IDs, strict W3C trace correlation, request-scoped fields, and one structured terminal access record. Application and access logs share the same correlation metadata, making all records from a request easier to find, filter, and understand.

Cloud presets map the same logging contract to provider-oriented fields without coupling application code to a cloud logging SDK. The package focuses on structured logging and request correlation: it does not create spans, configure OpenTelemetry, or ship logs to a backend.

Package scope

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.

It also does not export metrics, create AWS X-Ray segments, or emit generic net/http access logs.

Requirements

  • Go 1.25 or newer.
  • Huma v2.30.0 or newer within the Huma v2 line.
  • Zap.

The v1 release line follows Semantic Versioning. Exported APIs, structured log fields, defaults, and supported runtime versions are compatibility contracts. Breaking changes require a new major version and will include migration guidance.

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

When this documentation shows one configuration, it uses GCP. Complete runnable GCP, provider-neutral, AWS, and Azure applications are available in examples, with usage notes 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.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,
	}))

	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.PresetGCP,
})(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.

Handler Logging

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

func Health(ctx context.Context) {
	logger := obs.Logger(ctx)
	logger.Info("health check",
		zap.String("service_name", "example-service"),
		zap.String("service_version", "1.0.0"),
		zap.String("health_status", "ok"),
	)
	logger.Debug("dependency check",
		zap.String("dependency", "database"),
		zap.String("dependency_status", "ok"),
	)
}

Application records and the package-owned access record share the request logger's correlation fields. NewLogger defaults to info level; configure LoggerConfig.Level with zapcore.DebugLevel when debug events should be written. The canonical GCP example and its route-level tests demonstrate both level settings and decode newline-delimited JSON through the same writer boundary that defaults to stdout.

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.

Multiple tracestate header fields are combined in wire order as required by W3C Trace Context. The combined value is retained only when it is at most 512 bytes.

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

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

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

Development uses just. On macOS, install the workflow linters:

brew install actionlint zizmor

Then run the repository gates:

just install
just qa
just vuln

just qa runs formatting, lint, build, tests, race tests, actionlint, and zizmor. just vuln runs the Go vulnerability scanner separately.

References

Mutation Testing

Install Gremlins with Homebrew on macOS:

brew tap go-gremlins/tap
brew install gremlins

Then run its mutation campaign against covered production code with:

just mutation

Gremlins changes expressions and conditions, then checks whether the existing tests detect each behavioral change. Review LIVED mutants as possible test gaps; equivalent transformations do not need artificial assertions. Mutation testing intentionally runs outside just qa and may take several minutes. The configured per-mutant safety timeout does not limit the total campaign time.

Fuzz Testing

This repository uses Go's native fuzzing engine for FuzzParseTraceparent. Run the default ten-second session with:

just fuzz

Pass the target and duration explicitly for a longer run:

just fuzz FuzzParseTraceparent 1m

The equivalent native Go command is:

go test -fuzz=FuzzParseTraceparent -fuzztime=10s .

Go first replays the seed corpus and then generates new inputs. When fuzzing finds a failure, it minimizes the input and writes it under testdata/fuzz/FuzzParseTraceparent; normal go test ./... runs saved corpus inputs as regression tests. Review and commit a failing input together with the fix when it represents behavior the parser must preserve.

See the Go fuzzing documentation for the engine's workflow and additional flags.

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