obs

package module
v0.1.0 Latest Latest
Warning

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

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

README

echo-observability

echo-observability provides request correlation, request-scoped Zap loggers, and structured Zap access logging middleware for Labstack Echo v5.

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

This is not official Echo middleware. It is a small, opinionated package for services that want a consistent production logging contract on Echo v5.

When To Use It

Use this package when an Echo v5 service needs:

  • Validated or generated request IDs with response propagation.
  • Request-scoped *zap.Logger values through obs.Logger(ctx).
  • Strict W3C traceparent parsing and trace-level log correlation.
  • One structured access log after each Echo request.
  • Low-cardinality path_template values from Echo's c.Path().
  • Echo v5 status resolution for committed responses and errors implementing echo.HTTPStatusCoder.
  • Generic, Google Cloud, AWS, and Azure JSON field presets.
  • Panic access logging followed by re-panic for the application's recovery middleware.
  • Router-wide request metadata for health checks, readiness probes, redirects, static handlers, 404/405 handlers, and recovery middleware.

This package does not create traces, spans, metrics, Prometheus endpoints, or OpenTelemetry exporters. It does not provide separate endpoint exporters.

Requirements

  • Go 1.25 or newer; deploy with the latest available patch release.
  • Echo v5 only.
  • 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/echo-observability

Quick Start

package main

import (
	"net/http"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
	"go.uber.org/zap"

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

func main() {
	logger, err := obs.NewLogger(obs.LoggerConfig{})
	if err != nil {
		panic(err)
	}

	e := echo.New()
	e.Use(
		obs.RequestContext(obs.RequestContextConfig{Logger: logger}),
		obs.AccessLogger(obs.AccessLoggerConfig{Logger: logger}),
		middleware.Recover(),
	)

	e.GET("/health", func(c *echo.Context) error {
		obs.Logger(c.Request().Context()).Info("health checked")
		return c.NoContent(http.StatusNoContent)
	})

	if err := e.Start(":8080"); err != nil {
		logger.Error("server stopped", zap.Error(err))
	}
}

Install RequestContext before AccessLogger. Put recovery middleware after AccessLogger when panics must produce an access log before being recovered. Echo applies Use middleware after routing, so c.Path() contains the matched route template.

Middleware

RequestContext

RequestContext validates the incoming request ID, generates a safe ID when needed, parses W3C trace context, installs metadata on c.Request().Context(), and adds the request ID response header.

e.Use(obs.RequestContext(obs.RequestContextConfig{
	Logger: logger,
	Preset: obs.PresetGCP,
}))

Defaults:

Setting Default
Request ID header X-Request-Id
Response header Request ID header
Trace header traceparent
Trace state header tracestate
Request ID format 32 lowercase hexadecimal characters

Incoming request IDs are at most 128 bytes and may contain ASCII letters, digits, -, ., _, and ~. Customize generation or validation with NewRequestID and ValidateRequestID. Set DisableResponseHeader when the request ID must not be returned.

Access metadata anywhere a standard context.Context is available:

ctx := c.Request().Context()
requestID := obs.RequestID(ctx)
correlationID := obs.CorrelationID(ctx)
trace := obs.Trace(ctx)
logger := obs.Logger(ctx)

Logger always returns a non-nil logger. It returns a no-op logger outside an installed request context.

HTTPRequestContext

For services with both Echo and non-Echo routes, install HTTPRequestContext at the outer net/http boundary:

mux := http.NewServeMux()
mux.Handle("/", e)
mux.HandleFunc("GET /ready", readyHandler)

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

HTTPRequestContext installs request IDs, trace correlation metadata, response request ID headers, and an optional request-scoped logger for every HTTP request. Echo RequestContext reuses that metadata, so an inbound request keeps one request ID across both layers.

It does not emit access logs or wrap http.ResponseWriter. Echo routes should use AccessLogger; access logging for non-Echo routes remains application-owned. The same header, generation, validation, response-header, logger, and preset options are available in HTTPRequestContextConfig.

Handler Logging

Use obs.Logger(ctx) anywhere a standard request context is available:

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

Configure RequestContextConfig.Logger for Echo-only services, or HTTPRequestContextConfig.Logger at the router boundary for mixed services. Background jobs, scripts, direct service calls, and tests using context.Background() must use an explicit process logger; obs.Logger(ctx) is intentionally a no-op outside installed request metadata.

Access Logger

AccessLogger installs request metadata by itself when RequestContext is missing, but explicit installation of both middlewares is preferred. It emits:

  • method
  • path — escaped request path; never includes the query string
  • path_template — Echo route pattern such as /users/:id
  • operation_id — explicit Echo route name, when configured
  • status
  • duration_ms
  • remote_ip
  • user_agent
  • error — returned Echo error, when present

The request-scoped fields are request_id, correlation_id, and, for valid W3C trace context, trace_id, parent_id, trace_flags, and trace_sampled.

Echo's ResolveResponseStatus determines the logged status. A committed response wins over a later error; otherwise an Echo HTTPStatusCoder status wins, and a plain error maps to 500. The original error is returned unchanged for Echo's centralized HTTP error handler.

Application errors that a custom HTTPErrorHandler maps to a non-500 status must implement echo.HTTPStatusCoder so the access log and eventual response use the same status. AccessLogger intentionally does not invoke the global error handler itself because that would commit the response inside the logging middleware.

Use ExtraFields for application-owned access-log fields. Package-owned and provider-owned field names are ignored to prevent duplicate JSON keys.

e.Use(obs.AccessLogger(obs.AccessLoggerConfig{
	Logger: logger,
	ExtraFields: func(c *echo.Context) []zap.Field {
		return []zap.Field{zap.String("tenant_id", tenantID(c))}
	},
}))

StatusLevel can override the default mapping: 5xx is error, 4xx is warn, and all other statuses are info. Now exists for deterministic testing.

Named Routes

Echo assigns an internal default route name. operation_id is emitted only for an explicitly named route:

_, err := e.AddRoute(echo.Route{
	Method:  http.MethodGet,
	Path:    "/users/:id",
	Name:    "get-user",
	Handler: getUser,
})

The raw request /users/123 logs path=/users/123 and path_template=/users/:id. Group metrics or logs by path_template, not path, to avoid high-cardinality dimensions.

Trace Correlation

W3C traceparent is the only trace input. A valid trace ID becomes correlation_id; otherwise correlation_id falls back to request_id. Multiple tracestate header fields are combined in wire order, and the combined value is retained only when it is at most 512 bytes.

Provider-specific headers such as X-Cloud-Trace-Context, X-Amzn-Trace-Id, and Azure's legacy Request-Id are intentionally not parsed. The package correlates logs; it does not create spans or provider trace segments.

Cloud Presets

Use the same preset for NewLogger, RequestContext, and AccessLogger.

logger, err := obs.NewLogger(obs.LoggerConfig{Preset: obs.PresetGCP})
e.Use(
	obs.RequestContext(obs.RequestContextConfig{
		Logger: logger, Preset: obs.PresetGCP,
	}),
	obs.AccessLogger(obs.AccessLoggerConfig{
		Logger: logger, Preset: obs.PresetGCP,
	}),
)
Google Cloud

The GCP preset emits severity instead of level, a structured httpRequest object on access lines, logging.googleapis.com/trace, and logging.googleapis.com/trace_sampled. The trace field contains the raw W3C trace ID, which is Google Cloud's preferred format. It deliberately does not emit logging.googleapis.com/spanId from the incoming parent ID.

AWS

The AWS preset keeps flat timestamp, level, and message fields. A valid W3C trace also emits xray_trace_id in 1-8hex-24hex form. It does not create X-Ray segments or treat the incoming parent ID as a current X-Ray span.

Azure

The Azure preset keeps flat JSON and maps a valid W3C trace to operation_Id and operation_ParentId. It does not initialize Application Insights or create dependency/request telemetry.

An incoming W3C parent ID is not emitted as a current span ID. A current span ID can only come from real tracing instrumentation.

Field Contract

Every JSON line created by NewLogger uses:

  • timestamp: UTC RFC3339 with nanosecond precision.
  • level, or severity for GCP.
  • logger: present for named Zap loggers.
  • message.

Request-scoped lines add:

  • request_id.
  • correlation_id.
  • trace_id, parent_id, trace_flags, and trace_sampled only for a valid W3C trace.
  • Provider-specific trace fields selected by the configured preset.

Access lines add:

  • method.
  • path: escaped concrete URL path without the query string.
  • path_template: parameterized Echo route path when matched.
  • operation_id: explicitly configured Echo route name.
  • status.
  • duration_ms.
  • remote_ip: c.RealIP(), honoring the application's Echo IPExtractor.
  • user_agent when present.
  • error when Echo middleware or the handler returns an error.
  • httpRequest for the GCP preset only.

ExtraFields applies only to the access line. Reserved package and provider keys are ignored so custom fields cannot produce duplicate or forged owned values.

Request IDs

The default generator reads 128 bits from crypto/rand and encodes them as 32 lowercase hexadecimal characters. If entropy acquisition fails, or a custom generator returns invalid data twice, a process-local atomic fallback is used.

The default validator accepts 1–128 ASCII characters from the unreserved URI set: letters, digits, -, ., _, and ~. Invalid client input is replaced, never copied to response headers or logs. Applications may supply both NewRequestID and ValidateRequestID when they need a different contract.

Middleware Placement

Install request context and access logging at the outer observability boundary so downstream middleware failures are correlated and logged:

e.Use(
	obs.RequestContext(obs.RequestContextConfig{
		Logger: logger,
		Preset: obs.PresetGCP,
	}),
	obs.AccessLogger(obs.AccessLoggerConfig{
		Logger: logger,
		Preset: obs.PresetGCP,
	}),
	middleware.CORS(),
	middleware.BodyLimit(1<<20),
	middleware.Recover(),
)

Read request metadata with obs.RequestID(c.Request().Context()) and log with obs.Logger(c.Request().Context()). Echo's own e.Logger remains separate from application request logging.

Configure e.IPExtractor for the real deployment topology before serving requests. AccessLogger uses Echo's resolved client IP and therefore inherits its trust model for proxy headers.

Keep the observability pair outside middleware such as BodyLimit, CORS, and authentication when their rejected requests must also receive request IDs and access logs.

Logger Configuration

NewLogger writes JSON application logs to stdout and Zap internal errors to stderr by default. LoggerConfig supports Level, Writer, ErrorWriter, AddCaller, and Development. Add stable application fields to the returned base logger before passing it to middleware:

logger = logger.With(
	zap.String("service", "example-api"),
	zap.String("environment", "production"),
	zap.String("version", version),
)

Do not log authorization headers, cookies, tokens, request bodies, or other secrets and personal data.

Panic Behavior

AccessLogger recovers a panic only long enough to emit an access log, then re-panics with the original value. An uncommitted response is logged as 500. If the response was already committed, its wire status is preserved in the log. Install the application's recovery middleware inside it—later in the e.Use list—when the application must turn panics into HTTP responses. The package never swallows a panic or owns the response format.

Optional Local Wrapper

Projects that prefer application-specific helpers can wrap the context API without introducing another logging backend. A complete tested example is in examples/local-wrapper/applog.

func Info(ctx context.Context, msg string, fields ...zap.Field) {
	obs.Logger(ctx).Info(msg, fields...)
}

Keep the wrapper local to the application. This package intentionally exposes Zap directly rather than defining a second logger interface.

Validation

The repository validates the Go 1.25 support line using its latest patched toolchain with:

go test ./...
go test -race ./...
go vet ./...
golangci-lint run ./...

The suite covers the real Echo adapter path, standard net/http composition, request ID and trace boundaries, returned and committed response errors, panic rethrow, concurrent logging, cloud field contracts, reserved fields, and request-context immutability. ParseTraceparent also has a fuzz target.

References

License

MIT. See LICENSE.

Documentation

Overview

Package obs provides request correlation, request-scoped Zap loggers, and structured Zap access logging middleware for Labstack Echo v5, 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) echo.MiddlewareFunc

AccessLogger returns Echo v5 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

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) echo.MiddlewareFunc

RequestContext returns Echo v5 middleware that installs request-scoped correlation metadata on the standard 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(*echo.Context) []zap.Field
}

AccessLoggerConfig configures AccessLogger middleware.

type HTTPRequestContextConfig

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