huma-observability
huma-observability provides request correlation and structured Zap access
logging middleware for Huma APIs.
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 middleware into every application.
When To Use It
Use this package when your Huma 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.
- 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, or create AWS X-Ray segments.
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 it explicitly as obs in examples and application code:
import obs "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"
obs "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{}))
api.UseMiddleware(obs.AccessLogger(obs.AccessLoggerConfig{
Logger: logger,
}))
return nil
}
Middleware order is part of the contract: install RequestContext before
AccessLogger. That gives handlers and lower-level services access to request
metadata and the request-scoped 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. If no request logger has been installed, it
returns a no-op logger.
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 and AccessLogger.
Google Cloud
logger, err := obs.NewLogger(obs.LoggerConfig{
Preset: obs.PresetGCP,
})
if err != nil {
return err
}
api.UseMiddleware(obs.RequestContext(obs.RequestContextConfig{}))
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 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{}))
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{}))
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 |
Access log fields:
method
path
path_template
operation_id
status
duration_ms
remote_ip
user_agent
Logger keys:
- Generic, AWS, Azure:
timestamp, level, message, optional logger.
- GCP:
timestamp, severity, message, optional logger.
AccessLoggerConfig.ExtraFields may add application-specific fields to the
access log. Fields using package-owned or provider-reserved keys are ignored to
avoid duplicate core keys in the JSON output.
Request IDs
Default request context 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.
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 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