This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Package Reference

API reference documentation for Rivaas packages

Detailed API reference for all Rivaas packages. Each package reference includes complete documentation of types, methods, options, and technical details.

Available Packages

App (rivaas.dev/app)

A batteries-included web framework built on top of the Rivaas router. Includes integrated observability (metrics, tracing, logging), lifecycle management with hooks, graceful shutdown handling, health and debug endpoints, and request binding/validation.

View App Package Reference →

Router (rivaas.dev/router)

High-performance HTTP router with radix tree routing, bloom filters, and compiled route tables. Features sub-microsecond routing, built-in middleware support, request binding, OpenTelemetry native tracing, API versioning, and content negotiation.

View Router Package Reference →

Config (rivaas.dev/config)

Powerful configuration management for Go applications with support for multiple sources (files, environment variables, remote sources), format-agnostic with built-in JSON/YAML/TOML support, hierarchical configuration merging, and automatic struct binding with validation.

View Config Package Reference →

Binding (rivaas.dev/binding)

High-performance request data binding for Go web applications. Maps values from various sources (query parameters, form data, JSON bodies, headers, cookies, path parameters) into Go structs using struct tags with type-safe generic API.

View Binding Package Reference →

Validation (rivaas.dev/validation)

Flexible, multi-strategy validation for Go structs with support for struct tags, JSON Schema, and custom interfaces. Features partial validation for PATCH requests, sensitive data redaction, and detailed field-level error reporting.

View Validation Package Reference →

Logging (rivaas.dev/logging)

Structured logging for Go applications using Go’s standard log/slog package. Features multiple output formats (JSON, Text, Console), context-aware logging with OpenTelemetry trace correlation, automatic sensitive data redaction, and log sampling.

View Logging Package Reference →

Metrics (rivaas.dev/metrics)

OpenTelemetry-based metrics collection for Go applications with support for Prometheus, OTLP, and stdout exporters. Includes built-in HTTP metrics middleware, custom metrics (counters, histograms, gauges), and automatic header filtering for security.

View Metrics Package Reference →

Tracing (rivaas.dev/tracing)

OpenTelemetry-based distributed tracing for Go applications with support for Stdout, OTLP (gRPC and HTTP), and Noop providers. Includes built-in HTTP middleware for request tracing, manual span management, and context propagation.

View Tracing Package Reference →

OpenAPI (rivaas.dev/openapi)

Automatic OpenAPI 3.0.4 and 3.1.2 specification generation from Go code using struct tags and reflection. Features fluent HTTP method constructors, automatic parameter discovery, schema generation, built-in validation, and Swagger UI configuration support.

View OpenAPI Package Reference →

1 - Config Package

API reference for rivaas.dev/config - Configuration management for Go applications

This is the API reference for the rivaas.dev/config package. For learning-focused documentation, see the Config Guide.

Package Information

Package Overview

The config package provides powerful configuration management for Go applications with support for multiple sources, formats, and validation strategies.

Core Features

  • Multiple configuration sources (files, environment variables, remote sources)
  • Format-agnostic with built-in JSON, YAML, and TOML support
  • Hierarchical configuration merging
  • Automatic struct binding with type safety
  • Multiple validation strategies
  • Thread-safe operations
  • Nil-safe getter methods

Architecture

The package is organized into several key components:

Main Package (rivaas.dev/config)

Core configuration management including:

  • Config struct - Main configuration container
  • New() / MustNew() - Configuration initialization
  • Getter methods - Type-safe value retrieval
  • Load() / Dump() - Loading and saving configuration

Sub-packages

  • codec - Format encoding/decoding (JSON, YAML, TOML, etc.)
  • source - Configuration sources (file, environment, Consul, etc.)
  • dumper - Configuration output destinations

Quick API Index

Configuration Creation

cfg, err := config.New(options...)     // With error handling
cfg := config.MustNew(options...)      // Panics on error

Loading Configuration

err := cfg.Load(ctx context.Context)

Accessing Values

// Direct access (returns zero values for missing keys)
value := cfg.String("key")
value := cfg.Int("key")
value := cfg.Bool("key")

// With defaults
value := cfg.StringOr("key", "default")
value := cfg.IntOr("key", 8080)

// With error handling
value, err := config.GetE[Type](cfg, "key")

Dumping Configuration

err := cfg.Dump(ctx context.Context)

Reference Pages

API Reference

Complete documentation of the Config struct and all methods including:

  • Configuration lifecycle methods
  • All getter method signatures
  • Error types and handling
  • Nil-safety guarantees

Options

Comprehensive list of all configuration options:

  • Source options (WithFile, WithEnv, WithConsul, etc.)
  • Validation options (WithBinding, WithValidator, WithJSONSchema)
  • Dumper options (WithFileDumper, WithDumper)

Codecs

Built-in and custom codec documentation:

  • Format codecs (JSON, YAML, TOML, EnvVar)
  • Caster codecs (Int, Bool, Duration, Time, etc.)
  • Creating custom codecs
  • File extension auto-detection

Troubleshooting

Common issues and solutions:

  • Configuration not loading
  • Struct not populating
  • Environment variable mapping
  • Performance considerations
  • Thread-safety information

Type Reference

Config

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

Main configuration container. Thread-safe for concurrent access.

ConfigError

type ConfigError struct {
    Source    string // Where the error occurred
    Field     string // Specific field with error
    Operation string // Operation being performed
    Err       error  // Underlying error
}

Error type for configuration operations with detailed context.

Option

type Option func(*Config) error

Configuration option function type used with New() and MustNew().

Common Patterns

Basic Usage

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)
cfg.Load(context.Background())

port := cfg.Int("server.port")

With Struct Binding

type AppConfig struct {
    Server struct {
        Port int `config:"port"`
    } `config:"server"`
}

var appConfig AppConfig
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithBinding(&appConfig),
)
cfg.Load(context.Background())

With Validation

func (c *AppConfig) Validate() error {
    if c.Server.Port <= 0 {
        return errors.New("port must be positive")
    }
    return nil
}

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithBinding(&appConfig),  // Validation runs after binding
)

Thread Safety

The Config type is thread-safe for:

  • Concurrent Load() operations
  • Concurrent getter operations
  • Mixed Load() and getter operations

Not thread-safe for:

  • Concurrent modification of the same configuration instance during initialization

Performance Notes

  • Getter methods are O(1) for simple keys, O(n) for nested dot notation paths
  • Load performance depends on source count and data size
  • Struct binding uses reflection, minimal overhead for most applications
  • Validation overhead depends on validation complexity

Version Compatibility

The config package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

Next Steps

For learning-focused guides, see the Configuration Guide.

1.1 - API Reference

Complete API documentation for the Config type and methods

Complete API reference for the Config struct and all its methods.

Types

Config

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

Main configuration container. Thread-safe for concurrent read operations and loading.

Key properties:

  • Thread-safe for concurrent Load() and getter operations.
  • Nil-safe. All getter methods handle nil instances gracefully.
  • Hierarchical data storage with dot notation support.

ConfigError

type ConfigError struct {
    Source    string // Where the error occurred (e.g., "source[0]", "json-schema")
    Field     string // Specific field with the error (optional)
    Operation string // Operation being performed (e.g., "load", "validate")
    Err       error  // Underlying error
}

Error type providing detailed context about configuration errors.

Example error messages:

config error in source[0] during load: file not found: config.yaml
config error in json-schema during validate: server.port: must be >= 1
config error in binding during bind: failed to decode configuration

Initialization Functions

New

func New(options ...Option) (*Config, error)

Creates a new Config instance with the given options. Returns an error if any option fails.

Parameters:

  • options - Variable number of Option functions.

Returns:

  • *Config - Initialized configuration instance.
  • error - Error if initialization fails.

Example:

cfg, err := config.New(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)
if err != nil {
    log.Fatalf("failed to create config: %v", err)
}

Use when: You need explicit error handling. Recommended for libraries.

MustNew

func MustNew(options ...Option) *Config

Creates a new Config instance with the given options. Panics if any option fails.

Parameters:

  • options - Variable number of Option functions

Returns:

  • *Config - Initialized configuration instance

Panics: If any option returns an error

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)

Use when: In main() or initialization code where panic is acceptable.

Lifecycle Methods

Load

func (c *Config) Load(ctx context.Context) error

Loads configuration from all configured sources, merges them, and runs validation.

Parameters:

  • ctx - Context for cancellation and deadlines (must not be nil)

Returns:

  • error - ConfigError if loading, merging, or validation fails

Behavior:

  1. Loads data from all sources sequentially
  2. Merges data hierarchically (later sources override earlier ones)
  3. Runs JSON Schema validation (if configured)
  4. Runs custom validation functions (if configured)
  5. Binds to struct (if configured)
  6. Runs struct Validate() method (if implemented)

Example:

if err := cfg.Load(context.Background()); err != nil {
    log.Fatalf("failed to load config: %v", err)
}

Thread-safety: Safe for concurrent calls (uses internal locking).

Dump

func (c *Config) Dump(ctx context.Context) error

Writes the current configuration state to all configured dumpers.

Parameters:

  • ctx - Context for cancellation and deadlines (must not be nil)

Returns:

  • error - Error if any dumper fails

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFileDumper("output.yaml"),
)
cfg.Load(context.Background())
cfg.Dump(context.Background())  // Writes to output.yaml

Use cases: Debugging, configuration snapshots, generating configuration files.

Getter Methods

Get

func (c *Config) Get(key string) any

Retrieves the value at the given key path. Returns nil for missing keys.

Parameters:

  • key - Dot-separated path (e.g., “server.port”)

Returns:

  • any - Value at the key, or nil if not found

Nil-safe: Returns nil if Config instance is nil.

Example:

value := cfg.Get("server.port")
if port, ok := value.(int); ok {
    fmt.Printf("Port: %d\n", port)
}

String

func (c *Config) String(key string) string

Retrieves a string value at the given key.

Returns: Empty string "" if key not found or on nil instance.

Example:

host := cfg.String("server.host")  // "" if missing

Int

func (c *Config) Int(key string) int

Retrieves an integer value at the given key.

Returns: 0 if key not found or on nil instance.

Int64

func (c *Config) Int64(key string) int64

Retrieves an int64 value at the given key.

Returns: 0 if key not found or on nil instance.

Float64

func (c *Config) Float64(key string) float64

Retrieves a float64 value at the given key.

Returns: 0.0 if key not found or on nil instance.

Bool

func (c *Config) Bool(key string) bool

Retrieves a boolean value at the given key.

Returns: false if key not found or on nil instance.

Duration

func (c *Config) Duration(key string) time.Duration

Retrieves a time.Duration value at the given key. Supports duration strings like “30s”, “5m”, “1h”.

Returns: 0 if key not found or on nil instance.

Example:

timeout := cfg.Duration("server.timeout")  // Parses "30s" to 30 * time.Second

Time

func (c *Config) Time(key string) time.Time

Retrieves a time.Time value at the given key.

Returns: Zero time (time.Time{}) if key not found or on nil instance.

StringSlice

func (c *Config) StringSlice(key string) []string

Retrieves a string slice at the given key.

Returns: Empty slice []string{} (not nil) if key not found or on nil instance.

Example:

hosts := cfg.StringSlice("servers")  // []string{} if missing

IntSlice

func (c *Config) IntSlice(key string) []int

Retrieves an integer slice at the given key.

Returns: Empty slice []int{} (not nil) if key not found or on nil instance.

StringMap

func (c *Config) StringMap(key string) map[string]any

Retrieves a map at the given key.

Returns: Empty map map[string]any{} (not nil) if key not found or on nil instance.

Example:

metadata := cfg.StringMap("metadata")  // map[string]any{} if missing

Getter Methods with Defaults

StringOr

func (c *Config) StringOr(key, defaultVal string) string

Retrieves a string value or returns the default if not found.

Example:

host := cfg.StringOr("server.host", "localhost")

IntOr

func (c *Config) IntOr(key string, defaultVal int) int

Retrieves an integer value or returns the default if not found.

Example:

port := cfg.IntOr("server.port", 8080)

Int64Or

func (c *Config) Int64Or(key string, defaultVal int64) int64

Retrieves an int64 value or returns the default if not found.

Float64Or

func (c *Config) Float64Or(key string, defaultVal float64) float64

Retrieves a float64 value or returns the default if not found.

BoolOr

func (c *Config) BoolOr(key string, defaultVal bool) bool

Retrieves a boolean value or returns the default if not found.

Example:

debug := cfg.BoolOr("debug", false)

DurationOr

func (c *Config) DurationOr(key string, defaultVal time.Duration) time.Duration

Retrieves a duration value or returns the default if not found.

Example:

timeout := cfg.DurationOr("timeout", 30*time.Second)

TimeOr

func (c *Config) TimeOr(key string, defaultVal time.Time) time.Time

Retrieves a time.Time value or returns the default if not found.

StringSliceOr

func (c *Config) StringSliceOr(key string, defaultVal []string) []string

Retrieves a string slice or returns the default if not found.

IntSliceOr

func (c *Config) IntSliceOr(key string, defaultVal []int) []int

Retrieves an integer slice or returns the default if not found.

StringMapOr

func (c *Config) StringMapOr(key string, defaultVal map[string]any) map[string]any

Retrieves a map or returns the default if not found.

Generic Getter Functions

GetE

func GetE[T any](c *Config, key string) (T, error)

Generic getter that returns the value and an error. Useful for custom types and explicit error handling.

Type parameters:

  • T - Target type

Parameters:

  • c - Config instance
  • key - Dot-separated path

Returns:

  • T - Value at the key (zero value if error)
  • error - Error if key not found, type mismatch, or nil instance

Example:

port, err := config.GetE[int](cfg, "server.port")
if err != nil {
    log.Printf("invalid port: %v", err)
    port = 8080
}

// Custom type
type DatabaseConfig struct {
    Host string
    Port int
}

dbConfig, err := config.GetE[DatabaseConfig](cfg, "database")

GetOr

func GetOr[T any](c *Config, key string, defaultVal T) T

Generic getter that returns the value or a default if not found.

Example:

port := config.GetOr(cfg, "server.port", 8080)

Get

func Get[T any](c *Config, key string) T

Generic getter that returns the value or zero value if not found.

Example:

port := config.Get[int](cfg, "server.port")  // 0 if missing

Data Access Methods

Values

func (c *Config) Values() *map[string]any

Returns a pointer to the internal configuration map.

Returns: nil if Config instance is nil

Warning: Direct modification of the returned map is not recommended. Use for read-only operations.

Example:

values := cfg.Values()
if values != nil {
    fmt.Printf("Config data: %+v\n", *values)
}

Nil-Safety Guarantees

All getter methods handle nil Config instances gracefully:

var cfg *config.Config  // nil

// Short methods return zero values
cfg.String("key")       // Returns ""
cfg.Int("key")          // Returns 0
cfg.Bool("key")         // Returns false
cfg.StringSlice("key")  // Returns []string{}
cfg.StringMap("key")    // Returns map[string]any{}

// Error methods return errors
port, err := config.GetE[int](cfg, "key")
// err: "config instance is nil"

Thread Safety

Thread-safe operations:

  • Load() - Uses internal locking
  • All getter methods - Read-only operations are safe
  • Multiple goroutines can call Load() and getters concurrently

Not thread-safe:

  • Concurrent modification during initialization
  • Direct modification of values returned by Values()

Error Handling Patterns

Pattern 1: Simple Access

port := cfg.Int("server.port")  // Use zero value as implicit default

Pattern 2: Explicit Defaults

port := cfg.IntOr("server.port", 8080)  // Explicit default

Pattern 3: Error Handling

port, err := config.GetE[int](cfg, "server.port")
if err != nil {
    return fmt.Errorf("invalid port: %w", err)
}

Pattern 4: Load Errors

if err := cfg.Load(context.Background()); err != nil {
    var configErr *config.ConfigError
    if errors.As(err, &configErr) {
        log.Printf("Config error in %s during %s: %v",
            configErr.Source, configErr.Operation, configErr.Err)
    }
    return err
}

Performance Characteristics

OperationComplexityNotes
Get(key)O(n)n = depth of dot notation path
String(key), Int(key), etc.O(n)Uses Get() internally
Load()O(s × m)s = number of sources, m = data size
Dump()O(d × m)d = number of dumpers, m = data size

Next Steps

1.2 - Options Reference

Complete reference for all configuration option functions

Comprehensive documentation of all option functions used to configure Config instances.

Option Type

type Option func(*Config) error

Options are functions that configure a Config instance during initialization. They are passed to New() or MustNew().

Environment Variable Expansion

All path-based options (WithFile, WithFileAs, WithConsul, WithConsulAs, WithFileDumper, WithFileDumperAs) support environment variable expansion in paths. This makes it easy to use different paths based on your environment.

Supported syntax:

  • ${VAR} - Braced variable name
  • $VAR - Simple variable name

Note: Shell-style defaults like ${VAR:-default} are NOT supported. Set defaults in your code before calling the option.

Examples:

// Environment-based Consul path
config.WithConsul("${APP_ENV}/service.yaml")
// When APP_ENV=production, expands to: "production/service.yaml"

// Config directory from environment
config.WithFile("${CONFIG_DIR}/app.yaml")
// When CONFIG_DIR=/etc/myapp, expands to: "/etc/myapp/app.yaml"

// Multiple variables
config.WithFile("${REGION}/${ENV}/settings.yaml")
// When REGION=us-west and ENV=staging, expands to: "us-west/staging/settings.yaml"

// Output directory
config.WithFileDumper("${LOG_DIR}/effective-config.yaml")
// When LOG_DIR=/var/log, expands to: "/var/log/effective-config.yaml"

Handling unset variables:

If an environment variable is not set, it expands to an empty string:

// If APP_ENV is not set:
config.WithConsul("${APP_ENV}/service.yaml")  // Expands to: "/service.yaml"

To provide defaults, set them in your code:

if os.Getenv("APP_ENV") == "" {
    os.Setenv("APP_ENV", "development")
}
config.WithConsul("${APP_ENV}/service.yaml")  // Uses "development" if not set

Source Options

Source options specify where configuration data comes from.

WithFile

func WithFile(path string) Option

Loads configuration from a file with automatic format detection based on extension.

Parameters:

  • path - Path to configuration file.

Supported extensions:

  • .json - JSON format.
  • .yaml, .yml - YAML format.
  • .toml - TOML format.

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFile("config.json"),
)

Error conditions:

  • File does not exist (error occurs during Load(), not initialization)
  • Extension not recognized

WithFileAs

func WithFileAs(path string, codecType codec.Type) Option

Loads configuration from a file with explicit format specification.

Parameters:

  • path - Path to configuration file.
  • codecType - Codec type like codec.TypeYAML or codec.TypeJSON.

Example:

cfg := config.MustNew(
    config.WithFileAs("config.txt", codec.TypeYAML),
    config.WithFileAs("settings.conf", codec.TypeJSON),
)

Use when: File extension doesn’t match its format.

WithEnv

func WithEnv(prefix string) Option

Loads configuration from environment variables with the given prefix.

Parameters:

  • prefix - Prefix to filter environment variables (e.g., “APP_”, “MYAPP_”)

Naming convention:

  • PREFIX_KEYkey
  • PREFIX_SECTION_KEYsection.key
  • PREFIX_A_B_Ca.b.c

Example:

cfg := config.MustNew(
    config.WithEnv("MYAPP_"),
)

// Environment: MYAPP_SERVER_PORT=8080
// Maps to: server.port = 8080

See also: Environment Variables Guide

WithConsul

func WithConsul(path string) Option

Loads configuration from HashiCorp Consul. The format is detected from the file extension.

Works without Consul: If CONSUL_HTTP_ADDR isn’t set, this option does nothing. This means you can run your app locally without Consul. When you deploy to production, just set the environment variable and Consul will be used.

Parameters:

  • path - Consul key path (format detected from extension)

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithConsul("production/service.json"),  // Skipped in dev, used in prod
)

Environment variables:

  • CONSUL_HTTP_ADDR - Consul server address (required for Consul to work)
  • CONSUL_HTTP_TOKEN - Access token for authentication (optional)

WithConsulAs

func WithConsulAs(path string, codecType codec.Type) Option

Loads configuration from Consul with explicit format. Use this when the key path doesn’t have an extension.

Works without Consul: Like WithConsul, this option does nothing if CONSUL_HTTP_ADDR isn’t set. Your code works the same in dev and prod.

Parameters:

  • path - Consul key path
  • codecType - Codec type (like codec.TypeYAML or codec.TypeJSON)

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithConsulAs("config/app", codec.TypeYAML),  // No extension in key
)

Environment variables:

  • CONSUL_HTTP_ADDR - Consul server address (required for Consul to work)
  • CONSUL_HTTP_TOKEN - Access token for authentication (optional)

WithContent

func WithContent(data []byte, codecType codec.Type) Option

Loads configuration from a byte slice.

Parameters:

  • data - Configuration data as bytes
  • codecType - Codec type for decoding

Example:

configData := []byte(`{"server": {"port": 8080}}`)
cfg := config.MustNew(
    config.WithContent(configData, codec.TypeJSON),
)

Use cases:

  • Testing
  • Dynamic configuration
  • Embedded configuration

WithSource

func WithSource(loader Source) Option

Adds a custom configuration source.

Parameters:

  • loader - Custom source implementing the Source interface

Source interface:

type Source interface {
    Load(ctx context.Context) (map[string]any, error)
}

Example:

type CustomSource struct{}

func (s *CustomSource) Load(ctx context.Context) (map[string]any, error) {
    return map[string]any{"key": "value"}, nil
}

cfg := config.MustNew(
    config.WithSource(&CustomSource{}),
)

Validation Options

Validation options enable configuration validation.

WithBinding

func WithBinding(v any) Option

Binds configuration to a Go struct and optionally validates it.

Parameters:

  • v - Pointer to struct to bind configuration to

Example:

type Config struct {
    Port int `config:"port"`
}

var cfg Config
config := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithBinding(&cfg),
)

Validation: If the struct implements Validate() error, it will be called after binding.

Requirements:

  • Must pass a pointer to the struct
  • Struct fields must have config:"name" tags

See also: Struct Binding Guide

WithTag

func WithTag(tagName string) Option

Changes the struct tag name used for binding (default: “config”).

Parameters:

  • tagName - Tag name to use instead of “config”

Example:

type Config struct {
    Port int `yaml:"port"`
}

var cfg Config
config := config.MustNew(
    config.WithTag("yaml"),
    config.WithBinding(&cfg),
)

Use when: You want to reuse existing struct tags (e.g., json, yaml).

WithValidator

func WithValidator(fn func(map[string]any) error) Option

Registers a custom validation function for the configuration map.

Parameters:

  • fn - Validation function that receives the merged configuration

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithValidator(func(data map[string]any) error {
        port, ok := data["port"].(int)
        if !ok || port <= 0 {
            return errors.New("port must be a positive integer")
        }
        return nil
    }),
)

Timing: Validation runs after sources are merged, before struct binding.

Multiple validators: You can register multiple validators; all will be executed.

WithJSONSchema

func WithJSONSchema(schema []byte) Option

Validates configuration against a JSON Schema.

Parameters:

  • schema - JSON Schema as bytes

Example:

schemaBytes, _ := os.ReadFile("schema.json")
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithJSONSchema(schemaBytes),
)

Schema validation:

See also: Validation Guide

Dumper Options

Dumper options specify where to write configuration.

WithFileDumper

func WithFileDumper(path string) Option

Writes configuration to a file with automatic format detection.

Parameters:

  • path - Output file path (format detected from extension)

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithFileDumper("effective-config.yaml"),
)

cfg.Load(context.Background())
cfg.Dump(context.Background())  // Writes to effective-config.yaml

Default permissions: 0644 (owner read/write, group/others read)

WithFileDumperAs

func WithFileDumperAs(path string, codecType codec.Type) Option

Writes configuration to a file with explicit format specification.

Parameters:

  • path - Output file path
  • codecType - Codec type for encoding

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFileDumperAs("output.json", codec.TypeJSON),
)

WithDumper

func WithDumper(dumper Dumper) Option

Adds a custom configuration dumper.

Parameters:

  • dumper - Custom dumper implementing the Dumper interface

Dumper interface:

type Dumper interface {
    Dump(ctx context.Context, data map[string]any) error
}

Example:

type CustomDumper struct{}

func (d *CustomDumper) Dump(ctx context.Context, data map[string]any) error {
    // Write data somewhere
    return nil
}

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithDumper(&CustomDumper{}),
)

Option Composition

Options are applied in the order they are passed to New() or MustNew():

cfg := config.MustNew(
    // 1. Load base config
    config.WithFile("config.yaml"),
    
    // 2. Load environment-specific config
    config.WithFile("config.prod.yaml"),
    
    // 3. Override with environment variables (highest priority)
    config.WithEnv("APP_"),
    
    // 4. Set up validation
    config.WithJSONSchema(schemaBytes),
    config.WithValidator(customValidation),
    
    // 5. Bind to struct
    config.WithBinding(&appConfig),
    
    // 6. Set up dumper
    config.WithFileDumper("effective-config.yaml"),
)

Source Precedence

When multiple sources are configured, later sources override earlier ones:

cfg := config.MustNew(
    config.WithFile("config.yaml"),      // Priority 1 (lowest)
    config.WithFile("config.prod.yaml"), // Priority 2
    config.WithEnv("APP_"),              // Priority 3 (highest)
)

Validation Order

Validation happens in this sequence during Load():

  1. Load and merge all sources
  2. JSON Schema validation (if configured)
  3. Custom validation functions (if configured)
  4. Struct binding (if configured)
  5. Struct Validate() method (if implemented)

Common Patterns

Pattern 1: Basic Configuration

cfg := config.MustNew(
    config.WithFile("config.yaml"),
)

Pattern 2: Environment Override

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
)

Pattern 3: Multi-Environment

env := os.Getenv("APP_ENV")
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithFile("config."+env+".yaml"),
    config.WithEnv("APP_"),
)

Pattern 4: With Validation

var appConfig AppConfig
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithBinding(&appConfig),
)

Pattern 5: Production Setup

var appConfig AppConfig
schemaBytes, _ := os.ReadFile("schema.json")

cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithJSONSchema(schemaBytes),
    config.WithBinding(&appConfig),
    config.WithFileDumper("effective-config.yaml"),
)

Next Steps

1.3 - Codecs Reference

Built-in codecs for configuration format support and type conversion

Complete reference for built-in codecs and guidance on creating custom codecs.

Codec Interface

type Codec interface {
    Encode(v any) ([]byte, error)
    Decode(data []byte, v any) error
}

Codecs handle encoding and decoding of configuration data between different formats.

Built-in Format Codecs

JSON Codec

Type: codec.TypeJSON
Import: rivaas.dev/config/codec

Handles JSON format encoding and decoding.

Capabilities:

  • ✅ Encode
  • ✅ Decode

File extensions:

  • .json

Example:

import "rivaas.dev/config/codec"

cfg := config.MustNew(
    config.WithFileAs("config.txt", codec.TypeJSON),
)

Features:

  • Standard Go encoding/json implementation
  • Preserves JSON types (numbers, strings, booleans, arrays, objects)
  • Pretty-printed output when encoding

YAML Codec

Type: codec.TypeYAML
Import: rivaas.dev/config/codec

Handles YAML format encoding and decoding.

Capabilities:

  • ✅ Encode
  • ✅ Decode

File extensions:

  • .yaml
  • .yml

Example:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
)

Features:

  • Uses gopkg.in/yaml.v3
  • Supports YAML 1.2 features
  • Handles anchors and aliases
  • Preserves indentation on encoding

Common YAML types:

string_value: "hello"
number_value: 42
boolean_value: true
duration_value: 30s
list_value:
  - item1
  - item2
map_value:
  key1: value1
  key2: value2

TOML Codec

Type: codec.TypeTOML
Import: rivaas.dev/config/codec

Handles TOML format encoding and decoding.

Capabilities:

  • ✅ Encode
  • ✅ Decode

File extensions:

  • .toml

Example:

cfg := config.MustNew(
    config.WithFile("config.toml"),
)

Features:

Sample TOML:

[server]
host = "localhost"
port = 8080

[database]
host = "db.example.com"
port = 5432

Environment Variable Codec

Type: codec.TypeEnvVar
Import: rivaas.dev/config/codec

Handles environment variable format.

Capabilities:

  • ❌ Encode (returns error)
  • ✅ Decode

Example:

cfg := config.MustNew(
    config.WithEnv("APP_"),
)

Format:

PREFIX_SECTION_KEY=value
PREFIX_A_B_C=nested

Transformation:

  • Strips prefix
  • Converts to lowercase
  • Splits by underscores
  • Creates nested structure

See: Environment Variables Guide

Built-in Caster Codecs

Caster codecs provide automatic type conversion for getter methods.

Boolean Caster

Type: codec.TypeCasterBool

Converts values to bool.

Supported inputs:

  • true, "true", "True", "TRUE", 1, "1"true
  • false, "false", "False", "FALSE", 0, "0"false

Example:

debug := cfg.Bool("debug")  // Uses BoolCaster internally

Integer Casters

Convert values to integer types.

TypeCodecTarget Type
codec.TypeCasterIntIntint
codec.TypeCasterInt8Int8int8
codec.TypeCasterInt16Int16int16
codec.TypeCasterInt32Int32int32
codec.TypeCasterInt64Int64int64

Supported inputs:

  • Integer values: 42, 100
  • String integers: "42", "100"
  • Float values: 42.042
  • String floats: "42.0"42

Example:

port := cfg.Int("server.port")      // Uses IntCaster
timeout := cfg.Int64("timeout_ms")  // Uses Int64Caster

Unsigned Integer Casters

Convert values to unsigned integer types.

TypeCodecTarget Type
codec.TypeCasterUintUintuint
codec.TypeCasterUint8Uint8uint8
codec.TypeCasterUint16Uint16uint16
codec.TypeCasterUint32Uint32uint32
codec.TypeCasterUint64Uint64uint64

Supported inputs:

  • Positive integers: 42, 100
  • String integers: "42", "100"

Float Casters

Convert values to floating-point types.

TypeCodecTarget Type
codec.TypeCasterFloat32Float32float32
codec.TypeCasterFloat64Float64float64

Supported inputs:

  • Float values: 3.14, 2.5
  • String floats: "3.14", "2.5"
  • Integer values: 4242.0
  • String integers: "42"42.0

Example:

ratio := cfg.Float64("ratio")

String Caster

Type: codec.TypeCasterString

Converts any value to string.

Supported inputs:

  • String values: "hello""hello"
  • Numbers: 42"42"
  • Booleans: true"true"
  • Any value with String() method

Example:

value := cfg.String("key")  // Uses StringCaster internally

Time Caster

Type: codec.TypeCasterTime

Converts values to time.Time.

Supported inputs:

  • RFC3339 strings: "2025-01-01T00:00:00Z"
  • ISO8601 strings: "2025-01-01T00:00:00+00:00"
  • Unix timestamps: 1672531200

Example:

createdAt := cfg.Time("created_at")

Formats tried (in order):

  1. time.RFC3339 - "2006-01-02T15:04:05Z07:00"
  2. time.RFC3339Nano - "2006-01-02T15:04:05.999999999Z07:00"
  3. "2006-01-02" - Date only
  4. Unix timestamp (integer)

Duration Caster

Type: codec.TypeCasterDuration

Converts values to time.Duration.

Supported inputs:

  • Duration strings: "30s", "5m", "1h", "2h30m"
  • Integer nanoseconds: 3000000000030s
  • Float seconds: 2.52.5s

Example:

timeout := cfg.Duration("timeout")  // "30s" → 30 * time.Second

Duration units:

  • ns - nanoseconds
  • us or µs - microseconds
  • ms - milliseconds
  • s - seconds
  • m - minutes
  • h - hours

Codec Capabilities Table

CodecEncodeDecodeAuto-DetectExtensions
JSON.json
YAML.yaml, .yml
TOML.toml
EnvVar-
Bool-
Int*-
Uint*-
Float*-
String-
Time-
Duration-

Format Auto-Detection

The config package automatically detects formats based on file extensions:

cfg := config.MustNew(
    config.WithFile("config.json"),  // Auto-detects JSON
    config.WithFile("config.yaml"),  // Auto-detects YAML
    config.WithFile("config.toml"),  // Auto-detects TOML
)

Detection rules:

  1. Check file extension
  2. Look up registered decoder for that extension
  3. Use codec if found, error if not

Override auto-detection:

cfg := config.MustNew(
    config.WithFileAs("settings.txt", codec.TypeYAML),
)

Custom Codecs

Registering Custom Codecs

import "rivaas.dev/config/codec"

func init() {
    codec.RegisterEncoder("myformat", MyCodec{})
    codec.RegisterDecoder("myformat", MyCodec{})
}

Registration functions:

func RegisterEncoder(name string, encoder Codec)
func RegisterDecoder(name string, decoder Codec)

Custom Codec Example

type MyCodec struct{}

func (c MyCodec) Encode(v any) ([]byte, error) {
    data, ok := v.(map[string]any)
    if !ok {
        return nil, fmt.Errorf("expected map[string]any, got %T", v)
    }
    
    // Your encoding logic
    var buf bytes.Buffer
    // ... write to buf ...
    
    return buf.Bytes(), nil
}

func (c MyCodec) Decode(data []byte, v any) error {
    target, ok := v.(*map[string]any)
    if !ok {
        return fmt.Errorf("expected *map[string]any, got %T", v)
    }
    
    // Your decoding logic
    result := make(map[string]any)
    // ... parse data into result ...
    
    *target = result
    return nil
}

func init() {
    codec.RegisterEncoder("myformat", MyCodec{})
    codec.RegisterDecoder("myformat", MyCodec{})
}

See: Custom Codecs Guide

Common Patterns

Pattern 1: Mixed Formats

cfg := config.MustNew(
    config.WithFile("config.yaml"),  // YAML
    config.WithFile("secrets.json"), // JSON
    config.WithFile("extra.toml"),   // TOML
)

Pattern 2: Explicit Format

cfg := config.MustNew(
    config.WithFileAs("config.txt", codec.TypeYAML),
)

Pattern 3: Content Source

yamlData := []byte(`server: {port: 8080}`)
cfg := config.MustNew(
    config.WithContent(yamlData, codec.TypeYAML),
)

Pattern 4: Custom Codec

import _ "yourmodule/xmlcodec"  // Registers custom codec

cfg := config.MustNew(
    config.WithFileAs("config.xml", "xml"),
)

Type Conversion Examples

String to Duration

timeout: "30s"
timeout := cfg.Duration("timeout")  // 30 * time.Second

String to Int

port: "8080"
port := cfg.Int("port")  // 8080

String to Bool

debug: "true"
debug := cfg.Bool("debug")  // true

String to Time

created: "2025-01-01T00:00:00Z"
created := cfg.Time("created")  // time.Time

Error Handling

Decode Errors

if err := cfg.Load(context.Background()); err != nil {
    // Error format:
    // "config error in source[0] during load: yaml: unmarshal error"
    log.Printf("Failed to decode: %v", err)
}

Encode Errors

if err := cfg.Dump(context.Background()); err != nil {
    // Error format:
    // "config error in dumper[0] during dump: json: unsupported type"
    log.Printf("Failed to encode: %v", err)
}

Type Conversion Errors

// For error-returning methods
port, err := config.GetE[int](cfg, "server.port")
if err != nil {
    log.Printf("Invalid port: %v", err)
}

Performance Notes

  • JSON: Fast, minimal overhead
  • YAML: Moderate overhead (parsing complexity)
  • TOML: Fast, strict typing
  • Casters: Minimal overhead, optimized for common cases

Next Steps

1.4 - Troubleshooting

Common issues, solutions, and frequently asked questions

Solutions to common problems and frequently asked questions about the config package.

Configuration Loading Issues

File Not Found

Problem: Configuration file cannot be found.

config error in source[0] during load: open config.yaml: no such file or directory

Solutions:

  1. Check file path: Ensure the path is correct relative to where your application runs.
// Use absolute path if needed
cfg := config.MustNew(
    config.WithFile("/absolute/path/to/config.yaml"),
)
  1. Check working directory: Verify your application’s working directory.
wd, _ := os.Getwd()
fmt.Printf("Working directory: %s\n", wd)
  1. Make file optional: Handle missing files gracefully.
cfg, err := config.New(
    config.WithFile("config.yaml"),
)
if err != nil {
    log.Printf("Config file not found, using defaults: %v", err)
    // Use defaults
}

Format Not Recognized

Problem: File extension doesn’t match a known format.

config error in source[0] during load: no decoder registered for extension .conf

Solutions:

  1. Use explicit format:
cfg := config.MustNew(
    config.WithFileAs("config.conf", codec.TypeYAML),
)
  1. Register custom codec:
import _ "yourmodule/mycodec"  // Registers .conf format

Parse Errors

Problem: Configuration file has syntax errors.

config error in source[0] during load: yaml: unmarshal error

Solutions:

  1. Validate YAML/JSON syntax: Use online validators or linters
  2. Check indentation: YAML is indentation-sensitive
  3. Quote strings: Quote values with special characters
# Bad
url: http://example.com:8080

# Good
url: "http://example.com:8080"

Struct Binding Issues

Struct Not Populating

Problem: Struct fields remain at zero values after loading.

Solutions:

  1. Pass pointer to struct:
// Wrong
cfg := config.MustNew(config.WithBinding(myConfig))

// Correct
cfg := config.MustNew(config.WithBinding(&myConfig))
  1. Check struct tags:
// Config file: server.port = 8080
type Config struct {
    // Wrong - doesn't match config structure
    Port int `config:"port"`
    
    // Correct - matches nested structure
    Server struct {
        Port int `config:"port"`
    } `config:"server"`
}
  1. Verify tag names match config keys:
# config.yaml
server:
  host: localhost
  port: 8080
type Config struct {
    Server struct {
        Host string `config:"host"`  // Must match "host" in YAML
        Port int    `config:"port"`  // Must match "port" in YAML
    } `config:"server"`  // Must match "server" in YAML
}
  1. Export struct fields: Fields must be exported (start with uppercase)
// Wrong - unexported fields won't be populated
type Config struct {
    port int `config:"port"`
}

// Correct - exported field
type Config struct {
    Port int `config:"port"`
}

Type Mismatch Errors

Problem: Configuration value type doesn’t match struct field type.

Solutions:

  1. Use compatible types: Ensure types can be converted
# config.yaml
port: "8080"  # String
type Config struct {
    Port int `config:"port"`  // Will be converted from string
}
  1. Check slice vs scalar: Don’t mix slice and scalar values
# Wrong - port is an array but struct expects int
ports:
  - 8080
  - 8081
type Config struct {
    Ports []int `config:"ports"`  // Correct - expects slice
}

Validation Errors

Problem: Struct validation fails.

config error in binding during validate: port must be positive

Solutions:

  1. Check validation logic:
func (c *Config) Validate() error {
    if c.Port <= 0 {
        return fmt.Errorf("port must be positive, got %d", c.Port)
    }
    return nil
}
  1. Provide helpful error messages: Include the actual value in error

  2. Check validation order: Validation runs after binding

Environment Variable Issues

Environment Variables Not Loading

Problem: Environment variables are not being picked up.

Solutions:

  1. Check prefix: Ensure environment variables have the correct prefix
# Wrong - missing prefix
export SERVER_PORT=8080

# Correct - with MYAPP_ prefix
export MYAPP_SERVER_PORT=8080
cfg := config.MustNew(
    config.WithEnv("MYAPP_"),  // Must match prefix
)
  1. Verify environment variables are set:
env | grep MYAPP_
  1. Check variable names: Use underscores for nesting
# Maps to server.port
export MYAPP_SERVER_PORT=8080

# Maps to database.primary.host
export MYAPP_DATABASE_PRIMARY_HOST=localhost

Environment Variable Mapping Issues

Problem: Environment variables aren’t mapping to the right config keys.

Solutions:

  1. Understand naming convention:
Environment VariableConfig Path
MYAPP_SERVER_PORTserver.port
MYAPP_FOO_BAR_BAZfoo.bar.baz
MYAPP_FOO__BARfoo.bar (double underscore)
  1. Check case sensitivity: Environment variables are converted to lowercase
export MYAPP_SERVER_PORT=8080  # Becomes: server.port
  1. Test mapping:
cfg := config.MustNew(
    config.WithEnv("MYAPP_"),
)
cfg.Load(context.Background())

// Print effective configuration
values := cfg.Values()
fmt.Printf("Config: %+v\n", *values)

Type Conflicts

Problem: Environment variable creates conflict between scalar and nested.

export MYAPP_FOO=scalar
export MYAPP_FOO_BAR=nested

Solution: Nested structures take precedence. Result is foo.bar = "nested", scalar foo is overwritten.

Best practice: Don’t create such conflicts; structure your configuration hierarchically.

Validation Issues

Schema Validation Failures

Problem: JSON Schema validation fails.

config error in json-schema during validate: server.port: must be >= 1

Solutions:

  1. Check schema requirements: Ensure configuration meets schema constraints

  2. Debug with schema validator: Use online JSON Schema validators

  3. Provide all required fields:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": ["server", "database"]
}

Custom Validation Errors

Problem: Custom validation function fails.

Solutions:

  1. Add detailed error messages:
config.WithValidator(func(data map[string]any) error {
    port, ok := data["port"].(int)
    if !ok {
        return fmt.Errorf("port must be an integer, got %T", data["port"])
    }
    if port < 1 || port > 65535 {
        return fmt.Errorf("port must be 1-65535, got %d", port)
    }
    return nil
})
  1. Check data types: Values in map might not be expected type
// Type assertion with check
if port, ok := data["port"].(int); ok {
    // Use port
}

Performance Issues

Slow Configuration Loading

Problem: Configuration loading takes too long.

Solutions:

  1. Reduce source count: Combine configuration files when possible

  2. Avoid remote sources in hot paths: Cache remote configuration

  3. Profile loading:

start := time.Now()
err := cfg.Load(context.Background())
log.Printf("Config load time: %v", time.Since(start))
  1. Load once: Load configuration during initialization, not per-request

Memory Usage

Problem: High memory usage.

Solutions:

  1. Don’t keep multiple Config instances: Reuse single instance

  2. Clear unnecessary dumpers: Only use dumpers when needed

// Development only
if debug {
    cfg = config.MustNew(
        config.WithFile("config.yaml"),
        config.WithFileDumper("debug-config.yaml"),
    )
}

Common Misconceptions

Q: Why don’t changes to config files take effect?

A: Configuration is loaded once during Load(). It’s not automatically reloaded when files change.

Solution: Reload configuration explicitly:

// Reload configuration
if err := cfg.Load(context.Background()); err != nil {
    log.Printf("Failed to reload: %v", err)
}

Q: Why does my config work locally but not in Docker?

A: Likely a path or working directory issue.

Solutions:

  1. Use absolute paths in Docker:
cfg := config.MustNew(
    config.WithFile("/app/config/config.yaml"),
)
  1. Set working directory in Dockerfile:
WORKDIR /app
COPY config.yaml .
  1. Use environment variables for container configuration:
cfg := config.MustNew(
    config.WithFile("config.yaml"),     // Defaults
    config.WithEnv("APP_"),             // Override in container
)

Q: Can I modify configuration at runtime?

A: The Config instance is read-only after loading. You need to reload to pick up changes.

Pattern for dynamic updates:

type ConfigManager struct {
    cfg *config.Config
    mu  sync.RWMutex
}

func (cm *ConfigManager) Reload(ctx context.Context) error {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    return cm.cfg.Load(ctx)
}

func (cm *ConfigManager) Get(key string) any {
    cm.mu.RLock()
    defer cm.mu.RUnlock()
    return cm.cfg.Get(key)
}

FAQ

Q: Is Config thread-safe?

A: Yes, Load() and all getter methods are thread-safe.

Q: What happens with nil Config instances?

A: Getter methods return zero values, error methods return errors. No panics.

Q: Can I load from multiple sources?

A: Yes, sources are merged with later sources overriding earlier ones.

Q: How do I handle secrets?

A:

  1. Use environment variables for secrets (not config files)
  2. Use secret management systems (Vault, AWS Secrets Manager)
  3. Never commit secrets to version control

Q: Can I use the same struct tags for JSON and config?

A: Yes, using WithTag():

type Config struct {
    Port int `json:"port"`
}

cfg := config.MustNew(
    config.WithTag("json"),
    config.WithBinding(&myConfig),
)

Q: How do I debug configuration loading?

A:

  1. Use WithFileDumper() to see merged config
  2. Print values after loading
  3. Check error messages for source context
cfg := config.MustNew(
    config.WithFile("config.yaml"),
    config.WithEnv("APP_"),
    config.WithFileDumper("debug-config.yaml"),
)

if err := cfg.Load(context.Background()); err != nil {
    log.Printf("Load error: %v", err)
}

cfg.Dump(context.Background())  // Writes to debug-config.yaml

values := cfg.Values()
fmt.Printf("Loaded config: %+v\n", *values)

Q: What’s the difference between Get, GetE, and GetOr?

A:

  • Get[T]() - Returns value or zero value (no error)
  • GetE[T]() - Returns value and error
  • GetOr[T]() - Returns value or provided default

Q: Can I use config without struct binding?

A: Yes, use getter methods directly:

cfg := config.MustNew(
    config.WithFile("config.yaml"),
)
cfg.Load(context.Background())

port := cfg.Int("server.port")
host := cfg.String("server.host")

Q: How do I validate required fields?

A: Use struct validation:

func (c *Config) Validate() error {
    if c.Database.Host == "" {
        return errors.New("database.host is required")
    }
    return nil
}

Performance Notes

Configuration access:

  • Getter methods: O(n) where n = dot notation depth
  • Direct Get(): O(n)
  • No caching of individual keys

Best practices:

  • Load configuration once at startup
  • Cache frequently accessed values in local variables
  • Use struct binding for best performance

Thread safety overhead:

  • Minimal locking overhead
  • Read operations are concurrent
  • Write operations (Load) use exclusive lock

Getting Help

If you encounter issues not covered here:

  1. Check the Configuration Guide
  2. Review API Reference
  3. Search GitHub Issues
  4. Ask in the community forums

When reporting issues, include:

  • Go version
  • Config package version
  • Minimal reproducible example
  • Error messages
  • Expected vs actual behavior

2 - Binding Package

Complete API reference for the rivaas.dev/binding package

This is the API reference for the rivaas.dev/binding package. For learning-focused documentation, see the Binding Guide.

Package Information

Overview

The binding package provides a high-performance, type-safe way to bind request data from various sources (query parameters, JSON bodies, headers, etc.) into Go structs using struct tags.

import "rivaas.dev/binding"

type CreateUserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

// Generic API (preferred)
user, err := binding.JSON[CreateUserRequest](body)

Key Features

  • Type-Safe Generic API: Compile-time type safety with zero runtime overhead
  • Multiple Sources: Query, path, form, header, cookie, JSON, XML, YAML, TOML, MessagePack, Protocol Buffers
  • Zero Allocation: Struct reflection info cached for optimal performance
  • Flexible Type Support: Primitives, time types, collections, nested structs, custom types
  • Detailed Errors: Field-level error information with context
  • Extensible: Custom type converters and value getters
  • Multi-Source Binding: Combine data from multiple sources with precedence control

Package Structure

graph TB
    A[binding]:::info --> B[Core API]:::warning
    A --> C[Sub-Packages]:::success
    
    B --> B1[JSON/XML/Form]
    B --> B2[Query/Header/Cookie]
    B --> B3[Multi-Source]
    B --> B4[Custom Binders]
    
    C --> C1[yaml]
    C --> C2[toml]
    C --> C3[msgpack]
    C --> C4[proto]
    
    classDef default fill:#F8FAF9,stroke:#1E6F5C,color:#1F2A27
    classDef info fill:#D1ECF1,stroke:#17A2B8,color:#1F2A27
    classDef success fill:#D4EDDA,stroke:#28A745,color:#1F2A27
    classDef warning fill:#FFF3CD,stroke:#FFC107,color:#1F2A27

Quick Navigation

API Reference

Core types, functions, and interfaces for request binding.

View →

Options

Configuration options and binding settings.

View →

Sub-Packages

YAML, TOML, MessagePack, and Protocol Buffers support.

View →

Troubleshooting

Common issues and solutions for binding problems.

View →

User Guide

Step-by-step tutorials and examples.

View →

Core API

Generic Functions

Type-safe binding with compile-time guarantees:

// JSON binding
func JSON[T any](data []byte, opts ...Option) (T, error)

// Query parameter binding
func Query[T any](values url.Values, opts ...Option) (T, error)

// Form data binding
func Form[T any](values url.Values, opts ...Option) (T, error)

// Header binding
func Header[T any](headers http.Header, opts ...Option) (T, error)

// Cookie binding
func Cookie[T any](cookies []*http.Cookie, opts ...Option) (T, error)

// Path parameter binding
func Path[T any](params map[string]string, opts ...Option) (T, error)

// XML binding
func XML[T any](data []byte, opts ...Option) (T, error)

// Multi-source binding
func Bind[T any](sources ...Source) (T, error)

Non-Generic Functions

For cases where type comes from a variable:

// JSON binding to pointer
func JSONTo(data []byte, target interface{}, opts ...Option) error

// Query binding to pointer
func QueryTo(values url.Values, target interface{}, opts ...Option) error

// ... similar for other sources

Reader Variants

Stream from io.Reader for large payloads:

func JSONReader[T any](r io.Reader, opts ...Option) (T, error)
func XMLReader[T any](r io.Reader, opts ...Option) (T, error)

Type System

Built-in Type Support

CategoryTypes
Primitivesstring, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool
Timetime.Time, time.Duration
Networknet.IP, net.IPNet, url.URL
Regexregexp.Regexp
Collections[]T, map[string]T
Pointers*T for any supported type
NestedNested structs with dot notation

Custom Types

Register custom converters for unsupported types:

import "github.com/google/uuid"

binder := binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
)

Struct Tags

Control binding behavior with struct tags:

TagPurposeExample
jsonJSON body fieldjson:"field_name"
queryQuery parameterquery:"param_name"
formForm dataform:"field_name"
headerHTTP headerheader:"X-Header-Name"
pathPath parameterpath:"param_name"
cookieHTTP cookiecookie:"cookie_name"
defaultDefault valuedefault:"value"
validateValidation rulesvalidate:"required,email"

Error Types

BindError

Field-specific binding error:

type BindError struct {
    Field  string // Field name
    Source string // Source ("query", "json", etc.)
    Value  string // Raw value
    Type   string // Expected type
    Reason string // Error reason
    Err    error  // Underlying error
}

UnknownFieldError

Unknown fields in strict mode:

type UnknownFieldError struct {
    Fields []string // List of unknown fields
}

MultiError

Multiple errors with WithAllErrors():

type MultiError struct {
    Errors []*BindError
}

Configuration Options

Common options for all binding functions:

// Security limits
binding.WithMaxDepth(16)        // Max struct nesting
binding.WithMaxSliceLen(1000)   // Max slice elements
binding.WithMaxMapSize(500)     // Max map entries

// Unknown fields
binding.WithStrictJSON()         // Fail on unknown fields
binding.WithUnknownFields(mode)  // UnknownError/UnknownWarn/UnknownIgnore

// Slice parsing
binding.WithSliceMode(mode)      // SliceRepeat or SliceCSV

// Error collection
binding.WithAllErrors()          // Collect all errors instead of failing on first

Reusable Binders

Create configured binder instances:

binder := binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithTimeLayouts("2006-01-02", "01/02/2006"),
    binding.WithMaxDepth(16),
)

// Use across handlers
user, err := binder.JSON[User](body)
params, err := binder.Query[Params](values)

Sub-Packages

Additional format support via sub-packages:

PackageFormatImport Path
yamlYAMLrivaas.dev/binding/yaml
tomlTOMLrivaas.dev/binding/toml
msgpackMessagePackrivaas.dev/binding/msgpack
protoProtocol Buffersrivaas.dev/binding/proto

Performance Characteristics

  • First binding: ~500ns overhead for reflection
  • Subsequent bindings: ~50ns overhead (cache lookup)
  • Query/Path/Form: Zero allocations for primitive types
  • JSON/XML: Allocations depend on encoding/json and encoding/xml
  • Thread-safe: All operations are safe for concurrent use

Integration

With net/http

func Handler(w http.ResponseWriter, r *http.Request) {
    req, err := binding.JSON[CreateUserRequest](r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // Process request...
}

With rivaas.dev/router

import "rivaas.dev/router"

func Handler(c *router.Context) error {
    req, err := binding.JSON[CreateUserRequest](c.Request().Body)
    if err != nil {
        return c.JSON(http.StatusBadRequest, err)
    }
    return c.JSON(http.StatusOK, processRequest(req))
}

With rivaas.dev/app

import "rivaas.dev/app"

func Handler(c *app.Context) error {
    var req CreateUserRequest
    if err := c.Bind(&req); err != nil {
        return err  // Automatically handled
    }
    return c.JSON(http.StatusOK, processRequest(req))
}

Version Compatibility

The binding package follows semantic versioning:

  • v1.x: Stable API, backward compatible
  • v2.x: Major changes, may require code updates

See Also


For step-by-step guides and tutorials, see the Binding Guide.

For real-world examples, see the Examples page.

2.1 - API Reference

Complete API documentation for all types, functions, and interfaces

Detailed API reference for all exported types, functions, and interfaces in the rivaas.dev/binding package.

Core Binding Functions

Generic API

JSON

func JSON[T any](data []byte, opts ...Option) (T, error)

Binds JSON data to a struct of type T.

Parameters:

  • data: JSON bytes to parse.
  • opts: Optional configuration options.

Returns:

  • Populated struct of type T.
  • Error if binding fails.

Example:

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

user, err := binding.JSON[User](jsonData)

JSONReader

func JSONReader[T any](r io.Reader, opts ...Option) (T, error)

Binds JSON from an io.Reader. More memory-efficient for large payloads.

Example:

user, err := binding.JSONReader[User](r.Body)

Query

func Query[T any](values url.Values, opts ...Option) (T, error)

Binds URL query parameters to a struct.

Parameters:

  • values: URL query values. Use r.URL.Query().
  • opts: Optional configuration options.

Example:

type Params struct {
    Page  int      `query:"page" default:"1"`
    Limit int      `query:"limit" default:"20"`
    Tags  []string `query:"tags"`
}

params, err := binding.Query[Params](r.URL.Query())

Form

func Form[T any](values url.Values, opts ...Option) (T, error)

Binds form data to a struct.

Parameters:

  • values: Form values (r.Form or r.PostForm)
  • opts: Optional configuration options

Example:

type LoginForm struct {
    Username string `form:"username"`
    Password string `form:"password"`
}

form, err := binding.Form[LoginForm](r.PostForm)

Multipart

func Multipart[T any](form *multipart.Form, opts ...Option) (T, error)

Binds multipart form data including file uploads to a struct. Use *binding.File type for file fields.

Parameters:

  • form: Multipart form from r.MultipartForm after calling r.ParseMultipartForm()
  • opts: Optional configuration options

Returns:

  • Populated struct of type T with form fields and files
  • Error if binding fails

Example:

type UploadRequest struct {
    File        *binding.File `form:"file"`
    Title       string        `form:"title"`
    Description string        `form:"description"`
    Tags        []string      `form:"tags"`
}

// Parse multipart form first (32MB limit)
if err := r.ParseMultipartForm(32 << 20); err != nil {
    return err
}

req, err := binding.Multipart[UploadRequest](r.MultipartForm)
if err != nil {
    return err
}

// Save the uploaded file
if err := req.File.Save("/uploads/" + req.File.Name); err != nil {
    return err
}

Multiple files:

type GalleryUpload struct {
    Photos []*binding.File `form:"photos"`
    Title  string          `form:"title"`
}

req, err := binding.Multipart[GalleryUpload](r.MultipartForm)
for _, photo := range req.Photos {
    photo.Save("/uploads/" + photo.Name)
}

JSON in form fields:

Multipart binding automatically parses JSON strings from form fields into nested structs:

type Settings struct {
    Theme         string `json:"theme"`
    Notifications bool   `json:"notifications"`
}

type ProfileUpdate struct {
    Avatar   *binding.File `form:"avatar"`
    Settings Settings      `form:"settings"` // JSON automatically parsed
}

// Form field "settings" contains: {"theme":"dark","notifications":true}
req, err := binding.Multipart[ProfileUpdate](r.MultipartForm)
// req.Settings is now populated from the JSON string
func Header[T any](headers http.Header, opts ...Option) (T, error)

Binds HTTP headers to a struct.

Example:

type Headers struct {
    APIKey    string `header:"X-API-Key"`
    RequestID string `header:"X-Request-ID"`
}

headers, err := binding.Header[Headers](r.Header)
func Cookie[T any](cookies []*http.Cookie, opts ...Option) (T, error)

Binds HTTP cookies to a struct.

Example:

type Cookies struct {
    SessionID string `cookie:"session_id"`
    Theme     string `cookie:"theme" default:"light"`
}

cookies, err := binding.Cookie[Cookies](r.Cookies())

Path

func Path[T any](params map[string]string, opts ...Option) (T, error)

Binds URL path parameters to a struct.

Example:

type PathParams struct {
    UserID int `path:"user_id"`
}

// With gorilla/mux or chi
params, err := binding.Path[PathParams](mux.Vars(r))

XML

func XML[T any](data []byte, opts ...Option) (T, error)

Binds XML data to a struct.

Example:

type Document struct {
    Title string `xml:"title"`
    Body  string `xml:"body"`
}

doc, err := binding.XML[Document](xmlData)

XMLReader

func XMLReader[T any](r io.Reader, opts ...Option) (T, error)

Binds XML from an io.Reader.

Bind (Multi-Source)

func Bind[T any](sources ...Source) (T, error)

Binds from multiple sources with precedence.

Example:

type Request struct {
    UserID int    `query:"user_id" json:"user_id"`
    APIKey string `header:"X-API-Key"`
}

req, err := binding.Bind[Request](
    binding.FromQuery(r.URL.Query()),
    binding.FromJSON(r.Body),
    binding.FromHeader(r.Header),
)

Non-Generic API

JSONTo

func JSONTo(data []byte, target interface{}, opts ...Option) error

Binds JSON to a pointer. Use when type comes from a variable.

Example:

var user User
err := binding.JSONTo(jsonData, &user)

Similar non-generic functions exist for all sources:

  • QueryTo(values url.Values, target interface{}, opts ...Option) error
  • FormTo(values url.Values, target interface{}, opts ...Option) error
  • MultipartTo(form *multipart.Form, target interface{}, opts ...Option) error
  • HeaderTo(headers http.Header, target interface{}, opts ...Option) error
  • CookieTo(cookies []*http.Cookie, target interface{}, opts ...Option) error
  • PathTo(params map[string]string, target interface{}, opts ...Option) error
  • XMLTo(data []byte, target interface{}, opts ...Option) error

Source Constructors

For multi-source binding:

func FromJSON(r io.Reader) Source
func FromQuery(values url.Values) Source
func FromForm(values url.Values) Source
func FromMultipart(form *multipart.Form) Source
func FromHeader(headers http.Header) Source
func FromCookie(cookies []*http.Cookie) Source
func FromPath(params map[string]string) Source
func FromXML(r io.Reader) Source

Example with multipart:

type Request struct {
    UserID int           `path:"user_id"`
    File   *binding.File `form:"file"`
    Token  string        `header:"X-Token"`
}

req, err := binding.Bind[Request](
    binding.FromPath(pathParams),
    binding.FromMultipart(r.MultipartForm),
    binding.FromHeader(r.Header),
)

Binder Type

Constructor

func New(opts ...Option) (*Binder, error)
func MustNew(opts ...Option) *Binder

Creates a reusable binder with configuration.

Example:

binder := binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithMaxDepth(16),
)

user, err := binder.JSON[User](data)

Binder Methods

A Binder has the same methods as the package-level functions:

func (b *Binder) JSON[T any](data []byte, opts ...Option) (T, error)
func (b *Binder) Query[T any](values url.Values, opts ...Option) (T, error)
// ... etc for all binding functions

Error Types

BindError

Field-specific binding error with detailed context:

type BindError struct {
    Field  string // Field name that failed to bind
    Source string // Source ("query", "json", "header", etc.)
    Value  string // Raw value that failed to bind
    Type   string // Expected Go type
    Reason string // Human-readable reason
    Err    error  // Underlying error
}

func (e *BindError) Error() string
func (e *BindError) Unwrap() error
func (e *BindError) IsType() bool    // True if type conversion failed
func (e *BindError) IsMissing() bool // True if required field missing

Example:

user, err := binding.JSON[User](data)
if err != nil {
    var bindErr *binding.BindError
    if errors.As(err, &bindErr) {
        log.Printf("Field %s from %s failed: %v",
            bindErr.Field, bindErr.Source, bindErr.Err)
    }
}

UnknownFieldError

Returned in strict mode when unknown fields are encountered:

type UnknownFieldError struct {
    Fields []string // List of unknown field names
}

func (e *UnknownFieldError) Error() string

Example:

user, err := binding.JSON[User](data, binding.WithStrictJSON())
if err != nil {
    var unknownErr *binding.UnknownFieldError
    if errors.As(err, &unknownErr) {
        log.Printf("Unknown fields: %v", unknownErr.Fields)
    }
}

MultiError

Multiple errors collected with WithAllErrors():

type MultiError struct {
    Errors []*BindError
}

func (e *MultiError) Error() string
func (e *MultiError) Unwrap() []error

Example:

user, err := binding.JSON[User](data, binding.WithAllErrors())
if err != nil {
    var multi *binding.MultiError
    if errors.As(err, &multi) {
        for _, e := range multi.Errors {
            log.Printf("Field %s: %v", e.Field, e.Err)
        }
    }
}

Interfaces

ValueGetter

Interface for custom data sources:

type ValueGetter interface {
    Get(key string) string          // Get first value for key
    GetAll(key string) []string     // Get all values for key
    Has(key string) bool            // Check if key exists
}

Example Implementation:

type EnvGetter struct{}

func (g *EnvGetter) Get(key string) string {
    return os.Getenv(key)
}

func (g *EnvGetter) GetAll(key string) []string {
    if val := os.Getenv(key); val != "" {
        return []string{val}
    }
    return nil
}

func (g *EnvGetter) Has(key string) bool {
    _, exists := os.LookupEnv(key)
    return exists
}

ConverterFunc

Function type for custom type converters:

type ConverterFunc[T any] func(string) (T, error)

Example:

func ParseEmail(s string) (Email, error) {
    if !strings.Contains(s, "@") {
        return "", errors.New("invalid email")
    }
    return Email(s), nil
}

binder := binding.MustNew(
    binding.WithConverter[Email](ParseEmail),
)

Converter Factory Functions

Ready-to-use converter factories for common type patterns.

TimeConverter

func TimeConverter(layouts ...string) func(string) (time.Time, error)

Creates a converter that tries parsing time strings using the provided formats in order.

Example:

binder := binding.MustNew(
    binding.WithConverter(binding.TimeConverter(
        "2006-01-02",      // ISO format
        "01/02/2006",      // US format
        "02-Jan-2006",     // Short month
    )),
)

type Event struct {
    Date time.Time `query:"date"`
}

// Works with: ?date=2026-01-28 or ?date=01/28/2026 or ?date=28-Jan-2026
event, err := binder.Query[Event](values)

DurationConverter

func DurationConverter(aliases map[string]time.Duration) func(string) (time.Duration, error)

Creates a converter that parses duration strings. It supports both standard Go duration format (like "30m", "2h30m") and custom aliases you define.

Example:

binder := binding.MustNew(
    binding.WithConverter(binding.DurationConverter(map[string]time.Duration{
        "quick":  5 * time.Minute,
        "normal": 30 * time.Minute,
        "long":   2 * time.Hour,
    })),
)

type Config struct {
    Timeout time.Duration `query:"timeout"`
}

// Works with: ?timeout=quick or ?timeout=30m or ?timeout=2h30m
config, err := binder.Query[Config](values)

EnumConverter

func EnumConverter[T ~string](allowed ...T) func(string) (T, error)

Creates a converter that checks if a string value is one of the allowed options. Matching is case-insensitive.

Example:

type Status string

const (
    StatusActive   Status = "active"
    StatusPending  Status = "pending"
    StatusDisabled Status = "disabled"
)

binder := binding.MustNew(
    binding.WithConverter(binding.EnumConverter(
        StatusActive,
        StatusPending,
        StatusDisabled,
    )),
)

type User struct {
    Status Status `query:"status"`
}

// Works with: ?status=active or ?status=ACTIVE (case-insensitive)
// Returns error for: ?status=invalid
user, err := binder.Query[User](values)

BoolConverter

func BoolConverter(truthy, falsy []string) func(string) (bool, error)

Creates a converter that parses boolean values using your custom truthy and falsy strings. Matching is case-insensitive.

Example:

binder := binding.MustNew(
    binding.WithConverter(binding.BoolConverter(
        []string{"yes", "on", "enabled", "1"},   // truthy
        []string{"no", "off", "disabled", "0"},  // falsy
    )),
)

type Settings struct {
    Notifications bool `query:"notifications"`
}

// Works with: ?notifications=yes, ?notifications=ON, ?notifications=off
settings, err := binder.Query[Settings](values)

Helper Functions

MapGetter

Converts a map[string]string to a ValueGetter:

func MapGetter(m map[string]string) ValueGetter

Example:

data := map[string]string{"name": "Alice", "age": "30"}
getter := binding.MapGetter(data)
result, err := binding.RawInto[User](getter, "custom")

MultiMapGetter

Converts a map[string][]string to a ValueGetter:

func MultiMapGetter(m map[string][]string) ValueGetter

Example:

data := map[string][]string{
    "tags": {"go", "rust"},
    "name": {"Alice"},
}
getter := binding.MultiMapGetter(data)
result, err := binding.RawInto[User](getter, "custom")

GetterFunc

Adapts a function to the ValueGetter interface:

type GetterFunc func(key string) ([]string, bool)

func (f GetterFunc) Get(key string) string
func (f GetterFunc) GetAll(key string) []string
func (f GetterFunc) Has(key string) bool

Example:

getter := binding.GetterFunc(func(key string) ([]string, bool) {
    if val, ok := myMap[key]; ok {
        return []string{val}, true
    }
    return nil, false
})

Raw/RawInto

Low-level binding from custom ValueGetter:

func Raw[T any](getter ValueGetter, source string, opts ...Option) (T, error)
func RawInto(getter ValueGetter, source string, target interface{}, opts ...Option) error

Events and Observability

Events Type

Hooks for observing binding operations:

type Events struct {
    FieldBound   func(name, tag string)
    UnknownField func(name string)
    Done         func(stats Stats)
}

Example:

binder := binding.MustNew(
    binding.WithEvents(binding.Events{
        FieldBound: func(name, tag string) {
            log.Printf("Bound field %s from %s", name, tag)
        },
        UnknownField: func(name string) {
            log.Printf("Unknown field: %s", name)
        },
        Done: func(stats binding.Stats) {
            log.Printf("Binding completed: %d fields, %d errors",
                stats.FieldsBound, stats.ErrorCount)
        },
    }),
)

Stats Type

Statistics from binding operation:

type Stats struct {
    FieldsBound int           // Number of fields successfully bound
    ErrorCount  int           // Number of errors encountered
    Duration    time.Duration // Time taken for binding
}

Constants

Slice Modes

const (
    SliceRepeat SliceMode = iota // Repeated params: ?tags=a&tags=b (default)
    SliceCSV                     // CSV params: ?tags=a,b,c
)

Unknown Field Handling

const (
    UnknownIgnore UnknownMode = iota // Ignore unknown fields (default)
    UnknownWarn                       // Log warning for unknown fields
    UnknownError                      // Error on unknown fields
)

Merge Strategies

const (
    MergeLastWins  MergeStrategy = iota // Last source wins (default)
    MergeFirstWins                       // First source wins
)

Default Values

Time Layouts

var DefaultTimeLayouts = []string{
    time.RFC3339,
    time.RFC3339Nano,
    time.RFC1123,
    time.RFC1123Z,
    time.RFC822,
    time.RFC822Z,
    time.RFC850,
    time.ANSIC,
    time.UnixDate,
    time.RubyDate,
    time.Kitchen,
    time.Stamp,
    time.StampMilli,
    time.StampMicro,
    time.StampNano,
    time.DateTime,
    time.DateOnly,
    time.TimeOnly,
    "2006-01-02",
    "01/02/2006",
    "2006/01/02",
}

Can be extended with WithTimeLayouts().

Type Constraints

Supported Interface Types

Types implementing these interfaces are automatically supported:

  • encoding.TextUnmarshaler: For custom text unmarshaling
  • json.Unmarshaler: For custom JSON unmarshaling
  • xml.Unmarshaler: For custom XML unmarshaling

Example:

type Status string

func (s *Status) UnmarshalText(text []byte) error {
    // Custom parsing logic
    *s = Status(string(text))
    return nil
}

type Request struct {
    Status Status `query:"status"` // Automatically uses UnmarshalText
}

Thread Safety

All package-level functions and Binder methods are safe for concurrent use. The struct reflection cache is thread-safe and has no size limit.

See Also

For usage examples, see the Binding Guide.

2.2 - Options

Complete reference for all configuration options

Comprehensive reference for all configuration options available in the binding package.

Option Type

type Option func(*Config)

Options configure binding behavior. They can be passed to:

  • Package-level functions like binding.JSON[T](data, opts...).
  • Binder constructor like binding.MustNew(opts...).
  • Binder methods like binder.JSON[T](data, opts...).

Security Limits

WithMaxDepth

func WithMaxDepth(depth int) Option

Sets maximum struct nesting depth to prevent stack overflow from deeply nested structures.

Default: 32

Example:

user, err := binding.JSON[User](data, binding.WithMaxDepth(16))

Use Cases:

  • Protect against malicious deeply nested JSON.
  • Limit resource usage.
  • Prevent stack overflow.

WithMaxSliceLen

func WithMaxSliceLen(length int) Option

Sets maximum slice length to prevent memory exhaustion from large arrays.

Default: 10,000

Example:

params, err := binding.Query[Params](values, binding.WithMaxSliceLen(1000))

Use Cases:

  • Protect against memory attacks
  • Limit array sizes
  • Control memory allocation

WithMaxMapSize

func WithMaxMapSize(size int) Option

Sets maximum map size to prevent memory exhaustion from large objects.

Default: 1,000

Example:

config, err := binding.JSON[Config](data, binding.WithMaxMapSize(500))

Use Cases:

  • Protect against memory attacks.
  • Limit object sizes.
  • Control memory allocation.

Unknown Field Handling

WithStrictJSON

func WithStrictJSON() Option

Convenience function that sets WithUnknownFields(UnknownError). Fails binding if JSON contains fields not in the struct.

Example:

user, err := binding.JSON[User](data, binding.WithStrictJSON())
if err != nil {
    var unknownErr *binding.UnknownFieldError
    if errors.As(err, &unknownErr) {
        log.Printf("Unknown fields: %v", unknownErr.Fields)
    }
}

Use Cases:

  • API versioning
  • Catch typos in field names
  • Enforce strict contracts

WithUnknownFields

func WithUnknownFields(mode UnknownMode) Option

// Modes
const (
    UnknownIgnore UnknownMode = iota // Ignore unknown fields (default)
    UnknownWarn                       // Log warnings
    UnknownError                      // Return error
)

Controls how unknown fields are handled.

Example:

user, err := binding.JSON[User](data,
    binding.WithUnknownFields(binding.UnknownWarn))

Modes:

  • UnknownIgnore: Silently ignore (default, most flexible)
  • UnknownWarn: Log warnings (for debugging)
  • UnknownError: Fail binding (strict contracts)

Slice Parsing

WithSliceMode

func WithSliceMode(mode SliceMode) Option

// Modes
const (
    SliceRepeat SliceMode = iota // ?tags=a&tags=b (default)
    SliceCSV                     // ?tags=a,b,c
)

Controls how slices are parsed from query/form values.

Example:

// URL: ?tags=go,rust,python
params, err := binding.Query[Params](values,
    binding.WithSliceMode(binding.SliceCSV))

Modes:

  • SliceRepeat: Repeated parameters (default, standard HTTP)
  • SliceCSV: Comma-separated values (more compact)

Error Handling

WithAllErrors

func WithAllErrors() Option

Collects all binding errors instead of failing on the first error.

Example:

user, err := binding.JSON[User](data, binding.WithAllErrors())
if err != nil {
    var multi *binding.MultiError
    if errors.As(err, &multi) {
        for _, e := range multi.Errors {
            log.Printf("Field %s: %v", e.Field, e.Err)
        }
    }
}

Use Cases:

  • Show all validation errors to user
  • Debugging
  • Comprehensive error reporting

Type Conversion

WithConverter

func WithConverter[T any](fn func(string) (T, error)) Option

Registers a custom type converter for type T.

Example:

import "github.com/google/uuid"

binder := binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
)

type User struct {
    ID uuid.UUID `query:"id"`
}

user, err := binder.Query[User](values)

Use Cases:

  • Custom types (UUID, decimal, etc.)
  • Domain-specific types
  • Third-party types

Converter Factories

The binding package provides ready-to-use converter factories for common patterns. These are functions that return converter functions you can use with WithConverter.

TimeConverter

func TimeConverter(layouts ...string) func(string) (time.Time, error)

Creates a converter that parses time strings using the provided date formats. Tries each format in order until one succeeds.

Example:

binder := binding.MustNew(
    // Try ISO format first, then US format
    binding.WithConverter(binding.TimeConverter(
        "2006-01-02",
        "01/02/2006",
    )),
)

type Event struct {
    Date time.Time `query:"date"`
}

// Works with: ?date=2026-01-28 or ?date=01/28/2026
event, err := binder.Query[Event](values)

Common layouts:

  • "2006-01-02" - ISO date (YYYY-MM-DD)
  • "01/02/2006" - US format (MM/DD/YYYY)
  • "02-Jan-2006" - Short month name
  • "2006-01-02 15:04:05" - DateTime with seconds

DurationConverter

func DurationConverter(aliases map[string]time.Duration) func(string) (time.Duration, error)

Creates a converter that parses duration strings. Supports both standard Go duration format (like "30m", "2h30m") and custom aliases you define.

Example:

binder := binding.MustNew(
    binding.WithConverter(binding.DurationConverter(map[string]time.Duration{
        "quick":   5 * time.Minute,
        "normal":  30 * time.Minute,
        "long":    2 * time.Hour,
    })),
)

type Config struct {
    Timeout time.Duration `query:"timeout"`
}

// All of these work:
// ?timeout=quick      → 5 minutes
// ?timeout=30m        → 30 minutes
// ?timeout=2h30m      → 2 hours 30 minutes
config, err := binder.Query[Config](values)

Use Cases:

  • User-friendly duration aliases
  • Cache TTL presets
  • Timeout configurations
  • Fallback to standard durations

EnumConverter

func EnumConverter[T ~string](allowed ...T) func(string) (T, error)

Creates a converter that validates string values against a set of allowed options. Matching is case-insensitive.

Example:

type Status string

const (
    StatusActive   Status = "active"
    StatusPending  Status = "pending"
    StatusDisabled Status = "disabled"
)

binder := binding.MustNew(
    binding.WithConverter(binding.EnumConverter(
        StatusActive,
        StatusPending,
        StatusDisabled,
    )),
)

type User struct {
    Status Status `query:"status"`
}

// ?status=active   ✓ OK
// ?status=ACTIVE   ✓ OK (case-insensitive)
// ?status=invalid  ✗ Error: must be one of: active, pending, disabled
user, err := binder.Query[User](values)

Use Cases:

  • String enums with validation
  • Status fields
  • Category/type fields
  • Prevent invalid values

BoolConverter

func BoolConverter(truthy, falsy []string) func(string) (bool, error)

Creates a converter that parses boolean values using custom truthy and falsy strings. Matching is case-insensitive.

Example:

binder := binding.MustNew(
    binding.WithConverter(binding.BoolConverter(
        []string{"yes", "on", "enabled", "1"},   // truthy
        []string{"no", "off", "disabled", "0"},  // falsy
    )),
)

type Settings struct {
    Notifications bool `query:"notifications"`
}

// ?notifications=yes       → true
// ?notifications=enabled   → true
// ?notifications=OFF       → false (case-insensitive)
// ?notifications=0         → false
settings, err := binder.Query[Settings](values)

Use Cases:

  • User-friendly boolean inputs
  • Feature flags
  • Toggle settings
  • Forms with yes/no options

Combining Converters

You can use multiple converters together, including both factories and custom converters:

binder := binding.MustNew(
    // Time with custom formats
    binding.WithConverter(binding.TimeConverter("01/02/2006", "2006-01-02")),
    
    // Duration with friendly names
    binding.WithConverter(binding.DurationConverter(map[string]time.Duration{
        "short": 5 * time.Minute,
        "long":  1 * time.Hour,
    })),
    
    // Status enum
    binding.WithConverter(binding.EnumConverter("active", "pending", "disabled")),
    
    // Boolean with custom values
    binding.WithConverter(binding.BoolConverter(
        []string{"yes", "on"},
        []string{"no", "off"},
    )),
    
    // Third-party types
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithConverter[decimal.Decimal](decimal.NewFromString),
)

WithTimeLayouts

func WithTimeLayouts(layouts ...string) Option

Sets custom time parsing layouts. Replaces default layouts.

Default Layouts: See binding.DefaultTimeLayouts

Example:

binder := binding.MustNew(
    binding.WithTimeLayouts(
        "2006-01-02",           // Date only
        "01/02/2006",           // US format
        "2006-01-02 15:04:05",  // DateTime
    ),
)

Tip: Extend defaults instead of replacing:

binder := binding.MustNew(
    binding.WithTimeLayouts(
        append(binding.DefaultTimeLayouts, "01/02/2006", "02-Jan-2006")...,
    ),
)

Observability

WithEvents

func WithEvents(events Events) Option

type Events struct {
    FieldBound   func(name, tag string)
    UnknownField func(name string)
    Done         func(stats Stats)
}

type Stats struct {
    FieldsBound int
    ErrorCount  int
    Duration    time.Duration
}

Registers event handlers for observing binding operations.

Example:

binder := binding.MustNew(
    binding.WithEvents(binding.Events{
        FieldBound: func(name, tag string) {
            metrics.Increment("binding.field.bound",
                "field:"+name, "source:"+tag)
        },
        UnknownField: func(name string) {
            log.Warn("Unknown field", "name", name)
        },
        Done: func(stats binding.Stats) {
            metrics.Histogram("binding.duration",
                stats.Duration.Milliseconds())
            metrics.Gauge("binding.fields", stats.FieldsBound)
        },
    }),
)

Use Cases:

  • Metrics collection
  • Debugging
  • Performance monitoring
  • Audit logging

Multi-Source Options

WithMergeStrategy

func WithMergeStrategy(strategy MergeStrategy) Option

// Strategies
const (
    MergeLastWins  MergeStrategy = iota // Last source wins (default)
    MergeFirstWins                       // First source wins
)

Controls precedence when binding from multiple sources.

Example:

// First source wins
req, err := binding.Bind[Request](
    binding.WithMergeStrategy(binding.MergeFirstWins),
    binding.FromHeader(r.Header),      // Highest priority
    binding.FromQuery(r.URL.Query()),  // Lower priority
)

Strategies:

  • MergeLastWins: Last source overwrites (default)
  • MergeFirstWins: First non-empty value wins

JSON-Specific Options

WithDisallowUnknownFields

func WithDisallowUnknownFields() Option

Equivalent to WithStrictJSON(). Provided for clarity when explicitly disallowing unknown fields.

Example:

user, err := binding.JSON[User](data,
    binding.WithDisallowUnknownFields())

WithMaxBytes

func WithMaxBytes(bytes int64) Option

Limits the size of JSON/XML data to prevent memory exhaustion.

Example:

user, err := binding.JSON[User](data,
    binding.WithMaxBytes(1024 * 1024)) // 1MB limit

Use Cases:

  • Protect against large payloads
  • API rate limiting
  • Resource management

Custom Options

WithTagHandler

func WithTagHandler(tagName string, handler TagHandler) Option

type TagHandler interface {
    Get(fieldName, tagValue string) (string, bool)
}

Registers a custom struct tag handler.

Example:

type EnvTagHandler struct {
    prefix string
}

func (h *EnvTagHandler) Get(fieldName, tagValue string) (string, bool) {
    envKey := h.prefix + tagValue
    val, exists := os.LookupEnv(envKey)
    return val, exists
}

binder := binding.MustNew(
    binding.WithTagHandler("env", &EnvTagHandler{prefix: "APP_"}),
)

type Config struct {
    APIKey string `env:"API_KEY"`  // Looks up APP_API_KEY
}

Option Combinations

Production Configuration

var ProductionBinder = binding.MustNew(
    // Security
    binding.WithMaxDepth(16),
    binding.WithMaxSliceLen(1000),
    binding.WithMaxMapSize(500),
    binding.WithMaxBytes(10 * 1024 * 1024), // 10MB
    
    // Strict validation
    binding.WithStrictJSON(),
    
    // Custom types
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithConverter[decimal.Decimal](decimal.NewFromString),
    
    // Time formats
    binding.WithTimeLayouts(append(
        binding.DefaultTimeLayouts,
        "2006-01-02",
        "01/02/2006",
    )...),
    
    // Observability
    binding.WithEvents(binding.Events{
        FieldBound:   logFieldBound,
        UnknownField: logUnknownField,
        Done:         recordMetrics,
    }),
)

Development Configuration

var DevBinder = binding.MustNew(
    // Lenient limits
    binding.WithMaxDepth(32),
    binding.WithMaxSliceLen(10000),
    
    // Warnings instead of errors
    binding.WithUnknownFields(binding.UnknownWarn),
    
    // Collect all errors for debugging
    binding.WithAllErrors(),
    
    // Verbose logging
    binding.WithEvents(binding.Events{
        FieldBound: func(name, tag string) {
            log.Printf("[DEBUG] Bound %s from %s", name, tag)
        },
        UnknownField: func(name string) {
            log.Printf("[WARN] Unknown field: %s", name)
        },
        Done: func(stats binding.Stats) {
            log.Printf("[DEBUG] Binding: %d fields, %d errors, %v",
                stats.FieldsBound, stats.ErrorCount, stats.Duration)
        },
    }),
)

Testing Configuration

var TestBinder = binding.MustNew(
    // Strict validation
    binding.WithStrictJSON(),
    
    // Fail fast
    // (don't use WithAllErrors in tests)
    
    // Smaller limits for test data
    binding.WithMaxDepth(8),
    binding.WithMaxSliceLen(100),
)

Option Precedence

When options are provided to both MustNew() and individual functions:

  1. Function-level options override binder-level options
  2. Options are applied in order (last wins for same option)

Example:

binder := binding.MustNew(
    binding.WithMaxDepth(32),  // Binder default
)

// This call uses maxDepth=16 (overrides binder default)
user, err := binder.JSON[User](data,
    binding.WithMaxDepth(16))

Best Practices

1. Use Binders for Shared Configuration

// Good - shared configuration
var AppBinder = binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
    binding.WithMaxDepth(16),
)

func Handler1(r *http.Request) {
    user, err := AppBinder.JSON[User](r.Body)
}

func Handler2(r *http.Request) {
    params, err := AppBinder.Query[Params](r.URL.Query())
}

2. Set Security Limits

// Good - protect against attacks
user, err := binding.JSON[User](data,
    binding.WithMaxDepth(16),
    binding.WithMaxSliceLen(1000),
    binding.WithMaxBytes(1024*1024),
)

3. Use Strict Mode for APIs

// Good - catch client errors early
user, err := binding.JSON[User](data, binding.WithStrictJSON())

4. Collect All Errors for Forms

// Good - show all validation errors to user
form, err := binding.Form[Form](r.PostForm, binding.WithAllErrors())
if err != nil {
    var multi *binding.MultiError
    if errors.As(err, &multi) {
        // Show all errors to user
        for _, e := range multi.Errors {
            addError(e.Field, e.Err.Error())
        }
    }
}

See Also

For usage examples, see the Binding Guide.

2.3 - Sub-Packages

YAML, TOML, MessagePack, and Protocol Buffers support

Reference for sub-packages that add support for additional data formats beyond the core package.

Package Overview

Sub-PackageFormatImport Path
yamlYAMLrivaas.dev/binding/yaml
tomlTOMLrivaas.dev/binding/toml
msgpackMessagePackrivaas.dev/binding/msgpack
protoProtocol Buffersrivaas.dev/binding/proto

YAML Package

Import

import "rivaas.dev/binding/yaml"

Functions

YAML

func YAML[T any](data []byte, opts ...Option) (T, error)

Binds YAML data to a struct.

Example:

type Config struct {
    Name  string `yaml:"name"`
    Port  int    `yaml:"port"`
    Debug bool   `yaml:"debug"`
}

config, err := yaml.YAML[Config](yamlData)

YAMLReader

func YAMLReader[T any](r io.Reader, opts ...Option) (T, error)

Binds YAML from an io.Reader.

Example:

config, err := yaml.YAMLReader[Config](r.Body)

YAMLTo

func YAMLTo(data []byte, target interface{}, opts ...Option) error

Non-generic variant.

Options

WithStrict

func WithStrict() Option

Enables strict YAML parsing. Fails on unknown fields or duplicate keys.

Example:

config, err := yaml.YAML[Config](data, yaml.WithStrict())

Struct Tags

Use yaml struct tags:

type Config struct {
    Name  string `yaml:"name"`
    Port  int    `yaml:"port"`
    Debug bool   `yaml:"debug,omitempty"`
    
    // Inline nested struct
    Database struct {
        Host string `yaml:"host"`
        Port int    `yaml:"port"`
    } `yaml:"database"`
    
    // Ignore field
    Internal string `yaml:"-"`
}

Example

# config.yaml
name: my-app
port: 8080
debug: true
database:
  host: localhost
  port: 5432
data, _ := os.ReadFile("config.yaml")
config, err := yaml.YAML[Config](data)

TOML Package

Import

import "rivaas.dev/binding/toml"

Functions

TOML

func TOML[T any](data []byte, opts ...Option) (T, error)

Binds TOML data to a struct.

Example:

type Config struct {
    Name  string `toml:"name"`
    Port  int    `toml:"port"`
    Debug bool   `toml:"debug"`
}

config, err := toml.TOML[Config](tomlData)

TOMLReader

func TOMLReader[T any](r io.Reader, opts ...Option) (T, error)

Binds TOML from an io.Reader.

TOMLTo

func TOMLTo(data []byte, target interface{}, opts ...Option) error

Non-generic variant.

Struct Tags

Use toml struct tags:

type Config struct {
    Title string `toml:"title"`
    
    Owner struct {
        Name string `toml:"name"`
        DOB  time.Time `toml:"dob"`
    } `toml:"owner"`
    
    Database struct {
        Server  string `toml:"server"`
        Ports   []int  `toml:"ports"`
        Enabled bool   `toml:"enabled"`
    } `toml:"database"`
}

Example

# config.toml
title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00

[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
enabled = true
data, _ := os.ReadFile("config.toml")
config, err := toml.TOML[Config](data)

MessagePack Package

Import

import "rivaas.dev/binding/msgpack"

Functions

MsgPack

func MsgPack[T any](data []byte, opts ...Option) (T, error)

Binds MessagePack data to a struct.

Example:

type Message struct {
    ID   int    `msgpack:"id"`
    Data []byte `msgpack:"data"`
    Time time.Time `msgpack:"time"`
}

msg, err := msgpack.MsgPack[Message](msgpackData)

MsgPackReader

func MsgPackReader[T any](r io.Reader, opts ...Option) (T, error)

Binds MessagePack from an io.Reader.

Example:

msg, err := msgpack.MsgPackReader[Message](r.Body)

MsgPackTo

func MsgPackTo(data []byte, target interface{}, opts ...Option) error

Non-generic variant.

Struct Tags

Use msgpack struct tags:

type Message struct {
    ID      int       `msgpack:"id"`
    Type    string    `msgpack:"type"`
    Payload []byte    `msgpack:"payload"`
    Created time.Time `msgpack:"created"`
    
    // Omit if zero
    Metadata map[string]string `msgpack:"metadata,omitempty"`
    
    // Use as array (more compact)
    Points []int `msgpack:"points,as_array"`
}

Use Cases

  • High-performance binary serialization
  • Microservice communication
  • Event streaming
  • Cache serialization

Protocol Buffers Package

Import

import "rivaas.dev/binding/proto"
import pb "myapp/proto"  // Your generated proto files

Functions

Proto

func Proto[T proto.Message](data []byte, opts ...Option) (T, error)

Binds Protocol Buffer data to a proto message.

Example:

import pb "myapp/proto"

user, err := proto.Proto[*pb.User](protoData)

ProtoReader

func ProtoReader[T proto.Message](r io.Reader, opts ...Option) (T, error)

Binds Protocol Buffers from an io.Reader.

Example:

user, err := proto.ProtoReader[*pb.User](r.Body)

ProtoTo

func ProtoTo(data []byte, target proto.Message, opts ...Option) error

Non-generic variant.

Proto Definition

// user.proto
syntax = "proto3";

package example;
option go_package = "myapp/proto";

message User {
  int64 id = 1;
  string username = 2;
  string email = 3;
  int32 age = 4;
  repeated string tags = 5;
}

Example

import (
    "rivaas.dev/binding/proto"
    pb "myapp/proto"
)

func HandleProtoRequest(w http.ResponseWriter, r *http.Request) {
    user, err := proto.ProtoReader[*pb.User](r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Use user
    log.Printf("Received user: %s", user.Username)
}

Use Cases

  • gRPC services
  • High-performance APIs
  • Cross-language communication
  • Schema evolution

Common Patterns

Configuration Files

import (
    "rivaas.dev/binding/yaml"
    "rivaas.dev/binding/toml"
)

type Config struct {
    Name     string `yaml:"name" toml:"name"`
    Port     int    `yaml:"port" toml:"port"`
    Database struct {
        Host string `yaml:"host" toml:"host"`
        Port int    `yaml:"port" toml:"port"`
    } `yaml:"database" toml:"database"`
}

func LoadConfig(format, path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    
    switch format {
    case "yaml", "yml":
        return yaml.YAML[Config](data)
    case "toml":
        return toml.TOML[Config](data)
    default:
        return nil, fmt.Errorf("unsupported format: %s", format)
    }
}

Content Negotiation

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    contentType := r.Header.Get("Content-Type")
    
    var req CreateUserRequest
    var err error
    
    switch {
    case strings.Contains(contentType, "application/json"):
        req, err = binding.JSON[CreateUserRequest](r.Body)
        
    case strings.Contains(contentType, "application/x-yaml"):
        req, err = yaml.YAMLReader[CreateUserRequest](r.Body)
        
    case strings.Contains(contentType, "application/toml"):
        req, err = toml.TOMLReader[CreateUserRequest](r.Body)
        
    case strings.Contains(contentType, "application/x-msgpack"):
        req, err = msgpack.MsgPackReader[CreateUserRequest](r.Body)
        
    default:
        http.Error(w, "Unsupported content type", http.StatusUnsupportedMediaType)
        return
    }
    
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Process request...
}

Multi-Format API

type API struct {
    yaml    *yaml.Binder
    toml    *toml.Binder
    msgpack *msgpack.Binder
}

func NewAPI() *API {
    return &API{
        yaml:    yaml.MustNew(yaml.WithStrict()),
        toml:    toml.MustNew(),
        msgpack: msgpack.MustNew(),
    }
}

func (a *API) Bind(r *http.Request, target interface{}) error {
    contentType := r.Header.Get("Content-Type")
    
    switch {
    case strings.Contains(contentType, "yaml"):
        return a.yaml.YAMLReaderTo(r.Body, target)
    case strings.Contains(contentType, "toml"):
        return a.toml.TOMLReaderTo(r.Body, target)
    case strings.Contains(contentType, "msgpack"):
        return a.msgpack.MsgPackReaderTo(r.Body, target)
    default:
        return binding.JSONReaderTo(r.Body, target)
    }
}

Dependencies

Sub-packages have external dependencies:

PackageDependency
yamlgopkg.in/yaml.v3
tomlgithub.com/BurntSushi/toml
msgpackgithub.com/vmihailenco/msgpack/v5
protogoogle.golang.org/protobuf

Install with:

# YAML
go get gopkg.in/yaml.v3

# TOML
go get github.com/BurntSushi/toml

# MessagePack
go get github.com/vmihailenco/msgpack/v5

# Protocol Buffers
go get google.golang.org/protobuf

Performance Comparison

Approximate performance for a typical struct (10 fields):

FormatSpeed (ns/op)AllocsUse Case
JSON8003Web APIs, human-readable
MessagePack5002High performance, binary
Protocol Buffers4002Strongly typed, cross-language
YAML1,2005Configuration files
TOML1,0004Configuration files

Best Practices

1. Use Appropriate Format

  • JSON: Web APIs, JavaScript clients
  • YAML: Configuration files, human-readable
  • TOML: Configuration files, less ambiguous than YAML
  • MessagePack: High-performance microservices
  • Protocol Buffers: gRPC, schema evolution

2. Validate Input

All sub-packages support the same options as core binding:

config, err := yaml.YAML[Config](data,
    binding.WithMaxDepth(16),
    binding.WithMaxSliceLen(1000),
)

3. Stream Large Files

Use Reader variants for large payloads:

// Good - streams from disk
file, _ := os.Open("large-config.yaml")
config, err := yaml.YAMLReader[Config](file)

// Bad - loads entire file into memory
data, _ := os.ReadFile("large-config.yaml")
config, err := yaml.YAML[Config](data)

See Also

For usage examples, see the Binding Guide.

2.4 - Troubleshooting

Common issues, solutions, and FAQs

Solutions to common issues, frequently asked questions, and debugging strategies for the binding package.

Common Issues

Field Not Binding

Problem: Field remains zero value after binding.

Possible Causes:

  1. Field is unexported

    // Wrong - unexported field
    type Request struct {
        name string `json:"name"`  // Won't bind
    }
    
    // Correct
    type Request struct {
        Name string `json:"name"`
    }
    
  2. Tag name doesn’t match source key

    // JSON: {"username": "alice"}
    type Request struct {
        Name string `json:"name"`  // Wrong tag name
    }
    
    // Correct
    type Request struct {
        Name string `json:"username"`  // Matches JSON key
    }
    
  3. Wrong tag type for source

    // Binding from query parameters
    type Request struct {
        Name string `json:"name"`  // Wrong - should be `query:"name"`
    }
    
    // Correct
    type Request struct {
        Name string `query:"name"`
    }
    
  4. Source doesn’t contain the key

    // URL: ?page=1
    type Params struct {
        Page  int    `query:"page"`
        Limit int    `query:"limit"`  // Missing in URL
    }
    
    // Solution: Use default
    type Params struct {
        Page  int `query:"page" default:"1"`
        Limit int `query:"limit" default:"20"`
    }
    

Type Conversion Errors

Problem: Error like “cannot unmarshal string into int”.

Solutions:

  1. Check source data type

    // JSON: {"age": "30"}  <- string instead of number
    type User struct {
        Age int `json:"age"`
    }
    
    // Error: cannot unmarshal string into int
    

    Fix: Ensure JSON sends number: {"age": 30}

  2. Use string type and convert manually

    type User struct {
        AgeStr string `json:"age"`
    }
    
    user, err := binding.JSON[User](data)
    age, _ := strconv.Atoi(user.AgeStr)
    
  3. Register custom converter

    binder := binding.MustNew(
        binding.WithConverter[MyType](parseMyType),
    )
    

Slice Not Parsing

Problem: Slice remains empty or has unexpected values

Cause: Wrong slice mode for input format

// URL: ?tags=go,rust,python
type Params struct {
    Tags []string `query:"tags"`
}

// With default mode (SliceRepeat)
params, _ := binding.Query[Params](values)
// Result: Tags = ["go,rust,python"]  <- Wrong!

Solution: Use CSV mode

params, err := binding.Query[Params](values,
    binding.WithSliceMode(binding.SliceCSV))
// Result: Tags = ["go", "rust", "python"]  <- Correct!

Or use repeated parameters:

// URL: ?tags=go&tags=rust&tags=python
params, _ := binding.Query[Params](values)  // Default mode works

JSON Parsing Errors

Problem: “unexpected end of JSON input” or “invalid character”

Causes:

  1. Malformed JSON

    {"name": "test"  // Missing closing brace
    

    Solution: Validate JSON syntax

  2. Empty body

    // Body is empty but expecting JSON
    user, err := binding.JSON[User](r.Body)
    // Error: unexpected end of JSON input
    

    Solution: Check if body is empty first

    body, err := io.ReadAll(r.Body)
    if len(body) == 0 {
        return errors.New("empty body")
    }
    user, err := binding.JSON[User](body)
    
  3. Body already consumed

    body, _ := io.ReadAll(r.Body)  // Consumes body
    // ... some code ...
    user, err := binding.JSON[User](r.Body)  // Error: body empty
    

    Solution: Restore body

    body, _ := io.ReadAll(r.Body)
    r.Body = io.NopCloser(bytes.NewReader(body))
    user, err := binding.JSON[User](body)
    

Unknown Field Errors

Problem: Error in strict mode for valid JSON

Cause: JSON contains fields not in struct

// JSON: {"name": "alice", "extra": "field"}
type User struct {
    Name string `json:"name"`
}

user, err := binding.JSON[User](data, binding.WithStrictJSON())
// Error: json: unknown field "extra"

Solutions:

  1. Add field to struct

    type User struct {
        Name  string `json:"name"`
        Extra string `json:"extra"`
    }
    
  2. Remove strict mode

    user, err := binding.JSON[User](data)  // Ignores extra fields
    
  3. Use interface{} for unknown fields

    type User struct {
        Name  string                 `json:"name"`
        Extra map[string]interface{} `json:"-"`
    }
    

Pointer vs Value Confusion

Problem: Can’t distinguish between “not provided” and “zero value”

Example:

type UpdateRequest struct {
    Age int `json:"age"`
}

// JSON: {"age": 0}
// Can't tell if: 1) User wants to set age to 0, or 2) Field not provided

Solution: Use pointers

type UpdateRequest struct {
    Age *int `json:"age"`
}

// JSON: {"age": 0}      -> Age = &0 (explicitly set to zero)
// JSON: {}              -> Age = nil (not provided)
// JSON: {"age": null}   -> Age = nil (explicitly null)

Default Values Not Applied

Problem: Default value doesn’t work

Cause: Defaults only apply when field is missing, not for zero values

type Params struct {
    Page int `query:"page" default:"1"`
}

// URL: ?page=0
params, _ := binding.Query[Params](values)
// Result: Page = 0 (not 1, because 0 was provided)

Solution: Use pointer to distinguish nil from zero

type Params struct {
    Page *int `query:"page" default:"1"`
}

// URL: ?page=0  -> Page = &0
// URL: (no page) -> Page = &1 (default applied)

Nested Struct Not Binding

Problem: Nested struct fields remain zero

Example:

// JSON: {"user": {"name": "alice", "age": 30}}
type Request struct {
    User struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    } `json:"user"`
}

req, err := binding.JSON[Request](data)
// Works correctly

For query parameters, use dot notation:

// URL: ?user.name=alice&user.age=30
type Request struct {
    User struct {
        Name string `query:"user.name"`
        Age  int    `query:"user.age"`
    }
}

Time Parsing Errors

Problem: “parsing time … as …: cannot parse”

Cause: Time format doesn’t match any default layouts

// JSON: {"created": "01/02/2006"}
type Request struct {
    Created time.Time `json:"created"`
}
// Error: parsing time "01/02/2006"

Solution: Add custom time layout

binder := binding.MustNew(
    binding.WithTimeLayouts(
        append(binding.DefaultTimeLayouts, "01/02/2006")...,
    ),
)

req, err := binder.JSON[Request](data)

Memory Issues

Problem: Out of memory or slow performance

Causes:

  1. Large payloads without limits

    // No limit - vulnerable to memory attack
    user, err := binding.JSON[User](r.Body)
    

    Solution: Set size limits

    user, err := binding.JSON[User](r.Body,
        binding.WithMaxBytes(1024*1024),  // 1MB limit
        binding.WithMaxSliceLen(1000),
        binding.WithMaxMapSize(500),
    )
    
  2. Not using streaming for large data

    // Bad - loads entire body into memory
    body, _ := io.ReadAll(r.Body)
    user, err := binding.JSON[User](body)
    

    Solution: Stream from reader

    user, err := binding.JSONReader[User](r.Body)
    

Header Case Sensitivity

Problem: Header not binding

Cause: HTTP headers are case-insensitive but tag must match exact case

// Header: x-api-key: secret
type Request struct {
    APIKey string `header:"X-API-Key"`  // Still works!
}

// Headers are matched case-insensitively

Note: The binding package handles case-insensitive header matching automatically.

Multi-Source Precedence Issues

Problem: Wrong source value used

Example:

// Query: ?user_id=1
// JSON: {"user_id": 2}
type Request struct {
    UserID int `query:"user_id" json:"user_id"`
}

req, err := binding.Bind[Request](
    binding.FromQuery(values),  // user_id = 1
    binding.FromJSON(body),     // user_id = 2 (overwrites!)
)
// Result: UserID = 2

Solutions:

  1. Change source order (last wins)

    req, err := binding.Bind[Request](
        binding.FromJSON(body),      // user_id = 2
        binding.FromQuery(values),   // user_id = 1 (overwrites!)
    )
    // Result: UserID = 1
    
  2. Use first-wins strategy

    req, err := binding.Bind[Request](
        binding.WithMergeStrategy(binding.MergeFirstWins),
        binding.FromQuery(values),  // user_id = 1 (wins!)
        binding.FromJSON(body),     // user_id = 2 (ignored)
    )
    // Result: UserID = 1
    

Frequently Asked Questions

Q: How do I validate required fields?

A: Use the rivaas.dev/validation package after binding:

import "rivaas.dev/validation"

type Request struct {
    Name string `json:"name" validate:"required"`
    Age  int    `json:"age" validate:"required,min=18"`
}

req, err := binding.JSON[Request](data)
if err != nil {
    return err
}

// Validate after binding
if err := validation.Validate(req); err != nil {
    return err
}

Q: Can I bind to non-struct types?

A: Yes, but only for certain types:

// Array
type Batch []CreateUserRequest
batch, err := binding.JSON[Batch](data)

// Map
type Config map[string]string
config, err := binding.JSON[Config](data)

// Primitive (less useful)
var count int
err := binding.JSONTo([]byte("42"), &count)

Q: How do I handle optional vs. required fields?

A: Combine binding with validation:

type Request struct {
    Name  string  `json:"name" validate:"required"`
    Email *string `json:"email" validate:"omitempty,email"`
}

// Name is required (validation)
// Email is optional (pointer) but if provided must be valid (validation)

Q: Can I use custom JSON field names?

A: Yes, use the json tag:

type User struct {
    ID       int    `json:"user_id"`      // Maps to "user_id" in JSON
    FullName string `json:"full_name"`    // Maps to "full_name" in JSON
}

Q: How do I bind from multiple query parameters to one field?

A: Use tag aliases:

type Request struct {
    UserID int `query:"user_id,id,uid"`  // Accepts any of these
}

// Works with: ?user_id=123, ?id=123, or ?uid=123

Q: Can I use both JSON and form binding?

A: Yes, use multi-source binding:

type Request struct {
    Name string `json:"name" form:"name"`
}

req, err := binding.Bind[Request](
    binding.FromJSON(r.Body),
    binding.FromForm(r.Form),
)

Q: How do I debug binding issues?

A: Use event hooks:

binder := binding.MustNew(
    binding.WithEvents(binding.Events{
        FieldBound: func(name, tag string) {
            log.Printf("Bound %s from %s", name, tag)
        },
        UnknownField: func(name string) {
            log.Printf("Unknown field: %s", name)
        },
        Done: func(stats binding.Stats) {
            log.Printf("%d fields, %d errors, %v",
                stats.FieldsBound, stats.ErrorCount, stats.Duration)
        },
    }),
)

Q: Is binding thread-safe?

A: Yes, all operations are thread-safe. The struct cache uses lock-free reads and synchronized writes.

Q: How do I bind custom types?

A: Register a converter:

import "github.com/google/uuid"

binder := binding.MustNew(
    binding.WithConverter[uuid.UUID](uuid.Parse),
)

Or implement encoding.TextUnmarshaler:

type MyType string

func (m *MyType) UnmarshalText(text []byte) error {
    *m = MyType(string(text))
    return nil
}

Q: Can I bind from environment variables?

A: Not directly, but you can create a custom getter:

type EnvGetter struct{}

func (g *EnvGetter) Get(key string) string {
    return os.Getenv(key)
}

func (g *EnvGetter) GetAll(key string) []string {
    if val := os.Getenv(key); val != "" {
        return []string{val}
    }
    return nil
}

func (g *EnvGetter) Has(key string) bool {
    _, exists := os.LookupEnv(key)
    return exists
}

// Use with RawInto
config, err := binding.RawInto[Config](&EnvGetter{}, "env")

Q: What’s the difference between JSON and JSONReader?

A:

  • JSON: Takes []byte, entire data in memory
  • JSONReader: Takes io.Reader, streams data

Use JSONReader for large payloads (>1MB) to reduce memory usage.

Q: How do I handle API versioning?

A: Use different struct types per version:

type CreateUserRequestV1 struct {
    Name string `json:"name"`
}

type CreateUserRequestV2 struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
}

// Route to appropriate handler based on version header

Debugging Strategies

1. Enable Debug Logging

import "log/slog"

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
})))

2. Inspect Raw Request

// Save body for debugging
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))

log.Printf("Raw body: %s", string(body))
log.Printf("Content-Type: %s", r.Header.Get("Content-Type"))

req, err := binding.JSON[Request](r.Body)

3. Use Curl to Test

# Test JSON binding
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"alice","age":30}'

# Test query parameters
curl "http://localhost:8080/users?page=2&limit=50"

# Test headers
curl -H "X-API-Key: secret" http://localhost:8080/users

4. Write Unit Tests

func TestBinding(t *testing.T) {
    payload := `{"name":"test","age":30}`
    
    user, err := binding.JSON[User]([]byte(payload))
    if err != nil {
        t.Fatalf("binding failed: %v", err)
    }
    
    if user.Name != "test" {
        t.Errorf("expected name=test, got %s", user.Name)
    }
}

Getting Help

If you’re still stuck:

  1. Check the examples: Binding Guide
  2. Review API docs: API Reference
  3. Search GitHub issues: rivaas-dev/rivaas/issues
  4. Ask for help: Open a new issue with:
    • Minimal reproducible example
    • Expected vs. actual behavior
    • Relevant logs/errors

See Also


For more examples and patterns, see the Binding Guide.

3 - Logging Package

API reference for rivaas.dev/logging - Structured logging for Go applications

This is the API reference for the rivaas.dev/logging package. For learning-focused documentation, see the Logging Guide.

Package Information

Package Overview

The logging package provides structured logging for Rivaas applications using Go’s standard log/slog package, with additional features for production environments.

Core Features

  • Multiple output formats (JSON, Text, Console)
  • Context-aware logging with OpenTelemetry trace correlation
  • Automatic sensitive data redaction
  • Log sampling for high-traffic scenarios
  • Dynamic log level changes at runtime
  • Convenience methods for common patterns
  • Comprehensive testing utilities
  • Zero external dependencies (except OpenTelemetry for tracing)

Architecture

The package is organized around key components:

Main Types

Logger - Main logging type with structured logging methods

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

ContextLogger - Context-aware logger with automatic trace correlation

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

Option - Functional option for logger configuration

type Option func(*Logger)

Quick API Index

Logger Creation

logger, err := logging.New(options...)     // With error handling
logger := logging.MustNew(options...)      // Panics on error

Logging Methods

logger.Debug(msg string, args ...any)
logger.Info(msg string, args ...any)
logger.Warn(msg string, args ...any)
logger.Error(msg string, args ...any)

Convenience Methods

logger.LogRequest(r *http.Request, extra ...any)
logger.LogError(err error, msg string, extra ...any)
logger.LogDuration(msg string, start time.Time, extra ...any)
logger.ErrorWithStack(msg string, err error, includeStack bool, extra ...any)

Context-Aware Logging

cl := logging.NewContextLogger(ctx context.Context, logger *Logger)
cl.Info(msg string, args ...any)  // Includes trace_id and span_id

Configuration Methods

logger.SetLevel(level Level) error
logger.Level() Level
logger.Shutdown(ctx context.Context) error

Reference Pages

API Reference

Logger and ContextLogger types with all methods.

View →

Options

Configuration options for handlers and output.

View →

Testing Utilities

Test helpers and mocking utilities.

View →

Troubleshooting

Common logging issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Type Reference

Logger

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

Main logging type. Thread-safe for concurrent access.

Creation:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)

Key Methods:

  • Debug, Info, Warn, Error - Logging at different levels
  • LogRequest, LogError, LogDuration - Convenience methods
  • SetLevel, Level - Dynamic level management
  • Shutdown - Graceful shutdown

ContextLogger

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

Context-aware logger with automatic trace correlation.

Creation:

cl := logging.NewContextLogger(ctx, logger)

Key Methods:

  • Debug, Info, Warn, Error - Logging with trace correlation
  • TraceID, SpanID - Access trace information
  • Logger - Get underlying slog.Logger

HandlerType

type HandlerType string

const (
    JSONHandler    HandlerType = "json"
    TextHandler    HandlerType = "text"
    ConsoleHandler HandlerType = "console"
)

Output format type.

Level

type Level = slog.Level

const (
    LevelDebug = slog.LevelDebug  // -4
    LevelInfo  = slog.LevelInfo   // 0
    LevelWarn  = slog.LevelWarn   // 4
    LevelError = slog.LevelError  // 8
)

Log level constants.

SamplingConfig

type SamplingConfig struct {
    Initial    int           // Log first N entries unconditionally
    Thereafter int           // After Initial, log 1 of every M entries
    Tick       time.Duration // Reset sampling counter every interval
}

Configuration for log sampling.

Error Types

The package defines sentinel errors for better error handling:

var (
    ErrNilLogger         = errors.New("custom logger is nil")
    ErrInvalidHandler    = errors.New("invalid handler type")
    ErrLoggerShutdown    = errors.New("logger is shut down")
    ErrInvalidLevel      = errors.New("invalid log level")
    ErrCannotChangeLevel = errors.New("cannot change level on custom logger")
)

Usage:

if err := logger.SetLevel(level); err != nil {
    if errors.Is(err, logging.ErrCannotChangeLevel) {
        // Handle immutable logger case
    }
}

Common Patterns

Basic Usage

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)
defer logger.Shutdown(context.Background())

logger.Info("operation completed", "items", 100)

With Service Metadata

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("payment-api"),
    logging.WithServiceVersion("v2.1.0"),
    logging.WithEnvironment("production"),
)

With Context and Tracing

cl := logging.NewContextLogger(ctx, logger)
cl.Info("processing request", "user_id", userID)
// Automatically includes trace_id and span_id

With Sampling

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,
        Thereafter: 100,
        Tick:       time.Minute,
    }),
)

Thread Safety

The Logger type is thread-safe for:

  • Concurrent logging operations
  • Concurrent SetLevel calls (serialized internally)
  • Mixed logging and configuration operations

Not thread-safe for:

  • Concurrent modification during initialization (use synchronization)

Performance Notes

  • Logging overhead: ~500ns per log entry
  • Level checks: ~5ns per check
  • Sampling overhead: ~20ns per log entry
  • Zero allocations: Standard log calls with inline fields
  • Stack traces: ~150µs capture cost (only when requested)

Version Compatibility

The logging package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

Next Steps

For learning-focused guides, see the Logging Guide.

3.1 - API Reference

Complete API reference for all types and methods in the logging package

Complete API reference for all public types and methods in the logging package.

Core Functions

New

func New(opts ...Option) (*Logger, error)

Creates a new Logger with the given options. Returns an error if configuration is invalid.

Parameters:

  • opts - Variadic list of configuration options.

Returns:

  • *Logger - Configured logger instance.
  • error - Configuration error, if any.

Example:

logger, err := logging.New(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)
if err != nil {
    log.Fatalf("failed to create logger: %v", err)
}

MustNew

func MustNew(opts ...Option) *Logger

Creates a new Logger or panics on error. Use for initialization where errors are fatal.

Parameters:

  • opts - Variadic list of configuration options

Returns:

  • *Logger - Configured logger instance

Panics:

  • If configuration is invalid

Example:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)

Automatic Trace Correlation

When you create a logger with this package (and optionally set it as the global logger with WithGlobalLogger()), trace correlation is automatic. You do not need a special logger type.

Any call to the standard library’s context-aware methods — slog.InfoContext(ctx, ...), slog.ErrorContext(ctx, ...), and so on — will automatically get trace_id and span_id added to the log record if the context contains an active OpenTelemetry span. The logging package wraps the handler with a context-aware layer that reads the span from the context and injects these fields.

Example (in an HTTP handler):

// Pass the request context when you log
slog.InfoContext(c.RequestContext(), "processing request", "order_id", orderID)
// Output includes trace_id and span_id when tracing is enabled

Use the same pattern with slog.DebugContext, slog.WarnContext, and slog.ErrorContext. No wrapper type or extra API is required.

Logger Type

Logging Methods

Debug

func (l *Logger) Debug(msg string, args ...any)

Logs a debug message with structured attributes.

Parameters:

  • msg - Log message
  • args - Key-value pairs (must be even number of arguments)

Example:

logger.Debug("cache lookup", "key", cacheKey, "hit", true)

Info

func (l *Logger) Info(msg string, args ...any)

Logs an informational message with structured attributes.

Parameters:

  • msg - Log message
  • args - Key-value pairs

Example:

logger.Info("server started", "port", 8080, "version", "v1.0.0")

Warn

func (l *Logger) Warn(msg string, args ...any)

Logs a warning message with structured attributes.

Parameters:

  • msg - Log message
  • args - Key-value pairs

Example:

logger.Warn("high memory usage", "used_mb", 8192, "threshold_mb", 10240)

Error

func (l *Logger) Error(msg string, args ...any)

Logs an error message with structured attributes. Errors bypass log sampling.

Parameters:

  • msg - Log message
  • args - Key-value pairs

Example:

logger.Error("database connection failed", "error", err, "retry_count", 3)

Convenience Methods

LogRequest

func (l *Logger) LogRequest(r *http.Request, extra ...any)

Logs an HTTP request with standard fields (method, path, remote, user_agent, query).

Parameters:

  • r - HTTP request
  • extra - Additional key-value pairs

Example:

logger.LogRequest(r, "status", 200, "duration_ms", 45)

LogError

func (l *Logger) LogError(err error, msg string, extra ...any)

Logs an error with automatic error field.

Parameters:

  • err - Error to log
  • msg - Log message
  • extra - Additional key-value pairs

Example:

logger.LogError(err, "operation failed", "operation", "INSERT", "table", "users")

LogDuration

func (l *Logger) LogDuration(msg string, start time.Time, extra ...any)

Logs operation duration with automatic duration_ms and duration fields.

Parameters:

  • msg - Log message
  • start - Operation start time
  • extra - Additional key-value pairs

Example:

start := time.Now()
// ... operation ...
logger.LogDuration("processing completed", start, "items", 100)

ErrorWithStack

func (l *Logger) ErrorWithStack(msg string, err error, includeStack bool, extra ...any)

Logs an error with optional stack trace.

Parameters:

  • msg - Log message
  • err - Error to log
  • includeStack - Whether to capture and include stack trace
  • extra - Additional key-value pairs

Example:

logger.ErrorWithStack("critical failure", err, true, "user_id", userID)

Context Methods

Logger

func (l *Logger) Logger() *slog.Logger

Returns the underlying slog.Logger for advanced usage.

Returns:

  • *slog.Logger - Underlying logger

Example:

slogger := logger.Logger()

With

func (l *Logger) With(args ...any) *slog.Logger

Returns a slog.Logger with additional attributes that persist across log calls.

Parameters:

  • args - Key-value pairs to add as persistent attributes

Returns:

  • *slog.Logger - Logger with added attributes

Example:

requestLogger := logger.With("request_id", "req-123", "user_id", "user-456")
requestLogger.Info("processing")  // Includes request_id and user_id

WithGroup

func (l *Logger) WithGroup(name string) *slog.Logger

Returns a slog.Logger with a group name for nested attributes.

Parameters:

  • name - Group name

Returns:

  • *slog.Logger - Logger with group

Example:

dbLogger := logger.WithGroup("database")
dbLogger.Info("query", "sql", "SELECT * FROM users")
// Output: {...,"database":{"sql":"SELECT * FROM users"}}

Configuration Methods

SetLevel

func (l *Logger) SetLevel(level Level) error

Dynamically changes the minimum log level at runtime.

Parameters:

  • level - New log level

Returns:

  • error - ErrCannotChangeLevel if using custom logger

Example:

if err := logger.SetLevel(logging.LevelDebug); err != nil {
    log.Printf("failed to change level: %v", err)
}

Level

func (l *Logger) Level() Level

Returns the current minimum log level.

Returns:

  • Level - Current log level

Example:

currentLevel := logger.Level()
fmt.Printf("Current level: %s\n", currentLevel)

Metadata Methods

ServiceName

func (l *Logger) ServiceName() string

Returns the configured service name.

Returns:

  • string - Service name, or empty if not configured

ServiceVersion

func (l *Logger) ServiceVersion() string

Returns the configured service version.

Returns:

  • string - Service version, or empty if not configured

Environment

func (l *Logger) Environment() string

Returns the configured environment.

Returns:

  • string - Environment, or empty if not configured

Lifecycle Methods

IsEnabled

func (l *Logger) IsEnabled() bool

Returns true if logging is enabled (not shut down).

Returns:

  • bool - Whether logger is active

DebugInfo

func (l *Logger) DebugInfo() map[string]any

Returns diagnostic information about logger state.

Returns:

  • map[string]any - Diagnostic information

Example:

info := logger.DebugInfo()
fmt.Printf("Handler: %s\n", info["handler_type"])
fmt.Printf("Level: %s\n", info["level"])

Shutdown

func (l *Logger) Shutdown(ctx context.Context) error

Gracefully shuts down the logger, flushing any buffered logs.

Parameters:

  • ctx - Context for timeout control

Returns:

  • error - Shutdown error, if any

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := logger.Shutdown(ctx); err != nil {
    fmt.Fprintf(os.Stderr, "shutdown error: %v\n", err)
}

Next Steps

For usage guides, see the Logging Guide.

3.2 - Options Reference

Complete reference for all logger configuration options

Complete reference for all configuration options available in the logging package.

Handler Options

Configure the output format for logs.

WithHandlerType

func WithHandlerType(t HandlerType) Option

Sets the logging handler type directly.

Parameters:

  • t - Handler type. Use JSONHandler, TextHandler, or ConsoleHandler.

Example:

logging.WithHandlerType(logging.JSONHandler)

WithJSONHandler

func WithJSONHandler() Option

Uses JSON structured logging. This is the default. Best for production and log aggregation.

Example:

logger := logging.MustNew(logging.WithJSONHandler())

Output format:

{"time":"2024-01-15T10:30:45.123Z","level":"INFO","msg":"test","key":"value"}

WithTextHandler

func WithTextHandler() Option

Uses text key=value logging. Good for systems that prefer this format.

Example:

logger := logging.MustNew(logging.WithTextHandler())

Output format:

time=2024-01-15T10:30:45.123Z level=INFO msg=test key=value

WithConsoleHandler

func WithConsoleHandler() Option

Uses human-readable console logging with colors. Best for development.

Example:

logger := logging.MustNew(logging.WithConsoleHandler())

Output format:

10:30:45.123 INFO  test key=value

Level Options

Configure the minimum log level.

WithLevel

func WithLevel(level Level) Option

Sets the minimum log level.

Parameters:

  • level - Minimum level (LevelDebug, LevelInfo, LevelWarn, LevelError)

Example:

logger := logging.MustNew(
    logging.WithLevel(logging.LevelInfo),
)

WithDebugLevel

func WithDebugLevel() Option

Convenience function to enable debug logging. Equivalent to WithLevel(LevelDebug).

Example:

logger := logging.MustNew(logging.WithDebugLevel())

Output Options

Configure where logs are written.

WithOutput

func WithOutput(w io.Writer) Option

Sets the output destination for logs.

Parameters:

  • w - io.Writer to write logs to

Default: os.Stdout

Example:

logFile, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logger := logging.MustNew(
    logging.WithOutput(logFile),
)

Multiple outputs:

logger := logging.MustNew(
    logging.WithOutput(io.MultiWriter(os.Stdout, logFile)),
)

Service Metadata Options

Configure service identification fields automatically added to every log entry.

WithServiceName

func WithServiceName(name string) Option

Sets the service name, automatically added to all log entries as service field.

Parameters:

  • name - Service name

Example:

logger := logging.MustNew(
    logging.WithServiceName("payment-api"),
)

WithServiceVersion

func WithServiceVersion(version string) Option

Sets the service version, automatically added to all log entries as version field.

Parameters:

  • version - Service version

Example:

logger := logging.MustNew(
    logging.WithServiceVersion("v2.1.0"),
)

WithEnvironment

func WithEnvironment(env string) Option

Sets the environment, automatically added to all log entries as env field.

Parameters:

  • env - Environment name

Example:

logger := logging.MustNew(
    logging.WithEnvironment("production"),
)

Combined example:

logger := logging.MustNew(
    logging.WithServiceName("payment-api"),
    logging.WithServiceVersion("v2.1.0"),
    logging.WithEnvironment("production"),
)
// All logs include: "service":"payment-api","version":"v2.1.0","env":"production"

Feature Options

Enable additional logging features.

WithSource

func WithSource(enabled bool) Option

Enables source code location (file and line number) in logs.

Parameters:

  • enabled - Whether to include source location

Default: false

Example:

logger := logging.MustNew(
    logging.WithSource(true),
)
// Output includes: "source":{"file":"main.go","line":42}

Note: Source location adds overhead. Use only for debugging.

WithDebugMode

func WithDebugMode(enabled bool) Option

Enables verbose debugging mode. Automatically enables debug level and source location.

Parameters:

  • enabled - Whether to enable debug mode

Example:

logger := logging.MustNew(
    logging.WithDebugMode(true),
)
// Equivalent to:
// WithDebugLevel() + WithSource(true)

WithGlobalLogger

func WithGlobalLogger() Option

Registers this logger as the global slog default logger. Allows third-party libraries using slog to use your configured logger.

Example:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithGlobalLogger(),
)
// Now slog.Info() uses this logger

Default: Not registered globally (allows multiple independent loggers)

WithSampling

func WithSampling(cfg SamplingConfig) Option

Enables log sampling to reduce volume in high-traffic scenarios.

Parameters:

  • cfg - Sampling configuration

Example:

logger := logging.MustNew(
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,         // First 1000 logs
        Thereafter: 100,          // Then 1% sampling
        Tick:       time.Minute,  // Reset every minute
    }),
)

SamplingConfig fields:

  • Initial (int) - Log first N entries unconditionally
  • Thereafter (int) - After Initial, log 1 of every M entries (0 = log all)
  • Tick (time.Duration) - Reset counter every interval (0 = never reset)

Note: Errors (level >= ERROR) always bypass sampling.

Advanced Options

Advanced configuration for specialized use cases.

WithReplaceAttr

func WithReplaceAttr(fn func(groups []string, a slog.Attr) slog.Attr) Option

Sets a custom attribute replacer function for transforming or filtering log attributes.

Parameters:

  • fn - Function to transform attributes

Example - Custom redaction:

logger := logging.MustNew(
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "credit_card" {
            return slog.String(a.Key, "***REDACTED***")
        }
        return a
    }),
)

Example - Dropping attributes:

logger := logging.MustNew(
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "internal_field" {
            return slog.Attr{}  // Drop this field
        }
        return a
    }),
)

Example - Transforming values:

logger := logging.MustNew(
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "time" {
            if t, ok := a.Value.Any().(time.Time); ok {
                return slog.String(a.Key, t.Format(time.RFC3339))
            }
        }
        return a
    }),
)

WithCustomLogger

func WithCustomLogger(customLogger *slog.Logger) Option

Uses a custom slog.Logger instead of creating one. For advanced use cases where you need full control over the logger.

Parameters:

  • customLogger - Pre-configured slog.Logger

Example:

customLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level:     slog.LevelDebug,
    AddSource: true,
}))

logger := logging.MustNew(
    logging.WithCustomLogger(customLogger),
)

Limitations:

  • Dynamic level changes (SetLevel) not supported
  • Service metadata must be added to custom logger directly

Configuration Examples

Development Configuration

logger := logging.MustNew(
    logging.WithConsoleHandler(),
    logging.WithDebugLevel(),
    logging.WithSource(true),
)

Production Configuration

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
    logging.WithServiceName(os.Getenv("SERVICE_NAME")),
    logging.WithServiceVersion(os.Getenv("VERSION")),
    logging.WithEnvironment("production"),
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,
        Thereafter: 100,
        Tick:       time.Minute,
    }),
)

Testing Configuration

buf := &bytes.Buffer{}
logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(buf),
    logging.WithLevel(logging.LevelDebug),
)

File Logging Configuration

logFile, _ := os.OpenFile("app.log",
    os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(logFile),
    logging.WithServiceName("myapp"),
)

Multiple Output Configuration

logFile, _ := os.OpenFile("app.log",
    os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(io.MultiWriter(os.Stdout, logFile)),
)

Next Steps

For usage guides, see the Configuration Guide.

3.3 - Testing Utilities

Complete reference for logging test utilities and helpers

Complete reference for testing utilities provided by the logging package.

Test Logger Creation

NewTestLogger

func NewTestLogger() (*Logger, *bytes.Buffer)

Creates a Logger for testing with an in-memory buffer. The logger is configured with JSON handler, debug level, and writes to the returned buffer.

Returns:

  • *Logger - Configured test logger
  • *bytes.Buffer - Buffer containing log output

Example:

func TestMyFunction(t *testing.T) {
    logger, buf := logging.NewTestLogger()
    
    myFunction(logger)
    
    entries, err := logging.ParseJSONLogEntries(buf)
    require.NoError(t, err)
    assert.Len(t, entries, 1)
}

ParseJSONLogEntries

func ParseJSONLogEntries(buf *bytes.Buffer) ([]LogEntry, error)

Parses JSON log entries from buffer into LogEntry slices. Creates a copy of the buffer so the original is not consumed.

Parameters:

  • buf - Buffer containing JSON log entries (one per line)

Returns:

  • []LogEntry - Parsed log entries
  • error - Parse error, if any

Example:

entries, err := logging.ParseJSONLogEntries(buf)
require.NoError(t, err)

for _, entry := range entries {
    fmt.Printf("%s: %s\n", entry.Level, entry.Message)
}

TestHelper

High-level testing utility with convenience methods.

NewTestHelper

func NewTestHelper(t *testing.T, opts ...Option) *TestHelper

Creates a TestHelper with in-memory logging and additional options.

Parameters:

  • t - Testing instance
  • opts - Optional configuration options

Returns:

  • *TestHelper - Test helper instance

Example:

func TestService(t *testing.T) {
    th := logging.NewTestHelper(t)
    
    svc := NewService(th.Logger)
    svc.DoSomething()
    
    th.AssertLog(t, "INFO", "operation completed", map[string]any{
        "status": "success",
    })
}

With custom configuration:

th := logging.NewTestHelper(t,
    logging.WithLevel(logging.LevelWarn),  // Only warnings and errors
)

TestHelper.Logs

func (th *TestHelper) Logs() ([]LogEntry, error)

Returns all parsed log entries.

Returns:

  • []LogEntry - All log entries
  • error - Parse error, if any

Example:

logs, err := th.Logs()
require.NoError(t, err)
assert.Len(t, logs, 3)

TestHelper.LastLog

func (th *TestHelper) LastLog() (*LogEntry, error)

Returns the most recent log entry.

Returns:

  • *LogEntry - Most recent log entry
  • error - Error if no logs or parse error

Example:

last, err := th.LastLog()
require.NoError(t, err)
assert.Equal(t, "INFO", last.Level)

TestHelper.ContainsLog

func (th *TestHelper) ContainsLog(msg string) bool

Checks if any log entry contains the given message.

Parameters:

  • msg - Message to search for

Returns:

  • bool - True if message found

Example:

if !th.ContainsLog("user created") {
    t.Error("expected user created log")
}

TestHelper.ContainsAttr

func (th *TestHelper) ContainsAttr(key string, value any) bool

Checks if any log entry contains the given attribute.

Parameters:

  • key - Attribute key
  • value - Attribute value

Returns:

  • bool - True if attribute found

Example:

if !th.ContainsAttr("user_id", "123") {
    t.Error("expected user_id attribute")
}

TestHelper.CountLevel

func (th *TestHelper) CountLevel(level string) int

Returns the number of log entries at the given level.

Parameters:

  • level - Log level (“DEBUG”, “INFO”, “WARN”, “ERROR”)

Returns:

  • int - Count of logs at that level

Example:

errorCount := th.CountLevel("ERROR")
assert.Equal(t, 2, errorCount)

TestHelper.Reset

func (th *TestHelper) Reset()

Clears the buffer for fresh testing.

Example:

th.Reset()  // Start fresh for next test phase

TestHelper.AssertLog

func (th *TestHelper) AssertLog(t *testing.T, level, msg string, attrs map[string]any)

Checks that a log entry exists with the given properties. Fails the test if not found.

Parameters:

  • t - Testing instance
  • level - Expected log level
  • msg - Expected message
  • attrs - Expected attributes

Example:

th.AssertLog(t, "INFO", "user created", map[string]any{
    "username": "alice",
    "email":    "alice@example.com",
})

LogEntry Type

type LogEntry struct {
    Time    time.Time
    Level   string
    Message string
    Attrs   map[string]any
}

Represents a parsed log entry for testing.

Fields:

  • Time - Log timestamp
  • Level - Log level (“DEBUG”, “INFO”, “WARN”, “ERROR”)
  • Message - Log message
  • Attrs - All other fields as map

Example:

entry := logs[0]
assert.Equal(t, "INFO", entry.Level)
assert.Equal(t, "test message", entry.Message)
assert.Equal(t, "value", entry.Attrs["key"])

Mock Writers

MockWriter

Records all writes for inspection.

Type:

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

Methods:

Write

func (mw *MockWriter) Write(p []byte) (n int, err error)

Implements io.Writer. Records the write.

WriteCount

func (mw *MockWriter) WriteCount() int

Returns the number of write calls.

BytesWritten

func (mw *MockWriter) BytesWritten() int

Returns total bytes written.

LastWrite

func (mw *MockWriter) LastWrite() []byte

Returns the most recent write.

Reset

func (mw *MockWriter) Reset()

Clears all recorded writes.

Example:

func TestWriteBehavior(t *testing.T) {
    mw := &logging.MockWriter{}
    logger := logging.MustNew(
        logging.WithJSONHandler(),
        logging.WithOutput(mw),
    )
    
    logger.Info("test 1")
    logger.Info("test 2")
    logger.Info("test 3")
    
    assert.Equal(t, 3, mw.WriteCount())
    assert.Contains(t, string(mw.LastWrite()), "test 3")
    assert.Greater(t, mw.BytesWritten(), 0)
}

CountingWriter

Counts bytes written without storing content.

Type:

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

Methods:

Write

func (cw *CountingWriter) Write(p []byte) (n int, err error)

Implements io.Writer. Counts bytes.

Count

func (cw *CountingWriter) Count() int64

Returns the total bytes written.

Example:

func TestLogVolume(t *testing.T) {
    cw := &logging.CountingWriter{}
    logger := logging.MustNew(
        logging.WithJSONHandler(),
        logging.WithOutput(cw),
    )
    
    for i := 0; i < 1000; i++ {
        logger.Info("test message", "index", i)
    }
    
    bytesLogged := cw.Count()
    t.Logf("Total bytes: %d", bytesLogged)
}

SlowWriter

Simulates slow I/O for testing timeouts.

Type:

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

Constructor:

NewSlowWriter

func NewSlowWriter(delay time.Duration, inner io.Writer) *SlowWriter

Creates a writer that delays each write.

Parameters:

  • delay - Delay duration for each write
  • inner - Optional inner writer to actually write to

Returns:

  • *SlowWriter - Slow writer instance

Example:

func TestSlowLogging(t *testing.T) {
    buf := &bytes.Buffer{}
    sw := logging.NewSlowWriter(100*time.Millisecond, buf)
    
    logger := logging.MustNew(
        logging.WithJSONHandler(),
        logging.WithOutput(sw),
    )
    
    start := time.Now()
    logger.Info("test")
    duration := time.Since(start)
    
    assert.GreaterOrEqual(t, duration, 100*time.Millisecond)
}

HandlerSpy

Implements slog.Handler and records all Handle calls.

Type:

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

Methods:

Enabled

func (hs *HandlerSpy) Enabled(_ context.Context, _ slog.Level) bool

Always returns true.

Handle

func (hs *HandlerSpy) Handle(_ context.Context, r slog.Record) error

Records the log record.

WithAttrs

func (hs *HandlerSpy) WithAttrs(_ []slog.Attr) slog.Handler

Returns the same handler (for compatibility).

WithGroup

func (hs *HandlerSpy) WithGroup(_ string) slog.Handler

Returns the same handler (for compatibility).

Records

func (hs *HandlerSpy) Records() []slog.Record

Returns all captured records.

RecordCount

func (hs *HandlerSpy) RecordCount() int

Returns the number of captured records.

Reset

func (hs *HandlerSpy) Reset()

Clears all captured records.

Example:

func TestHandlerBehavior(t *testing.T) {
    spy := &logging.HandlerSpy{}
    logger := slog.New(spy)
    
    logger.Info("test", "key", "value")
    
    assert.Equal(t, 1, spy.RecordCount())
    
    records := spy.Records()
    assert.Equal(t, "test", records[0].Message)
}

Testing Patterns

Testing Error Logging

func TestErrorHandling(t *testing.T) {
    th := logging.NewTestHelper(t)
    
    svc := NewService(th.Logger)
    err := svc.DoSomethingThatFails()
    
    require.Error(t, err)
    th.AssertLog(t, "ERROR", "operation failed", map[string]any{
        "error": "expected failure",
    })
}

Table-Driven Tests

func TestLogLevels(t *testing.T) {
    tests := []struct {
        name         string
        level        logging.Level
        expectLogged bool
    }{
        {"debug at info", logging.LevelInfo, false},
        {"info at info", logging.LevelInfo, true},
        {"error at warn", logging.LevelWarn, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            th := logging.NewTestHelper(t,
                logging.WithLevel(tt.level),
            )
            
            th.Logger.Debug("test")
            
            logs, _ := th.Logs()
            if tt.expectLogged {
                assert.Len(t, logs, 1)
            } else {
                assert.Len(t, logs, 0)
            }
        })
    }
}

Next Steps

For complete testing patterns, see the Testing Guide.

3.4 - Troubleshooting

Common issues and solutions for the logging package

Common issues and solutions when using the logging package.

Logs Not Appearing

Debug Logs Not Showing

Problem: Debug logs don’t appear in output.

Cause: Log level is set higher than Debug.

Solution: Enable debug level:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithDebugLevel(),  // Enable debug logs
)

Or check current level:

currentLevel := logger.Level()
fmt.Printf("Current level: %s\n", currentLevel)

No Logs at All

Problem: No logs appear, even errors.

Possible causes:

  1. Logger shutdown: Check if logger was shut down.
if !logger.IsEnabled() {
    fmt.Println("Logger is shut down")
}
  1. Wrong output: Verify output destination.
logger := logging.MustNew(
    logging.WithOutput(os.Stdout),  // Not stderr
)
  1. Sampling too aggressive: Check sampling configuration.
info := logger.DebugInfo()
if sampling, ok := info["sampling"]; ok {
    fmt.Printf("Sampling: %+v\n", sampling)
}

Logs Disappear After Some Time

Problem: Logs stop appearing after initial burst.

Cause: Log sampling is dropping logs.

Solution: Adjust sampling or disable:

// Less aggressive sampling
logger := logging.MustNew(
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,
        Thereafter: 10,  // 10% instead of 1%
        Tick:       time.Minute,
    }),
)

// Or disable sampling
logger := logging.MustNew(
    logging.WithJSONHandler(),
    // No WithSampling() call
)

Sensitive Data Issues

Sensitive Data Not Redacted

Problem: Custom sensitive fields not being redacted.

Cause: Only built-in fields are automatically redacted.

Solution: Add custom redaction:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        // Redact custom fields
        if a.Key == "credit_card" || a.Key == "ssn" {
            return slog.String(a.Key, "***REDACTED***")
        }
        return a
    }),
)

Built-in redacted fields:

  • password
  • token
  • secret
  • api_key
  • authorization

Too Much Redaction

Problem: Fields being redacted unnecessarily.

Cause: Field names match redaction patterns.

Solution: Rename fields to avoid keywords:

// Instead of "token" (redacted)
log.Info("processing", "request_token_id", tokenID)

// Instead of "secret" (redacted)
log.Info("config", "shared_secret_name", secretName)

Trace Correlation Issues

No Trace IDs in Logs

Problem: Logs don’t include trace_id and span_id.

Possible causes:

  1. Tracing not initialized:
// Initialize tracing
tracer := tracing.MustNew(
    tracing.WithOTLP("localhost:4317"),
)
defer tracer.Shutdown(context.Background())
  1. Not passing the request context when logging:
// Wrong - no context, so no trace_id/span_id
slog.Info("message")

// Right - pass context so trace_id and span_id are injected automatically
slog.InfoContext(ctx, "message")
  1. Context has no active span:
// Start a span so the context carries trace info
ctx, span := tracer.Start(context.Background(), "operation")
defer span.End()

slog.InfoContext(ctx, "message")  // Now includes trace_id and span_id

Wrong Trace IDs

Problem: Trace IDs don’t match distributed trace.

Cause: Context not properly propagated.

Solution: Ensure context flows through the call chain and pass it when you log:

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()  // Context carries trace from middleware
    
    result := processRequest(ctx)
    w.Write(result)
}

func processRequest(ctx context.Context) []byte {
    slog.InfoContext(ctx, "processing")  // Uses same context, so same trace
    return data
}

Performance Issues

High CPU Usage

Problem: Logging causes high CPU usage.

Possible causes:

  1. Logging in tight loops:
// Bad - logs thousands of times
for _, item := range items {
    logger.Debug("processing", "item", item)
}

// Good - log summary
logger.Info("processing batch", "count", len(items))
  1. Source location enabled in production:
// Bad for production
logger := logging.MustNew(
    logging.WithSource(true),  // Adds overhead
)

// Good for production
logger := logging.MustNew(
    logging.WithJSONHandler(),
    // No source location
)
  1. Debug level in production:
// Bad - debug logs have overhead even if filtered
logger := logging.MustNew(
    logging.WithDebugLevel(),
)

// Good - appropriate level
logger := logging.MustNew(
    logging.WithLevel(logging.LevelInfo),
)

High Memory Usage

Problem: Memory usage grows over time.

Possible causes:

  1. No log rotation: Logs written to file without rotation.

Solution: Use external log rotation (logrotate) or rotate in code:

// Use external tool like logrotate
// Or implement rotation
  1. Buffered output not flushed: Buffers growing without flush.

Solution: Ensure proper shutdown:

defer func() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    logger.Shutdown(ctx)
}()

Configuration Issues

Cannot Change Log Level

Problem: SetLevel returns error.

Cause: Using custom logger.

Error:

err := logger.SetLevel(logging.LevelDebug)
if errors.Is(err, logging.ErrCannotChangeLevel) {
    // Custom logger doesn't support dynamic level changes
}

Solution: Control level in custom logger:

var levelVar slog.LevelVar
levelVar.Set(slog.LevelInfo)

customLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: &levelVar,
}))

// Change level directly
levelVar.Set(slog.LevelDebug)

Service Metadata Not Appearing

Problem: Service name, version, or environment not in logs.

Cause: Not configured or using custom logger.

Solution: Configure service metadata:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("my-api"),
    logging.WithServiceVersion("v1.0.0"),
    logging.WithEnvironment("production"),
)

For custom logger, add metadata manually:

customLogger := slog.New(handler).With(
    "service", "my-api",
    "version", "v1.0.0",
    "env", "production",
)

Router Integration Issues

Access Log Not Working

Problem: HTTP requests not being logged.

Possible causes:

  1. Logger not set on router:
r := router.MustNew()
logger := logging.MustNew(logging.WithJSONHandler())
r.SetLogger(logger)  // Must set logger
  1. Middleware not applied:
import "rivaas.dev/router/middleware/accesslog"

r.Use(accesslog.New())  // Apply middleware
  1. Path excluded:
r.Use(accesslog.New(
    accesslog.WithExcludePaths("/health", "/metrics"),
))
// /health and /metrics won't be logged

No Trace IDs in Handler Logs

Problem: Handler logs have no trace_id or span_id.

Cause: Tracing not initialized, or you are not using the context when logging.

Solution: Initialize tracing with the app, and always pass the request context when you log:

a, _ := app.New(
    app.WithServiceName("my-api"),
    app.WithObservability(
        app.WithLogging(logging.WithJSONHandler()),
        app.WithTracing(tracing.WithOTLP("localhost:4317")),
    ),
)
// In handlers, use: slog.InfoContext(c.RequestContext(), "message", ...)

Trace IDs are injected automatically for any slog.*Context(ctx, ...) call when the context has an active OpenTelemetry span.

Testing Issues

Test Logs Not Captured

Problem: Logs not appearing in test buffer.

Cause: Using wrong logger instance.

Solution: Use TestHelper or ensure buffer is captured:

func TestMyFunction(t *testing.T) {
    th := logging.NewTestHelper(t)
    
    myFunction(th.Logger)  // Pass test logger
    
    logs, _ := th.Logs()
    assert.Len(t, logs, 1)
}

Parse Errors

Problem: ParseJSONLogEntries returns error.

Cause: Non-JSON output or malformed JSON.

Solution: Ensure JSON handler:

th := logging.NewTestHelper(t,
    logging.WithJSONHandler(),  // Must be JSON
)

Error Types

ErrNilLogger

var ErrNilLogger = errors.New("custom logger is nil")

When: Providing nil custom logger.

Solution:

if customLogger != nil {
    logger := logging.MustNew(
        logging.WithCustomLogger(customLogger),
    )
}

ErrInvalidHandler

var ErrInvalidHandler = errors.New("invalid handler type")

When: Invalid handler type specified.

Solution: Use valid handler types:

logging.WithHandlerType(logging.JSONHandler)
logging.WithHandlerType(logging.TextHandler)
logging.WithHandlerType(logging.ConsoleHandler)

ErrLoggerShutdown

var ErrLoggerShutdown = errors.New("logger is shut down")

When: Operations after shutdown.

Solution: Don’t use logger after shutdown:

defer logger.Shutdown(context.Background())
// Don't log after this point

ErrInvalidLevel

var ErrInvalidLevel = errors.New("invalid log level")

When: Invalid log level provided.

Solution: Use valid levels:

logging.LevelDebug
logging.LevelInfo
logging.LevelWarn
logging.LevelError

ErrCannotChangeLevel

var ErrCannotChangeLevel = errors.New("cannot change level on custom logger")

When: Calling SetLevel on custom logger.

Solution: Control level in custom logger directly or don’t use custom logger.

Getting Help

If you encounter issues not covered here:

  1. Check the API Reference for method details
  2. Review Examples for patterns
  3. See Best Practices for recommendations
  4. Check the GitHub issues

Debugging Tips

Enable Debug Info

info := logger.DebugInfo()
fmt.Printf("Logger state: %+v\n", info)

Check Sampling State

info := logger.DebugInfo()
if sampling, ok := info["sampling"]; ok {
    fmt.Printf("Sampling config: %+v\n", sampling)
}

Verify Configuration

fmt.Printf("Service: %s\n", logger.ServiceName())
fmt.Printf("Version: %s\n", logger.ServiceVersion())
fmt.Printf("Environment: %s\n", logger.Environment())
fmt.Printf("Level: %s\n", logger.Level())
fmt.Printf("Enabled: %v\n", logger.IsEnabled())

Next Steps

4 - OpenAPI Package

API reference for rivaas.dev/openapi - Automatic OpenAPI specification generation

This is the API reference for the rivaas.dev/openapi package. For learning-focused documentation, see the OpenAPI Guide.

Package Information

Package Overview

The openapi package provides automatic OpenAPI 3.0.4 and 3.1.2 specification generation from Go code using struct tags and reflection.

Core Features

  • Automatic OpenAPI specification generation from Go code
  • Support for OpenAPI 3.0.4 and 3.1.2 specifications
  • Type-safe version selection with V30x and V31x constants
  • Fluent HTTP method constructors (GET, POST, PUT, etc.)
  • Automatic parameter discovery from struct tags
  • Schema generation from Go types
  • Built-in validation against official meta-schemas
  • Type-safe warning diagnostics via diag package
  • Swagger UI configuration support

Architecture

The package is organized into two main components:

Main Package (rivaas.dev/openapi)

Core specification generation including:

  • API struct - Configuration container
  • New() / MustNew() - API initialization
  • HTTP method constructors - GET(), POST(), PUT(), etc.
  • Operation options - WithRequest(), WithResponse(), WithSecurity(), etc.
  • Generate() - Specification generation

Sub-package (rivaas.dev/openapi/diag)

Type-safe warning diagnostics:

  • Warning interface - Individual warning
  • Warnings type - Warning collection
  • WarningCode type - Type-safe warning codes
  • WarningCategory type - Warning categories

Validator (rivaas.dev/openapi/validate)

Standalone specification validator:

  • Validator type - Validates OpenAPI specifications
  • Validate() - Validate against specific version
  • ValidateAuto() - Auto-detect version and validate

Quick API Index

API Creation

api, err := openapi.New(options...)     // With error handling
api := openapi.MustNew(options...)      // Panics on error

Specification Generation

result, err := api.Generate(ctx context.Context, operations...)

HTTP Method Constructors

openapi.GET(path, ...opts) Operation
openapi.POST(path, ...opts) Operation
openapi.PUT(path, ...opts) Operation
openapi.PATCH(path, ...opts) Operation
openapi.DELETE(path, ...opts) Operation
openapi.HEAD(path, ...opts) Operation
openapi.OPTIONS(path, ...opts) Operation
openapi.TRACE(path, ...opts) Operation

Result Access

result.JSON      // OpenAPI spec as JSON bytes
result.YAML      // OpenAPI spec as YAML bytes
result.Warnings  // Generation warnings

Reference Pages

API Reference

Core types, HTTP method constructors, and generation API.

View →

Options

API-level configuration for info, servers, and security.

View →

Operation Options

Operation-level configuration for endpoints.

View →

Swagger UI Options

Customize the Swagger UI interface.

View →

Diagnostics

Warning system and diagnostic codes.

View →

Troubleshooting

Common issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Type Reference

API

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

Main API configuration container. Created via New() or MustNew() with functional options.

Operation

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

Represents an HTTP operation with method, path, and metadata. Created via HTTP method constructors.

Result

type Result struct {
    JSON     []byte    // OpenAPI spec as JSON
    YAML     []byte    // OpenAPI spec as YAML
    Warnings Warnings  // Generation warnings
}

Result of specification generation containing the spec in multiple formats and any warnings.

Version

type Version int

const (
    V30x Version = iota  // OpenAPI 3.0.x (generates 3.0.4)
    V31x                 // OpenAPI 3.1.x (generates 3.1.2)
)

Type-safe OpenAPI version selection.

Option

type Option func(*API) error

Functional option for API configuration.

OperationOption

type OperationOption func(*Operation) error

Functional option for operation configuration.

Common Patterns

Basic Generation

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
)

result, err := api.Generate(context.Background(),
    openapi.GET("/users/:id",
        openapi.WithSummary("Get user"),
        openapi.WithResponse(200, User{}),
    ),
)

With Security

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
)

result, err := api.Generate(context.Background(),
    openapi.GET("/users/:id",
        openapi.WithSecurity("bearerAuth"),
        openapi.WithResponse(200, User{}),
    ),
)

With Validation

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithValidation(true),
)

result, err := api.Generate(context.Background(), operations...)
// Fails if spec is invalid

With Diagnostics

import "rivaas.dev/openapi/diag"

result, err := api.Generate(context.Background(), operations...)
if err != nil {
    log.Fatal(err)
}

if result.Warnings.Has(diag.WarnDownlevelInfoSummary) {
    log.Warn("info.summary was dropped")
}

Thread Safety

The API type is safe for concurrent use:

  • Multiple goroutines can call Generate() simultaneously
  • Configuration is immutable after creation

Not thread-safe:

  • Modifying API configuration during initialization

Performance Notes

  • Schema generation: First use per type ~500ns (reflection), subsequent uses ~50ns (cached)
  • Validation: Adds 10-20ms on first validation (schema compilation), 1-5ms subsequent
  • Generation: Depends on operation count and complexity

Version Compatibility

The package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

Next Steps

For learning-focused guides, see the OpenAPI Guide.

4.1 - API Reference

Complete API reference for types, functions, and methods

Complete reference for all types, functions, and methods in the openapi package.

Key Types

API

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

Main API configuration container. Holds the OpenAPI specification metadata and configuration.

Created by:

  • New(...Option) (*API, error) - With error handling.
  • MustNew(...Option) *API - Panics on error.

Methods:

  • Generate(ctx context.Context, ...Operation) (*Result, error) - Generate OpenAPI specification.
  • Version() string - Get target OpenAPI version like “3.0.4” or “3.1.2”.

Operation

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

Represents an HTTP operation with method, path, and configuration.

Created by HTTP method constructors:

  • GET(path string, ...OperationOption) Operation
  • POST(path string, ...OperationOption) Operation
  • PUT(path string, ...OperationOption) Operation
  • PATCH(path string, ...OperationOption) Operation
  • DELETE(path string, ...OperationOption) Operation
  • HEAD(path string, ...OperationOption) Operation
  • OPTIONS(path string, ...OperationOption) Operation
  • TRACE(path string, ...OperationOption) Operation

Result

type Result struct {
    JSON     []byte
    YAML     []byte
    Warnings Warnings
}

Result of specification generation.

Fields:

  • JSON - OpenAPI specification as JSON bytes.
  • YAML - OpenAPI specification as YAML bytes.
  • Warnings - Collection of generation warnings. Check Diagnostics for details.

Version

type Version int

const (
    V30x Version = iota  // OpenAPI 3.0.x (generates 3.0.4)
    V31x                 // OpenAPI 3.1.x (generates 3.1.2)
)

Type-safe OpenAPI version selection. Use with WithVersion() option.

Constants:

  • V30x - Target OpenAPI 3.0.x family. Generates 3.0.4 specification.
  • V31x - Target OpenAPI 3.1.x family. Generates 3.1.2 specification.

Option

type Option func(*API) error

Functional option for configuring the API. See Options for all available options.

OperationOption

type OperationOption func(*Operation) error

Functional option for configuring operations. See Operation Options for all available options.

Functions

New

func New(opts ...Option) (*API, error)

Creates a new API configuration with error handling.

Parameters:

  • opts - Variable number of Option functions

Returns:

  • *API - Configured API instance
  • error - Configuration error if any

Example:

api, err := openapi.New(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithServer("http://localhost:8080", "Development"),
)
if err != nil {
    log.Fatal(err)
}

MustNew

func MustNew(opts ...Option) *API

Creates a new API configuration. Panics if configuration fails.

Parameters:

  • opts - Variable number of Option functions

Returns:

  • *API - Configured API instance

Panics:

  • If configuration fails

Example:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithServer("http://localhost:8080", "Development"),
)

HTTP Method Constructors

GET

func GET(path string, opts ...OperationOption) Operation

Creates a GET operation.

Parameters:

  • path - URL path (use :param syntax for path parameters)
  • opts - Variable number of OperationOption functions

Returns:

  • Operation - Configured operation

Example:

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)

POST

func POST(path string, opts ...OperationOption) Operation

Creates a POST operation.

Parameters:

  • path - URL path
  • opts - Variable number of OperationOption functions

Returns:

  • Operation - Configured operation

Example:

openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)

PUT

func PUT(path string, opts ...OperationOption) Operation

Creates a PUT operation.

PATCH

func PATCH(path string, opts ...OperationOption) Operation

Creates a PATCH operation.

DELETE

func DELETE(path string, opts ...OperationOption) Operation

Creates a DELETE operation.

Example:

openapi.DELETE("/users/:id",
    openapi.WithSummary("Delete user"),
    openapi.WithResponse(204, nil),
)
func HEAD(path string, opts ...OperationOption) Operation

Creates a HEAD operation.

OPTIONS

func OPTIONS(path string, opts ...OperationOption) Operation

Creates an OPTIONS operation.

TRACE

func TRACE(path string, opts ...OperationOption) Operation

Creates a TRACE operation.

Methods

API.Generate

func (api *API) Generate(ctx context.Context, operations ...Operation) (*Result, error)

Generates an OpenAPI specification from the configured API and operations.

Parameters:

  • ctx - Context for cancellation
  • operations - Variable number of Operation instances

Returns:

  • *Result - Generation result with JSON, YAML, and warnings
  • error - Generation or validation error if any

Errors:

  • Returns error if context is nil
  • Returns error if generation fails
  • Returns error if validation is enabled and spec is invalid

Example:

result, err := api.Generate(context.Background(),
    openapi.GET("/users/:id",
        openapi.WithSummary("Get user"),
        openapi.WithResponse(200, User{}),
    ),
    openapi.POST("/users",
        openapi.WithSummary("Create user"),
        openapi.WithRequest(CreateUserRequest{}),
        openapi.WithResponse(201, User{}),
    ),
)
if err != nil {
    log.Fatal(err)
}

// Use result.JSON or result.YAML
fmt.Println(string(result.JSON))

API.Version

func (api *API) Version() string

Returns the target OpenAPI version as a string.

Returns:

  • string - Version string (“3.0.4” or “3.1.2”)

Example:

api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V31x),
)

fmt.Println(api.Version()) // "3.1.2"

Type Aliases and Constants

Parameter Locations

const (
    InHeader ParameterLocation = "header"
    InQuery  ParameterLocation = "query"
    InCookie ParameterLocation = "cookie"
)

Used with WithAPIKey() to specify where the API key is located.

OAuth2 Flow Types

const (
    FlowAuthorizationCode OAuthFlowType = "authorizationCode"
    FlowImplicit          OAuthFlowType = "implicit"
    FlowPassword          OAuthFlowType = "password"
    FlowClientCredentials OAuthFlowType = "clientCredentials"
)

Used with WithOAuth2() to specify the OAuth2 flow type.

Swagger UI Constants

// Document expansion
const (
    DocExpansionList DocExpansion = "list"
    DocExpansionFull DocExpansion = "full"
    DocExpansionNone DocExpansion = "none"
)

// Model rendering
const (
    ModelRenderingExample ModelRendering = "example"
    ModelRenderingModel   ModelRendering = "model"
)

// Operations sorting
const (
    OperationsSorterAlpha  OperationsSorter = "alpha"
    OperationsSorterMethod OperationsSorter = "method"
)

// Tags sorting
const (
    TagsSorterAlpha TagsSorter = "alpha"
)

// Validators (untyped string constants)
const (
    ValidatorLocal = "local"  // Use embedded meta-schema validation
    ValidatorNone  = "none"   // Disable validation
)

// Syntax themes
const (
    SyntaxThemeAgate        SyntaxTheme = "agate"
    SyntaxThemeArta         SyntaxTheme = "arta"
    SyntaxThemeMonokai      SyntaxTheme = "monokai"
    SyntaxThemeNord         SyntaxTheme = "nord"
    SyntaxThemeObsidian     SyntaxTheme = "obsidian"
    SyntaxThemeTomorrowNight SyntaxTheme = "tomorrow-night"
    SyntaxThemeIdea         SyntaxTheme = "idea"
)

// Request snippet languages
const (
    SnippetCurlBash       RequestSnippetLanguage = "curl_bash"
    SnippetCurlPowerShell RequestSnippetLanguage = "curl_powershell"
    SnippetCurlCmd        RequestSnippetLanguage = "curl_cmd"
)

See Swagger UI Options for usage.

Next Steps

4.2 - API Options

Complete reference for API-level configuration options

Complete reference for all API-level configuration options (functions passed to New() or MustNew()).

Info Options

WithTitle

func WithTitle(title, version string) Option

Sets the API title and version. Required.

Parameters:

  • title - API title.
  • version - API version like “1.0.0”.

Example:

openapi.WithTitle("My API", "1.0.0")

WithInfoDescription

func WithInfoDescription(description string) Option

Sets the API description.

Example:

openapi.WithInfoDescription("Comprehensive API for managing users and resources")

WithInfoSummary

func WithInfoSummary(summary string) Option

Sets a short summary for the API. OpenAPI 3.1 only. Generates warning if used with 3.0 target.

Example:

openapi.WithInfoSummary("User Management API")

WithTermsOfService

func WithTermsOfService(url string) Option

Sets the terms of service URL.

Example:

openapi.WithTermsOfService("https://example.com/terms")

WithContact

func WithContact(name, url, email string) Option

Sets contact information.

Parameters:

  • name - Contact name
  • url - Contact URL
  • email - Contact email

Example:

openapi.WithContact("API Support", "https://example.com/support", "support@example.com")

WithLicense

func WithLicense(name, url string) Option

Sets license information.

Parameters:

  • name - License name
  • url - License URL

Example:

openapi.WithLicense("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0.html")

WithLicenseIdentifier

func WithLicenseIdentifier(name, identifier string) Option

Sets license with SPDX identifier. OpenAPI 3.1 only.

Parameters:

  • name - License name
  • identifier - SPDX license identifier

Example:

openapi.WithLicenseIdentifier("Apache 2.0", "Apache-2.0")

WithInfoExtension

func WithInfoExtension(key string, value any) Option

Adds a custom extension to the info object.

Parameters:

  • key - Extension key (must start with x-)
  • value - Extension value

Example:

openapi.WithInfoExtension("x-api-id", "user-service")

Version Options

WithVersion

func WithVersion(version Version) Option

Sets the target OpenAPI version. Default is V30x.

Parameters:

  • version - Either V30x or V31x

Example:

openapi.WithVersion(openapi.V31x)

Server Options

WithServer

func WithServer(url, description string) Option

Adds a server configuration.

Parameters:

  • url - Server URL
  • description - Server description

Example:

openapi.WithServer("https://api.example.com", "Production")
openapi.WithServer("http://localhost:8080", "Development")

WithServerVariable

func WithServerVariable(name, defaultValue string, enumValues []string, description string) Option

Adds a server variable for URL templating.

Parameters:

  • name - Variable name
  • defaultValue - Default value
  • enumValues - Allowed values
  • description - Variable description

Example:

openapi.WithServer("https://{environment}.example.com", "Environment-based"),
openapi.WithServerVariable("environment", "api", 
    []string{"api", "staging", "dev"},
    "Environment to use",
)

Security Scheme Options

WithBearerAuth

func WithBearerAuth(name, description string) Option

Adds Bearer (JWT) authentication scheme.

Parameters:

  • name - Security scheme name (used in WithSecurity())
  • description - Scheme description

Example:

openapi.WithBearerAuth("bearerAuth", "JWT authentication")

WithAPIKey

func WithAPIKey(name, paramName string, location ParameterLocation, description string) Option

Adds API key authentication scheme.

Parameters:

  • name - Security scheme name
  • paramName - Parameter name (e.g., “X-API-Key”, “api_key”)
  • location - Where the key is located: InHeader, InQuery, or InCookie
  • description - Scheme description

Example:

openapi.WithAPIKey("apiKey", "X-API-Key", openapi.InHeader, "API key for authentication")

WithOAuth2

func WithOAuth2(name, description string, flows ...OAuth2Flow) Option

Adds OAuth2 authentication scheme.

Parameters:

  • name - Security scheme name
  • description - Scheme description
  • flows - OAuth2 flow configurations

Example:

openapi.WithOAuth2("oauth2", "OAuth2 authentication",
    openapi.OAuth2Flow{
        Type:             openapi.FlowAuthorizationCode,
        AuthorizationURL: "https://example.com/oauth/authorize",
        TokenURL:         "https://example.com/oauth/token",
        Scopes: map[string]string{
            "read":  "Read access",
            "write": "Write access",
        },
    },
)

WithOpenIDConnect

func WithOpenIDConnect(name, openIDConnectURL, description string) Option

Adds OpenID Connect authentication scheme.

Parameters:

  • name - Security scheme name
  • openIDConnectURL - OpenID Connect discovery URL
  • description - Scheme description

Example:

openapi.WithOpenIDConnect("openId", "https://example.com/.well-known/openid-configuration", "OpenID Connect")

WithDefaultSecurity

func WithDefaultSecurity(scheme string, scopes ...string) Option

Sets default security requirement at API level (applies to all operations unless overridden).

Parameters:

  • scheme - Security scheme name
  • scopes - Optional OAuth2 scopes

Example:

openapi.WithDefaultSecurity("bearerAuth")
openapi.WithDefaultSecurity("oauth2", "read", "write")

Tag Options

WithTag

func WithTag(name, description string) Option

Adds a tag for organizing operations.

Parameters:

  • name - Tag name
  • description - Tag description

Example:

openapi.WithTag("users", "User management operations")
openapi.WithTag("posts", "Post management operations")

External Documentation

WithExternalDocs

func WithExternalDocs(url, description string) Option

Links to external documentation.

Parameters:

  • url - Documentation URL
  • description - Documentation description

Example:

openapi.WithExternalDocs("https://docs.example.com", "Full API Documentation")

Validation Options

WithValidation

func WithValidation(enabled bool) Option

Enables or disables specification validation. Default is false.

Parameters:

  • enabled - Whether to validate generated specs

Example:

openapi.WithValidation(true) // Enable validation

WithStrictDownlevel

func WithStrictDownlevel(enabled bool) Option

Enables strict downlevel mode. When enabled, using 3.1 features with a 3.0 target causes errors instead of warnings. Default is false.

Parameters:

  • enabled - Whether to error on downlevel issues

Example:

openapi.WithStrictDownlevel(true) // Error on 3.1 features with 3.0 target

WithSpecPath

func WithSpecPath(path string) Option

Sets the path where the OpenAPI specification will be served.

Parameters:

  • path - URL path for the spec (e.g., “/openapi.json”)

Example:

openapi.WithSpecPath("/api/openapi.json")

Swagger UI Options

WithSwaggerUI

func WithSwaggerUI(path string, opts ...UIOption) Option

Configures Swagger UI at the specified path.

Parameters:

  • path - URL path where Swagger UI is served
  • opts - Swagger UI configuration options (see Swagger UI Options)

Example:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIExpansion(openapi.DocExpansionList),
    openapi.WithUITryItOut(true),
)

WithoutSwaggerUI

func WithoutSwaggerUI() Option

Disables Swagger UI.

Example:

openapi.WithoutSwaggerUI()

Extension Options

WithExtension

func WithExtension(key string, value interface{}) Option

Adds a custom x-* extension to the root of the specification.

Parameters:

  • key - Extension key (must start with x-)
  • value - Extension value (any JSON-serializable type)

Example:

openapi.WithExtension("x-api-version", "v2")
openapi.WithExtension("x-custom-config", map[string]interface{}{
    "feature": "enabled",
    "rate-limit": 100,
})

Next Steps

4.3 - Operation Options

Complete reference for operation-level configuration options

Complete reference for all operation-level configuration options (functions passed to HTTP method constructors).

Metadata Options

WithSummary

func WithSummary(summary string) OperationOption

Sets the operation summary (short description).

Example:

openapi.WithSummary("Get user by ID")

WithDescription

func WithDescription(description string) OperationOption

Sets the operation description (detailed explanation).

Example:

openapi.WithDescription("Retrieves a user by their unique identifier from the database")

WithOperationID

func WithOperationID(operationID string) OperationOption

Sets a custom operation ID. By default, operation IDs are auto-generated from method and path.

Example:

openapi.WithOperationID("getUserById")

Request and Response Options

WithRequest

func WithRequest(requestType interface{}, examples ...interface{}) OperationOption

Sets the request body type with optional examples.

Parameters:

  • requestType - Go type to convert to schema
  • examples - Optional example instances

Example:

exampleUser := CreateUserRequest{Name: "John", Email: "john@example.com"}
openapi.WithRequest(CreateUserRequest{}, exampleUser)

WithResponse

func WithResponse(statusCode int, responseType interface{}, examples ...interface{}) OperationOption

Adds a response type for a specific status code.

Parameters:

  • statusCode - HTTP status code
  • responseType - Go type to convert to schema (use nil for no body)
  • examples - Optional example instances

Example:

openapi.WithResponse(200, User{})
openapi.WithResponse(204, nil) // No response body
openapi.WithResponse(404, ErrorResponse{})

Organization Options

WithTags

func WithTags(tags ...string) OperationOption

Adds tags to the operation for organization.

Parameters:

  • tags - Tag names

Example:

openapi.WithTags("users")
openapi.WithTags("users", "admin")

Security Options

WithSecurity

func WithSecurity(scheme string, scopes ...string) OperationOption

Adds a security requirement to the operation.

Parameters:

  • scheme - Security scheme name (defined with WithBearerAuth, WithAPIKey, etc.)
  • scopes - Optional OAuth2 scopes

Example:

openapi.WithSecurity("bearerAuth")
openapi.WithSecurity("oauth2", "read", "write")

Multiple calls create alternative security requirements (OR logic):

openapi.GET("/users/:id",
    openapi.WithSecurity("bearerAuth"),  // Can use bearer auth
    openapi.WithSecurity("apiKey"),      // OR can use API key
    openapi.WithResponse(200, User{}),
)

Content Type Options

WithConsumes

func WithConsumes(contentTypes ...string) OperationOption

Sets accepted content types for the request.

Parameters:

  • contentTypes - MIME types

Example:

openapi.WithConsumes("application/json", "application/xml")

WithProduces

func WithProduces(contentTypes ...string) OperationOption

Sets returned content types for the response.

Parameters:

  • contentTypes - MIME types

Example:

openapi.WithProduces("application/json", "application/xml")

Deprecation

WithDeprecated

func WithDeprecated() OperationOption

Marks the operation as deprecated.

Example:

openapi.GET("/users/legacy",
    openapi.WithSummary("Legacy user list"),
    openapi.WithDeprecated(),
    openapi.WithResponse(200, []User{}),
)

Extension Options

WithOperationExtension

func WithOperationExtension(key string, value interface{}) OperationOption

Adds a custom x-* extension to the operation.

Parameters:

  • key - Extension key (must start with x-)
  • value - Extension value (any JSON-serializable type)

Example:

openapi.WithOperationExtension("x-rate-limit", 100)
openapi.WithOperationExtension("x-cache-ttl", 300)

Composable Options

WithOptions

func WithOptions(opts ...OperationOption) OperationOption

Combines multiple operation options into a single reusable option.

Parameters:

  • opts - Operation options to combine

Example:

CommonErrors := openapi.WithOptions(
    openapi.WithResponse(400, ErrorResponse{}),
    openapi.WithResponse(500, ErrorResponse{}),
)

UserEndpoint := openapi.WithOptions(
    openapi.WithTags("users"),
    openapi.WithSecurity("bearerAuth"),
    CommonErrors,
)

// Use in operations
openapi.GET("/users/:id",
    UserEndpoint,
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)

Option Summary Table

OptionDescription
WithSummary(s)Set operation summary
WithDescription(s)Set operation description
WithOperationID(id)Set custom operation ID
WithRequest(type, examples...)Set request body type
WithResponse(status, type, examples...)Set response type for status code
WithTags(tags...)Add tags to operation
WithSecurity(scheme, scopes...)Add security requirement
WithDeprecated()Mark operation as deprecated
WithConsumes(types...)Set accepted content types
WithProduces(types...)Set returned content types
WithOperationExtension(key, value)Add operation extension
WithOptions(opts...)Combine options into reusable set

Next Steps

4.4 - Swagger UI Options

Complete reference for Swagger UI configuration options

Complete reference for all Swagger UI configuration options (functions passed to WithSwaggerUI()).

Display Options

WithUIExpansion

func WithUIExpansion(expansion DocExpansion) UIOption

Controls initial document expansion.

Parameters:

  • expansion - DocExpansionList, DocExpansionFull, or DocExpansionNone

Values:

  • DocExpansionList - Show endpoints, hide details (default)
  • DocExpansionFull - Show endpoints and details
  • DocExpansionNone - Hide everything

Example:

openapi.WithUIExpansion(openapi.DocExpansionFull)

WithUIDefaultModelRendering

func WithUIDefaultModelRendering(rendering ModelRendering) UIOption

Controls how models/schemas are rendered.

Parameters:

  • rendering - ModelRenderingExample or ModelRenderingModel

Example:

openapi.WithUIDefaultModelRendering(openapi.ModelRenderingExample)

WithUIModelExpandDepth

func WithUIModelExpandDepth(depth int) UIOption

Controls how deeply a single model is expanded.

Parameters:

  • depth - Expansion depth (-1 to disable, 1 for shallow, higher for deeper)

Example:

openapi.WithUIModelExpandDepth(2)

WithUIModelsExpandDepth

func WithUIModelsExpandDepth(depth int) UIOption

Controls how deeply the models section is expanded.

Example:

openapi.WithUIModelsExpandDepth(1)

WithUIDisplayOperationID

func WithUIDisplayOperationID(display bool) UIOption

Shows operation IDs alongside summaries.

Example:

openapi.WithUIDisplayOperationID(true)

Try It Out Options

WithUITryItOut

func WithUITryItOut(enabled bool) UIOption

Enables “Try it out” functionality.

Example:

openapi.WithUITryItOut(true)

WithUIRequestSnippets

func WithUIRequestSnippets(enabled bool, languages ...RequestSnippetLanguage) UIOption

Shows code snippets for making requests.

Parameters:

  • enabled - Whether to show snippets
  • languages - Snippet languages to show

Languages:

  • SnippetCurlBash - curl for bash/sh shells
  • SnippetCurlPowerShell - curl for PowerShell
  • SnippetCurlCmd - curl for Windows CMD

Example:

openapi.WithUIRequestSnippets(true,
    openapi.SnippetCurlBash,
    openapi.SnippetCurlPowerShell,
    openapi.SnippetCurlCmd,
)

WithUIRequestSnippetsExpanded

func WithUIRequestSnippetsExpanded(expanded bool) UIOption

Expands request snippets by default.

Example:

openapi.WithUIRequestSnippetsExpanded(true)

WithUIDisplayRequestDuration

func WithUIDisplayRequestDuration(display bool) UIOption

Shows how long requests take.

Example:

openapi.WithUIDisplayRequestDuration(true)

Filtering and Sorting Options

WithUIFilter

func WithUIFilter(enabled bool) UIOption

Enables filter/search box.

Example:

openapi.WithUIFilter(true)

WithUIMaxDisplayedTags

func WithUIMaxDisplayedTags(max int) UIOption

Limits the number of tags displayed.

Example:

openapi.WithUIMaxDisplayedTags(10)

WithUIOperationsSorter

func WithUIOperationsSorter(sorter OperationsSorter) UIOption

Sets operation sorting method.

Parameters:

  • sorter - OperationsSorterAlpha or OperationsSorterMethod

Example:

openapi.WithUIOperationsSorter(openapi.OperationsSorterAlpha)

WithUITagsSorter

func WithUITagsSorter(sorter TagsSorter) UIOption

Sets tag sorting method.

Parameters:

  • sorter - TagsSorterAlpha

Example:

openapi.WithUITagsSorter(openapi.TagsSorterAlpha)

Syntax Highlighting Options

WithUISyntaxHighlight

func WithUISyntaxHighlight(enabled bool) UIOption

Enables syntax highlighting.

Example:

openapi.WithUISyntaxHighlight(true)

WithUISyntaxTheme

func WithUISyntaxTheme(theme SyntaxTheme) UIOption

Sets syntax highlighting theme.

Available Themes:

  • SyntaxThemeAgate - Dark theme with blue accents
  • SyntaxThemeArta - Dark theme with orange accents
  • SyntaxThemeMonokai - Dark theme with vibrant colors
  • SyntaxThemeNord - Dark theme with cool blue tones
  • SyntaxThemeObsidian - Dark theme with green accents
  • SyntaxThemeTomorrowNight - Dark theme with muted colors
  • SyntaxThemeIdea - Light theme similar to IntelliJ IDEA

Example:

openapi.WithUISyntaxTheme(openapi.SyntaxThemeMonokai)

Authentication Options

WithUIPersistAuth

func WithUIPersistAuth(persist bool) UIOption

Persists authentication across browser refreshes.

Example:

openapi.WithUIPersistAuth(true)

WithUIWithCredentials

func WithUIWithCredentials(withCredentials bool) UIOption

Includes credentials in requests.

Example:

openapi.WithUIWithCredentials(true)

Additional Options

WithUIDeepLinking

func WithUIDeepLinking(enabled bool) UIOption

Enables deep linking for tags and operations.

Example:

openapi.WithUIDeepLinking(true)

WithUIShowExtensions

func WithUIShowExtensions(show bool) UIOption

Shows vendor extensions (x-*) in the UI.

Example:

openapi.WithUIShowExtensions(true)

WithUIShowCommonExtensions

func WithUIShowCommonExtensions(show bool) UIOption

Shows common extensions in the UI.

Example:

openapi.WithUIShowCommonExtensions(true)

WithUISupportedMethods

func WithUISupportedMethods(methods ...HTTPMethod) UIOption

Configures which HTTP methods are supported for “Try it out”.

Parameters:

  • methods - HTTP method constants (MethodGet, MethodPost, MethodPut, etc.)

Example:

openapi.WithUISupportedMethods(
    openapi.MethodGet,
    openapi.MethodPost,
    openapi.MethodPut,
    openapi.MethodDelete,
)

Validation Options

WithUIValidator

func WithUIValidator(url string) UIOption

Sets specification validator.

Parameters:

  • url - ValidatorLocal, ValidatorNone, or custom validator URL

Example:

openapi.WithUIValidator(openapi.ValidatorLocal)
openapi.WithUIValidator("https://validator.swagger.io/validator")
openapi.WithUIValidator(openapi.ValidatorNone)

Complete Example

openapi.WithSwaggerUI("/docs",
    // Display
    openapi.WithUIExpansion(openapi.DocExpansionList),
    openapi.WithUIModelExpandDepth(1),
    openapi.WithUIDisplayOperationID(true),
    
    // Try it out
    openapi.WithUITryItOut(true),
    openapi.WithUIRequestSnippets(true,
        openapi.SnippetCurlBash,
        openapi.SnippetCurlPowerShell,
        openapi.SnippetCurlCmd,
    ),
    openapi.WithUIDisplayRequestDuration(true),
    
    // Filtering/Sorting
    openapi.WithUIFilter(true),
    openapi.WithUIOperationsSorter(openapi.OperationsSorterAlpha),
    
    // Syntax
    openapi.WithUISyntaxHighlight(true),
    openapi.WithUISyntaxTheme(openapi.SyntaxThemeMonokai),
    
    // Auth
    openapi.WithUIPersistAuth(true),
    
    // Validation
    openapi.WithUIValidator(openapi.ValidatorLocal),
)

Next Steps

4.5 - Diagnostics

Warning system reference with codes and categories

Complete reference for the warning diagnostics system in rivaas.dev/openapi/diag.

Package Import

import "rivaas.dev/openapi/diag"

Warning Interface

type Warning interface {
    Code() WarningCode
    Message() string
    Path() string
    Category() WarningCategory
}

Individual warning with diagnostic information.

Methods:

  • Code() - Returns type-safe warning code
  • Message() - Returns human-readable message
  • Path() - Returns location in spec (e.g., “info.summary”)
  • Category() - Returns warning category

Warnings Collection

type Warnings []Warning

Collection of warnings with helper methods.

Has

func (w Warnings) Has(code WarningCode) bool

Checks if collection contains a specific warning code.

Example:

if result.Warnings.Has(diag.WarnDownlevelInfoSummary) {
    log.Warn("info.summary was dropped")
}

HasAny

func (w Warnings) HasAny(codes ...WarningCode) bool

Checks if collection contains any of the specified warning codes.

Example:

if result.Warnings.HasAny(
    diag.WarnDownlevelMutualTLS,
    diag.WarnDownlevelWebhooks,
) {
    log.Warn("Some 3.1 security features were dropped")
}

Filter

func (w Warnings) Filter(code WarningCode) Warnings

Returns warnings matching the specified code.

Example:

licenseWarnings := result.Warnings.Filter(diag.WarnDownlevelLicenseIdentifier)

FilterCategory

func (w Warnings) FilterCategory(category WarningCategory) Warnings

Returns warnings in the specified category.

Example:

downlevelWarnings := result.Warnings.FilterCategory(diag.CategoryDownlevel)

Exclude

func (w Warnings) Exclude(codes ...WarningCode) Warnings

Returns warnings excluding the specified codes.

Example:

expected := []diag.WarningCode{
    diag.WarnDownlevelInfoSummary,
}
unexpected := result.Warnings.Exclude(expected...)

Warning Codes

WarningCode Type

type WarningCode string

Type-safe warning code constant.

Downlevel Warning Codes

Warnings generated when using 3.1 features with a 3.0 target:

ConstantCode ValueDescription
WarnDownlevelWebhooksDOWNLEVEL_WEBHOOKSWebhooks dropped (3.0 doesn’t support them)
WarnDownlevelInfoSummaryDOWNLEVEL_INFO_SUMMARYinfo.summary dropped (3.0 doesn’t support it)
WarnDownlevelLicenseIdentifierDOWNLEVEL_LICENSE_IDENTIFIERlicense.identifier dropped
WarnDownlevelMutualTLSDOWNLEVEL_MUTUAL_TLSmutualTLS security scheme dropped
WarnDownlevelConstToEnumDOWNLEVEL_CONST_TO_ENUMJSON Schema const converted to enum
WarnDownlevelConstToEnumConflictDOWNLEVEL_CONST_TO_ENUM_CONFLICTconst conflicted with existing enum
WarnDownlevelPathItemsDOWNLEVEL_PATH_ITEMS$ref in pathItems was expanded
WarnDownlevelPatternPropertiesDOWNLEVEL_PATTERN_PROPERTIESpatternProperties dropped
WarnDownlevelUnevaluatedPropertiesDOWNLEVEL_UNEVALUATED_PROPERTIESunevaluatedProperties dropped
WarnDownlevelContentEncodingDOWNLEVEL_CONTENT_ENCODINGcontentEncoding dropped
WarnDownlevelContentMediaTypeDOWNLEVEL_CONTENT_MEDIA_TYPEcontentMediaType dropped
WarnDownlevelMultipleExamplesDOWNLEVEL_MULTIPLE_EXAMPLESMultiple examples collapsed to one
const (
    WarnDownlevelWebhooks              WarningCode = "DOWNLEVEL_WEBHOOKS"
    WarnDownlevelInfoSummary           WarningCode = "DOWNLEVEL_INFO_SUMMARY"
    WarnDownlevelLicenseIdentifier     WarningCode = "DOWNLEVEL_LICENSE_IDENTIFIER"
    WarnDownlevelMutualTLS             WarningCode = "DOWNLEVEL_MUTUAL_TLS"
    WarnDownlevelConstToEnum           WarningCode = "DOWNLEVEL_CONST_TO_ENUM"
    WarnDownlevelConstToEnumConflict   WarningCode = "DOWNLEVEL_CONST_TO_ENUM_CONFLICT"
    WarnDownlevelPathItems             WarningCode = "DOWNLEVEL_PATH_ITEMS"
    WarnDownlevelPatternProperties     WarningCode = "DOWNLEVEL_PATTERN_PROPERTIES"
    WarnDownlevelUnevaluatedProperties WarningCode = "DOWNLEVEL_UNEVALUATED_PROPERTIES"
    WarnDownlevelContentEncoding       WarningCode = "DOWNLEVEL_CONTENT_ENCODING"
    WarnDownlevelContentMediaType      WarningCode = "DOWNLEVEL_CONTENT_MEDIA_TYPE"
    WarnDownlevelMultipleExamples      WarningCode = "DOWNLEVEL_MULTIPLE_EXAMPLES"
)

Deprecation Warning Codes

Warnings for deprecated feature usage:

ConstantCode ValueDescription
WarnDeprecationExampleSingularDEPRECATION_EXAMPLE_SINGULARUsing deprecated singular example field
const (
    WarnDeprecationExampleSingular WarningCode = "DEPRECATION_EXAMPLE_SINGULAR"
)

Warning Categories

WarningCategory Type

type WarningCategory string

Category grouping for warnings.

Category Constants

CategoryDescription
CategoryDownlevel3.1 to 3.0 conversion feature losses (spec is still valid)
CategoryDeprecationDeprecated feature usage (feature still works but is discouraged)
CategoryUnknownUnrecognized warning codes
const (
    CategoryDownlevel   WarningCategory = "downlevel"
    CategoryDeprecation WarningCategory = "deprecation"
    CategoryUnknown     WarningCategory = "unknown"
)

Usage Examples

Check for Specific Warning

import "rivaas.dev/openapi/diag"

result, err := api.Generate(context.Background(), ops...)
if err != nil {
    log.Fatal(err)
}

if result.Warnings.Has(diag.WarnDownlevelInfoSummary) {
    log.Warn("info.summary was dropped (3.1 feature with 3.0 target)")
}

Filter by Category

downlevelWarnings := result.Warnings.FilterCategory(diag.CategoryDownlevel)
if len(downlevelWarnings) > 0 {
    fmt.Printf("Downlevel warnings: %d\n", len(downlevelWarnings))
    for _, warn := range downlevelWarnings {
        fmt.Printf("  [%s] %s at %s\n", 
            warn.Code(), 
            warn.Message(), 
            warn.Path(),
        )
    }
}

Check for Unexpected Warnings

expected := []diag.WarningCode{
    diag.WarnDownlevelInfoSummary,
    diag.WarnDownlevelLicenseIdentifier,
}

unexpected := result.Warnings.Exclude(expected...)
if len(unexpected) > 0 {
    log.Fatalf("Unexpected warnings: %d", len(unexpected))
}

Iterate All Warnings

for _, warn := range result.Warnings {
    fmt.Printf("[%s] %s\n", warn.Code(), warn.Message())
    fmt.Printf("  Location: %s\n", warn.Path())
    fmt.Printf("  Category: %s\n", warn.Category())
}

Complete Example

package main

import (
    "context"
    "fmt"
    "log"
    
    "rivaas.dev/openapi"
    "rivaas.dev/openapi/diag"
)

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("My API", "1.0.0"),
        openapi.WithVersion(openapi.V30x),
        openapi.WithInfoSummary("API Summary"), // 3.1 feature
    )
    
    result, err := api.Generate(context.Background(), operations...)
    if err != nil {
        log.Fatal(err)
    }
    
    // Handle specific warnings
    if result.Warnings.Has(diag.WarnDownlevelInfoSummary) {
        fmt.Println("Note: info.summary was dropped")
    }
    
    // Filter by category
    downlevelWarnings := result.Warnings.FilterCategory(diag.CategoryDownlevel)
    fmt.Printf("Downlevel warnings: %d\n", len(downlevelWarnings))
    
    // Check for unexpected
    expected := []diag.WarningCode{
        diag.WarnDownlevelInfoSummary,
    }
    unexpected := result.Warnings.Exclude(expected...)
    
    if len(unexpected) > 0 {
        fmt.Printf("UNEXPECTED warnings: %d\n", len(unexpected))
        for _, warn := range unexpected {
            fmt.Printf("  [%s] %s\n", warn.Code(), warn.Message())
        }
        log.Fatal("Unexpected warnings found")
    }
    
    fmt.Println("Generation complete")
}

Next Steps

4.6 - Troubleshooting

Common issues and solutions

Common issues and solutions for the openapi package.

Schema Name Collisions

Problem

Types with the same name in different packages may collide in the generated specification.

Solution

The package automatically uses pkgname.TypeName format for schema names:

// In package "api"
type User struct { ... }  // Becomes "api.User"

// In package "models"  
type User struct { ... }  // Becomes "models.User"

If you need custom schema names, use the openapi struct tag:

type User struct {
    ID   int    `json:"id" openapi:"name=CustomUser"`
    Name string `json:"name"`
}

Extension Validation

Problem

Custom extensions are rejected or filtered out.

Solution

Extensions must follow OpenAPI rules:

Valid:

openapi.WithExtension("x-custom", "value")
openapi.WithExtension("x-api-version", "v2")

Invalid:

// Missing x- prefix
openapi.WithExtension("custom", "value") // Error

// Reserved prefix in 3.1.x
openapi.WithExtension("x-oai-custom", "value") // Filtered out in 3.1.x
openapi.WithExtension("x-oas-custom", "value") // Filtered out in 3.1.x

Version Compatibility

Problem

Using OpenAPI 3.1 features with a 3.0 target generates warnings or errors.

Solution

When using OpenAPI 3.0.x target, some 3.1.x features are automatically down-leveled:

Feature3.0 Behavior
info.summaryDropped (warning)
license.identifierDropped (warning)
const in schemasConverted to enum with single value
examples (multiple)Converted to single example
webhooksDropped (warning)
mutualTLS securityDropped (warning)

Options:

  1. Accept warnings (default):
api := openapi.MustNew(
    openapi.WithVersion(openapi.V30x),
    openapi.WithInfoSummary("Summary"), // Generates warning
)

result, err := api.Generate(context.Background(), ops...)
// Check result.Warnings
  1. Enable strict mode (error on 3.1 features):
api := openapi.MustNew(
    openapi.WithVersion(openapi.V30x),
    openapi.WithStrictDownlevel(true), // Error on 3.1 features
    openapi.WithInfoSummary("Summary"), // Causes error
)
  1. Use 3.1 target:
api := openapi.MustNew(
    openapi.WithVersion(openapi.V31x), // All features available
    openapi.WithInfoSummary("Summary"), // No warning
)

Parameters Not Discovered

Problem

Parameters are not appearing in the generated specification.

Solution

Ensure struct tags are correct:

Common Issues:

// Wrong tag name
type Request struct {
    ID int `params:"id"` // Should be "path", "query", "header", or "cookie"
}

// Missing tag
type Request struct {
    ID int // No tag - won't be discovered
}

// Wrong location
type Request struct {
    ID int `query:"id"` // Should be "path" for path parameters
}

Correct:

type Request struct {
    // Path parameters
    ID int `path:"id" doc:"User ID"`
    
    // Query parameters
    Page int `query:"page" doc:"Page number"`
    
    // Header parameters
    Auth string `header:"Authorization" doc:"Auth token"`
    
    // Cookie parameters
    Session string `cookie:"session_id" doc:"Session ID"`
}

Validation Errors

Problem

Generated specification fails validation.

Solution

Enable validation to get detailed error messages:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithValidation(true), // Enable validation
)

result, err := api.Generate(context.Background(), ops...)
if err != nil {
    log.Printf("Validation failed: %v\n", err)
}

Common validation errors:

  1. Missing required fields:
// Missing version
openapi.MustNew(
    openapi.WithTitle("My API", ""), // Version required
)
  1. Invalid URLs:
// Invalid server URL
openapi.WithServer("not-a-url", "Server")
  1. Invalid enum values:
type Request struct {
    Status string `json:"status" enum:"active"` // Missing comma-separated values
}

Performance Issues

Problem

Specification generation is slow.

Solution

Typical performance:

  • First generation per type: ~500ns (reflection)
  • Subsequent generations: ~50ns (cached)
  • Validation overhead: 10-20ms first time, 1-5ms subsequent

Optimization tips:

  1. Disable validation in production:
api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithValidation(false), // Disable for production
)
  1. Generate once, cache result:
var cachedSpec []byte
var once sync.Once

func getSpec() []byte {
    once.Do(func() {
        result, _ := api.Generate(context.Background(), ops...)
        cachedSpec = result.JSON
    })
    return cachedSpec
}
  1. Pre-generate at build time:
# generate-spec.go
go run generate-spec.go > openapi.json

Schema Generation Issues

Problem

Go types are not converted correctly to OpenAPI schemas.

Solution

Supported types:

type Example struct {
    // Primitives
    String  string  `json:"string"`
    Int     int     `json:"int"`
    Bool    bool    `json:"bool"`
    Float   float64 `json:"float"`
    
    // Pointers (nullable)
    Optional *string `json:"optional,omitempty"`
    
    // Slices
    Tags []string `json:"tags"`
    
    // Maps
    Metadata map[string]string `json:"metadata"`
    
    // Nested structs
    Address Address `json:"address"`
    
    // Time
    CreatedAt time.Time `json:"created_at"`
}

Unsupported types:

type Unsupported struct {
    Channel chan int        // Not supported
    Func    func()          // Not supported
    Complex complex64       // Not supported
}

Workaround for unsupported types:

Use custom types or JSON marshaling:

type CustomType struct {
    data interface{}
}

func (c CustomType) MarshalJSON() ([]byte, error) {
    // Custom marshaling logic
}

Context Errors

Problem

Generate() returns “context is nil” error.

Solution

Always provide a valid context:

// Wrong
result, err := api.Generate(nil, ops...) // Error

// Correct
result, err := api.Generate(context.Background(), ops...)
result, err := api.Generate(ctx, ops...) // With existing context

Common FAQ

Q: How do I make a parameter optional?

A: For query/header/cookie parameters, omit validate:"required" tag. For request body fields, use pointer types or omitempty:

type Request struct {
    Required string  `json:"required" validate:"required"`
    Optional *string `json:"optional,omitempty"`
}

Q: How do I add multiple examples?

A: Pass multiple example instances to WithRequest() or WithResponse():

example1 := User{ID: 1, Name: "Alice"}
example2 := User{ID: 2, Name: "Bob"}

openapi.WithResponse(200, User{}, example1, example2)

Q: Can I generate specs for existing handlers?

A: Yes, define types that match your handlers and pass them to operations:

// Handler
func GetUser(id int) (*User, error) { ... }

// OpenAPI
openapi.GET("/users/:id",
    openapi.WithResponse(200, User{}),
)

Q: How do I document error responses?

A: Use multiple WithResponse() calls:

openapi.GET("/users/:id",
    openapi.WithResponse(200, User{}),
    openapi.WithResponse(400, ErrorResponse{}),
    openapi.WithResponse(404, ErrorResponse{}),
    openapi.WithResponse(500, ErrorResponse{}),
)

Q: Can I use this with existing OpenAPI specs?

A: Use the validate package to validate external specs:

import "rivaas.dev/openapi/validate"

validator := validate.New()
err := validator.ValidateAuto(context.Background(), specJSON)

Getting Help

If you encounter issues not covered here:

  1. Check the pkg.go.dev documentation
  2. Review examples
  3. Search GitHub issues
  4. Open a new issue with a minimal reproduction

Next Steps

5 - Metrics Package

API reference for rivaas.dev/metrics - Metrics collection for Go applications

This is the API reference for the rivaas.dev/metrics package. For learning-focused documentation, see the Metrics Guide.

Package Information

Package Overview

The metrics package provides OpenTelemetry-based metrics collection for Go applications with support for multiple exporters including Prometheus, OTLP, and stdout.

Core Features

  • Multiple metrics providers (Prometheus, OTLP, stdout)
  • Built-in HTTP metrics via middleware
  • Custom metrics (counters, histograms, gauges)
  • Thread-safe operations
  • Context-aware methods
  • Automatic header filtering for security
  • Testing utilities

Architecture

The package is built on OpenTelemetry and provides a simplified interface for common metrics use cases.

graph TD
    App[Application Code]
    Recorder[Recorder]
    Provider[Provider Layer]
    Prom[Prometheus]
    OTLP[OTLP]
    Stdout[Stdout]
    Middleware[HTTP Middleware]
    
    App -->|Record Metrics| Recorder
    Middleware -->|Auto-Collect| Recorder
    Recorder --> Provider
    Provider --> Prom
    Provider --> OTLP
    Provider --> Stdout

Components

Main Package (rivaas.dev/metrics)

Core metrics collection including:

  • Recorder - Main metrics recorder
  • New() / MustNew() - Recorder initialization
  • Custom metrics methods - Counters, histograms, gauges
  • Middleware() - HTTP metrics collection
  • Testing utilities

Quick API Index

Recorder Creation

recorder, err := metrics.New(options...)     // With error handling
recorder := metrics.MustNew(options...)      // Panics on error

Lifecycle Management

err := recorder.Start(ctx context.Context)   // Start metrics server/exporter
err := recorder.Shutdown(ctx context.Context) // Graceful shutdown
err := recorder.ForceFlush(ctx context.Context) // Force immediate export

Recording Metrics

// Counters
err := recorder.IncrementCounter(ctx, name, attributes...)
err := recorder.AddCounter(ctx, name, value, attributes...)

// Histograms
err := recorder.RecordHistogram(ctx, name, value, attributes...)

// Gauges
err := recorder.SetGauge(ctx, name, value, attributes...)

HTTP Middleware

handler := metrics.Middleware(recorder, options...)(httpHandler)

Provider-Specific Methods

address := recorder.ServerAddress()          // Prometheus: actual address
handler, err := recorder.Handler()           // Prometheus: metrics handler
count := recorder.CustomMetricCount()        // Number of custom metrics

Testing Utilities

recorder := metrics.TestingRecorder(t, serviceName)
recorder := metrics.TestingRecorderWithPrometheus(t, serviceName)
err := metrics.WaitForMetricsServer(t, address, timeout)

Reference Pages

API Reference

Recorder type, lifecycle methods, and custom metrics API.

View →

Options

Configuration options for providers and service metadata.

View →

Middleware Options

HTTP middleware configuration and path exclusion.

View →

Troubleshooting

Common metrics issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Type Reference

Recorder

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

Main metrics recorder. Thread-safe for concurrent access.

Methods: See API Reference for complete method documentation.

Option

type Option func(*Recorder)

Configuration option function type used with New() and MustNew().

Available Options: See Options for all options.

EventType

type EventType int

const (
    EventError   EventType = iota // Error events
    EventWarning                   // Warning events
    EventInfo                      // Informational events
    EventDebug                     // Debug events
)

Event severity levels for internal operational events.

Event

type Event struct {
    Type    EventType
    Message string
    Args    []any // slog-style key-value pairs
}

Internal operational event from the metrics package.

EventHandler

type EventHandler func(Event)

Processes internal operational events. Used with WithEventHandler option.

Common Patterns

Basic Usage

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
)
recorder.Start(context.Background())
defer recorder.Shutdown(context.Background())

_ = recorder.IncrementCounter(ctx, "requests_total")

With HTTP Middleware

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
)
recorder.Start(context.Background())

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health"),
)(httpHandler)

http.ListenAndServe(":8080", handler)

With OTLP

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithServiceName("my-service"),
)
recorder.Start(ctx) // Required before recording metrics
defer recorder.Shutdown(context.Background())

Thread Safety

The Recorder type is thread-safe for:

  • All metric recording methods
  • Concurrent Start() and Shutdown() operations
  • Mixed recording and lifecycle operations

Not thread-safe for:

  • Concurrent modification during initialization

Performance Notes

  • Metric recording: ~1-2 microseconds per operation
  • HTTP middleware: ~1-2 microseconds overhead per request
  • Memory usage: Scales with number of unique metric names and label combinations
  • Histogram overhead: Proportional to bucket count

Best Practices:

  • Use fire-and-forget pattern for most metrics (ignore errors)
  • Limit metric cardinality (avoid high-cardinality labels)
  • Customize histogram buckets for your use case
  • Exclude high-traffic paths from middleware when appropriate

Built-in Metrics

When using HTTP middleware, these metrics are automatically collected:

MetricTypeDescription
http_request_duration_secondsHistogramRequest duration distribution
http_requests_totalCounterTotal requests by method, path, status
http_requests_activeGaugeCurrently active requests
http_request_size_bytesHistogramRequest body size distribution
http_response_size_bytesHistogramResponse body size distribution
http_errors_totalCounterHTTP errors by status code
custom_metric_failures_totalCounterFailed custom metric creations
target_infoGaugeOpenTelemetry resource metadata (service name, version)

Version Compatibility

The metrics package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

OpenTelemetry compatibility: Uses OpenTelemetry SDK v1.x

Next Steps

For learning-focused guides, see the Metrics Guide.

5.1 - API Reference

Complete API documentation for the Recorder type and methods

Complete API reference for the metrics package core types and methods.

Recorder Type

The Recorder is the main type for collecting metrics. It is thread-safe. You can use it concurrently.

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

Creation Functions

New

func New(opts ...Option) (*Recorder, error)

Creates a new Recorder with the given options. Returns an error if configuration is invalid.

Parameters:

  • opts ...Option - Configuration options.

Returns:

  • *Recorder - Configured recorder.
  • error - Configuration error, if any.

Example:

recorder, err := metrics.New(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
)
if err != nil {
    log.Fatal(err)
}

Errors:

  • Multiple provider options specified.
  • Invalid service name.
  • Invalid port or endpoint configuration.

MustNew

func MustNew(opts ...Option) *Recorder

Creates a new Recorder with the given options. Panics if configuration is invalid.

Parameters:

  • opts ...Option - Configuration options.

Returns:

  • *Recorder - Configured recorder.

Panics: If configuration is invalid.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
)

Use Case: Applications that should fail fast on invalid metrics configuration.

Lifecycle Methods

Start

func (r *Recorder) Start(ctx context.Context) error

Starts the metrics recorder. For Prometheus, starts the HTTP server. For OTLP, establishes connection. For stdout, this is a no-op but safe to call.

Parameters:

  • ctx context.Context - Lifecycle context for the recorder

Returns:

  • error - Startup error, if any

Example:

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

if err := recorder.Start(ctx); err != nil {
    log.Fatal(err)
}

Errors:

  • Port already in use (Prometheus with WithStrictPort)
  • Cannot connect to OTLP endpoint
  • Context already canceled

Provider Behavior:

  • Prometheus: Starts HTTP server on configured port
  • OTLP: Establishes connection to collector
  • Stdout: No-op, safe to call

Shutdown

func (r *Recorder) Shutdown(ctx context.Context) error

Gracefully shuts down the metrics recorder, flushing any pending metrics.

Parameters:

  • ctx context.Context - Shutdown context with timeout

Returns:

  • error - Shutdown error, if any

Example:

shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := recorder.Shutdown(shutdownCtx); err != nil {
    log.Printf("Shutdown error: %v", err)
}

Behavior:

  • Stops accepting new metrics
  • Flushes pending metrics
  • Closes network connections
  • Stops HTTP server (Prometheus)
  • Idempotent (safe to call multiple times)

Best Practice: Always defer Shutdown with a timeout context.

ForceFlush

func (r *Recorder) ForceFlush(ctx context.Context) error

Forces immediate export of all pending metrics. Primarily useful for push-based providers (OTLP, stdout).

Parameters:

  • ctx context.Context - Flush context with timeout

Returns:

  • error - Flush error, if any

Example:

// Before critical operation
if err := recorder.ForceFlush(ctx); err != nil {
    log.Printf("Failed to flush metrics: %v", err)
}

Provider Behavior:

  • OTLP: Immediately exports all pending metrics
  • Stdout: Immediately prints all pending metrics
  • Prometheus: Typically a no-op (pull-based)

Use Cases:

  • Before deployment or shutdown
  • Checkpointing during long operations
  • Ensuring metrics visibility

Custom Metrics Methods

IncrementCounter

func (r *Recorder) IncrementCounter(ctx context.Context, name string, attrs ...attribute.KeyValue) error

Increments a counter metric by 1.

Parameters:

  • ctx context.Context - Context for the operation
  • name string - Metric name (must be valid)
  • attrs ...attribute.KeyValue - Optional metric attributes

Returns:

  • error - Error if metric name is invalid or limit reached

Example:

err := recorder.IncrementCounter(ctx, "requests_total",
    attribute.String("method", "GET"),
    attribute.String("status", "success"),
)

Naming Rules:

  • Must start with letter
  • Can contain letters, numbers, underscores, dots, hyphens
  • Cannot use reserved prefixes: __, http_, router_
  • Maximum 255 characters

AddCounter

func (r *Recorder) AddCounter(ctx context.Context, name string, value int64, attrs ...attribute.KeyValue) error

Adds a specific value to a counter metric.

Parameters:

  • ctx context.Context - Context for the operation
  • name string - Metric name (must be valid)
  • value int64 - Amount to add (must be non-negative)
  • attrs ...attribute.KeyValue - Optional metric attributes

Returns:

  • error - Error if metric name is invalid, value is negative, or limit reached

Example:

bytesProcessed := int64(1024)
err := recorder.AddCounter(ctx, "bytes_processed_total", bytesProcessed,
    attribute.String("direction", "inbound"),
)

RecordHistogram

func (r *Recorder) RecordHistogram(ctx context.Context, name string, value float64, attrs ...attribute.KeyValue) error

Records a value in a histogram metric.

Parameters:

  • ctx context.Context - Context for the operation
  • name string - Metric name (must be valid)
  • value float64 - Value to record
  • attrs ...attribute.KeyValue - Optional metric attributes

Returns:

  • error - Error if metric name is invalid or limit reached

Example:

start := time.Now()
// ... operation ...
duration := time.Since(start).Seconds()

err := recorder.RecordHistogram(ctx, "operation_duration_seconds", duration,
    attribute.String("operation", "create_user"),
)

Bucket Configuration: Use WithDurationBuckets or WithSizeBuckets to customize histogram boundaries.

SetGauge

func (r *Recorder) SetGauge(ctx context.Context, name string, value float64, attrs ...attribute.KeyValue) error

Sets a gauge metric to a specific value.

Parameters:

  • ctx context.Context - Context for the operation
  • name string - Metric name (must be valid)
  • value float64 - Value to set
  • attrs ...attribute.KeyValue - Optional metric attributes

Returns:

  • error - Error if metric name is invalid or limit reached

Example:

activeConnections := float64(pool.Active())
err := recorder.SetGauge(ctx, "active_connections", activeConnections,
    attribute.String("pool", "database"),
)

Provider-Specific Methods

ServerAddress

func (r *Recorder) ServerAddress() string

Returns the server address (port) for Prometheus provider. Returns empty string for other providers or if server is disabled.

Returns:

  • string - Server address in port format (e.g., :9090)

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
)
recorder.Start(ctx)

address := recorder.ServerAddress()
log.Printf("Metrics at: http://localhost%s/metrics", address)

Use Cases:

  • Logging actual port (when not using strict mode)
  • Testing with dynamic port allocation
  • Health check registration

Note: Returns the port string (e.g., :9090), not a full hostname. Prepend localhost for local access.

Handler

func (r *Recorder) Handler() (http.Handler, error)

Returns the HTTP handler for metrics endpoint. Only works with Prometheus provider.

Returns:

  • http.Handler - Metrics endpoint handler
  • error - Error if not using Prometheus provider or server disabled

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServerDisabled(),
    metrics.WithServiceName("my-api"),
)

handler, err := recorder.Handler()
if err != nil {
    log.Fatal(err)
}

http.Handle("/metrics", handler)
http.ListenAndServe(":8080", nil)

Errors:

  • Not using Prometheus provider
  • Server not disabled (use WithServerDisabled)

CustomMetricCount

func (r *Recorder) CustomMetricCount() int

Returns the number of custom metrics created.

Returns:

  • int - Number of custom metrics

Example:

count := recorder.CustomMetricCount()
log.Printf("Custom metrics: %d/%d", count, maxLimit)

// Expose as a metric
_ = recorder.SetGauge(ctx, "custom_metrics_count", float64(count))

Use Cases:

  • Monitoring metric cardinality
  • Debugging metric limit issues
  • Capacity planning

Note: Built-in HTTP metrics do not count toward this total.

Middleware Function

Middleware

func Middleware(recorder *Recorder, opts ...MiddlewareOption) func(http.Handler) http.Handler

Returns HTTP middleware that automatically collects metrics for requests.

Parameters:

  • recorder *Recorder - Metrics recorder
  • opts ...MiddlewareOption - Middleware configuration options

Returns:

  • func(http.Handler) http.Handler - Middleware function

Example:

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health", "/metrics"),
    metrics.WithHeaders("X-Request-ID"),
)(httpHandler)

Collected Metrics:

  • http_request_duration_seconds - Request duration histogram
  • http_requests_total - Request counter
  • http_requests_active - Active requests gauge
  • http_request_size_bytes - Request size histogram
  • http_response_size_bytes - Response size histogram
  • http_errors_total - Error counter

Middleware Options: See Middleware Options for details.

Testing Functions

TestingRecorder

func TestingRecorder(tb testing.TB, serviceName string, opts ...Option) *Recorder

Creates a test recorder with stdout provider. Automatically registers cleanup via t.Cleanup().

Parameters:

  • tb testing.TB - Test or benchmark instance
  • serviceName string - Service name for metrics
  • opts ...Option - Optional additional configuration options

Returns:

  • *Recorder - Test recorder

Example:

func TestHandler(t *testing.T) {
    t.Parallel()
    
    recorder := metrics.TestingRecorder(t, "test-service")
    
    // Use recorder in tests...
    // Cleanup is automatic
}

// With additional options
func TestWithOptions(t *testing.T) {
    recorder := metrics.TestingRecorder(t, "test-service",
        metrics.WithMaxCustomMetrics(100),
    )
}

Features:

  • No port conflicts (uses stdout)
  • Automatic cleanup
  • Parallel test safe
  • Works with both *testing.T and *testing.B

TestingRecorderWithPrometheus

func TestingRecorderWithPrometheus(tb testing.TB, serviceName string, opts ...Option) *Recorder

Creates a test recorder with Prometheus provider and dynamic port allocation. Automatically registers cleanup via t.Cleanup().

Parameters:

  • tb testing.TB - Test or benchmark instance
  • serviceName string - Service name for metrics
  • opts ...Option - Optional additional configuration options

Returns:

  • *Recorder - Test recorder with Prometheus

Example:

func TestMetricsEndpoint(t *testing.T) {
    t.Parallel()
    
    recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
    
    // Wait for server
    err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
    if err != nil {
        t.Fatal(err)
    }
    
    // Test metrics endpoint...
}

Features:

  • Dynamic port allocation
  • Real Prometheus endpoint
  • Automatic cleanup
  • Works with both *testing.T and *testing.B

WaitForMetricsServer

func WaitForMetricsServer(tb testing.TB, address string, timeout time.Duration) error

Waits for Prometheus metrics server to be ready.

Parameters:

  • tb testing.TB - Test or benchmark instance for logging
  • address string - Server address (e.g., :9090)
  • timeout time.Duration - Maximum wait time

Returns:

  • error - Error if server not ready within timeout

Example:

recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")

err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
    t.Fatalf("Server not ready: %v", err)
}

// Server is ready, make requests

Event Types

EventType

type EventType int

const (
    EventError   EventType = iota // Error events
    EventWarning                   // Warning events
    EventInfo                      // Informational events
    EventDebug                     // Debug events
)

Severity levels for internal operational events.

Event

type Event struct {
    Type    EventType
    Message string
    Args    []any // slog-style key-value pairs
}

Internal operational event from the metrics package.

Example:

metrics.WithEventHandler(func(e metrics.Event) {
    switch e.Type {
    case metrics.EventError:
        sentry.CaptureMessage(e.Message)
    case metrics.EventWarning:
        log.Printf("WARN: %s", e.Message)
    case metrics.EventInfo:
        log.Printf("INFO: %s", e.Message)
    }
})

EventHandler

type EventHandler func(Event)

Function type for handling internal operational events.

Example:

handler := func(e metrics.Event) {
    slog.Default().Info(e.Message, e.Args...)
}

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithEventHandler(handler),
)

Error Handling

All metric recording methods return error. Common error types:

Invalid Metric Name

err := recorder.IncrementCounter(ctx, "__reserved")
// Error: metric name uses reserved prefix "__"

Metric Limit Reached

err := recorder.IncrementCounter(ctx, "new_metric_1001")
// Error: custom metric limit reached (1000/1000)

Provider Not Started

recorder := metrics.MustNew(metrics.WithOTLP("http://localhost:4318"))
err := recorder.IncrementCounter(ctx, "metric")
// Error: OTLP provider not started (call Start first)

Thread Safety

All methods are thread-safe and can be called concurrently:

// Safe to call from multiple goroutines
go func() {
    _ = recorder.IncrementCounter(ctx, "worker_1")
}()

go func() {
    _ = recorder.IncrementCounter(ctx, "worker_2")
}()

Next Steps

5.2 - Configuration Options

Complete reference of all configuration options

Complete reference for all Option functions used to configure the Recorder.

Provider Options

Only one provider option can be used per Recorder. Using multiple provider options results in a validation error.

WithPrometheus

func WithPrometheus(port, path string) Option

Configures Prometheus provider with HTTP endpoint.

Parameters:

  • port string - Listen address like :9090 or localhost:9090.
  • path string - Metrics path like /metrics.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
)

Behavior:

  • Initializes immediately in New().
  • Starts HTTP server when Start() is called.
  • Metrics available at http://localhost:9090/metrics.

Related Options:

  • WithStrictPort() - Fail if port unavailable.
  • WithServerDisabled() - Manage HTTP server manually.

WithOTLP

func WithOTLP(endpoint string) Option

Configures OTLP (OpenTelemetry Protocol) provider for sending metrics to a collector.

Parameters:

  • endpoint string - OTLP collector HTTP endpoint like http://localhost:4318.

Example:

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithServiceName("my-service"),
)

Behavior:

  • Defers initialization until Start() is called.
  • Uses lifecycle context for network connections.
  • Important: Must call Start() before recording metrics.

Related Options:

  • WithExportInterval() - Configure export frequency.

WithStdout

func WithStdout() Option

Configures stdout provider for printing metrics to console.

Example:

recorder := metrics.MustNew(
    metrics.WithStdout(),
    metrics.WithServiceName("dev-service"),
)

Behavior:

  • Initializes immediately in New()
  • Works without calling Start() (but safe to call)
  • Prints metrics to stdout periodically

Use Cases:

  • Development and debugging
  • CI/CD pipelines
  • Unit tests

Related Options:

  • WithExportInterval() - Configure print frequency

Service Configuration Options

WithServiceName

func WithServiceName(name string) Option

Sets the service name for metrics identification.

Parameters:

  • name string - Service name

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("payment-api"),
)

Where It Appears:

The service name appears in the target_info metric, which holds resource-level information about your service:

target_info{service_name="payment-api",service_version="1.0.0"} 1

Individual metrics like http_requests_total do not include service_name as a label. This keeps label cardinality low, following Prometheus best practices.

Best Practices:

  • Use lowercase with hyphens: user-service, payment-api
  • Be consistent across services
  • Avoid changing names in production

WithServiceVersion

func WithServiceVersion(version string) Option

Sets the service version for metrics.

Parameters:

  • version string - Service version

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-api"),
    metrics.WithServiceVersion("v1.2.3"),
)

Best Practices:

  • Use semantic versioning: v1.2.3
  • Automate from CI/CD build information

Prometheus-Specific Options

WithStrictPort

func WithStrictPort() Option

Requires the metrics server to use the exact port specified. Fails if port is unavailable.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithStrictPort(),  // Fail if 9090 unavailable
    metrics.WithServiceName("my-api"),
)

Default Behavior: Automatically searches up to 100 ports if requested port is unavailable.

With Strict Mode: Returns error if exact port is not available.

Production Recommendation: Always use WithStrictPort() for predictable behavior.

WithServerDisabled

func WithServerDisabled() Option

Disables automatic metrics server startup. Use Handler() to get metrics handler for manual serving.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServerDisabled(),
    metrics.WithServiceName("my-api"),
)

handler, err := recorder.Handler()
if err != nil {
    log.Fatal(err)
}

// Serve on your own server
http.Handle("/metrics", handler)
http.ListenAndServe(":8080", nil)

Use Cases:

  • Serve metrics on same port as application
  • Custom server configuration
  • Integration with existing HTTP servers

WithoutScopeInfo

func WithoutScopeInfo() Option

Removes OpenTelemetry instrumentation scope labels from Prometheus metrics output.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithoutScopeInfo(),
    metrics.WithServiceName("my-api"),
)

What It Does:

By default, OpenTelemetry adds labels like otel_scope_name, otel_scope_version, and otel_scope_schema_url to every metric point. These labels identify which instrumentation library produced each metric.

When to Use:

  • You only have one instrumentation scope (common case)
  • You want to reduce label cardinality
  • The scope information is not useful for your use case

Only Affects: Prometheus provider (OTLP and stdout ignore this option)

Default Behavior: Scope labels are included on all metrics

WithoutTargetInfo

func WithoutTargetInfo() Option

Disables the target_info metric in Prometheus output.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithoutTargetInfo(),
    metrics.WithServiceName("my-api"),
)

What It Does:

By default, OpenTelemetry creates a target_info metric containing resource attributes like service_name and service_version. This metric helps identify and correlate metrics across your infrastructure.

When to Use:

  • You manage service identification through Prometheus external labels
  • You have your own service discovery mechanism
  • You don’t need the resource-level metadata

Only Affects: Prometheus provider (OTLP and stdout ignore this option)

Default Behavior: The target_info metric is created with service metadata

Histogram Bucket Options

WithDurationBuckets

func WithDurationBuckets(buckets ...float64) Option

Sets custom histogram bucket boundaries for duration metrics (in seconds).

Parameters:

  • buckets ...float64 - Bucket boundaries in seconds

Default: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10

Example:

// Fast API (most requests < 100ms)
recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithDurationBuckets(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 1),
    metrics.WithServiceName("fast-api"),
)

// Slow operations (seconds to minutes)
recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithDurationBuckets(1, 5, 10, 30, 60, 120, 300, 600),
    metrics.WithServiceName("batch-processor"),
)

Trade-offs:

  • More buckets = better resolution, higher memory/storage
  • Fewer buckets = lower overhead, coarser resolution

WithSizeBuckets

func WithSizeBuckets(buckets ...float64) Option

Sets custom histogram bucket boundaries for size metrics (in bytes).

Parameters:

  • buckets ...float64 - Bucket boundaries in bytes

Default: 100, 1000, 10000, 100000, 1000000, 10000000

Example:

// Small JSON API (< 10KB)
recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithSizeBuckets(100, 500, 1000, 5000, 10000, 50000),
    metrics.WithServiceName("json-api"),
)

// File uploads (KB to MB)
recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithSizeBuckets(1024, 10240, 102400, 1048576, 10485760, 104857600),
    metrics.WithServiceName("file-service"),
)

Advanced Options

WithExportInterval

func WithExportInterval(interval time.Duration) Option

Sets export interval for push-based providers (OTLP and stdout).

Parameters:

  • interval time.Duration - Export interval

Default: 30 seconds

Example:

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithExportInterval(10 * time.Second),
    metrics.WithServiceName("my-service"),
)

Applies To:

  • OTLP (push-based)
  • Stdout (push-based)

Does NOT Apply To:

  • Prometheus (pull-based, scraped on-demand)

Trade-offs:

  • Shorter interval: More timely data, higher overhead
  • Longer interval: Lower overhead, delayed visibility

WithMaxCustomMetrics

func WithMaxCustomMetrics(maxLimit int) Option

Sets the maximum number of custom metrics allowed.

Parameters:

  • maxLimit int - Maximum custom metrics

Default: 1000

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithMaxCustomMetrics(5000),
    metrics.WithServiceName("my-api"),
)

Purpose:

  • Prevent unbounded metric cardinality
  • Protect against memory exhaustion
  • Enforce metric discipline

Note: Built-in HTTP metrics do not count toward this limit.

Monitor Usage:

count := recorder.CustomMetricCount()
log.Printf("Custom metrics: %d/%d", count, maxLimit)

WithLogger

func WithLogger(logger *slog.Logger) Option

Sets the logger for internal operational events.

Parameters:

  • logger *slog.Logger - Logger instance

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithLogger(slog.Default()),
    metrics.WithServiceName("my-api"),
)

Events Logged:

  • Initialization events
  • Error messages (metric creation failures)
  • Warning messages (port conflicts, limits reached)

Alternative: Use WithEventHandler() for custom event handling.

WithEventHandler

func WithEventHandler(handler EventHandler) Option

Sets a custom event handler for internal operational events.

Parameters:

  • handler EventHandler - Event handler function

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithEventHandler(func(e metrics.Event) {
        switch e.Type {
        case metrics.EventError:
            sentry.CaptureMessage(e.Message)
        case metrics.EventWarning:
            log.Printf("WARN: %s", e.Message)
        case metrics.EventInfo:
            log.Printf("INFO: %s", e.Message)
        }
    }),
    metrics.WithServiceName("my-api"),
)

Use Cases:

  • Send errors to external monitoring (Sentry, etc.)
  • Custom logging formats
  • Metric collection about metric collection

Event Types:

  • EventError - Error events
  • EventWarning - Warning events
  • EventInfo - Informational events
  • EventDebug - Debug events

Advanced Provider Options

WithMeterProvider

func WithMeterProvider(provider metric.MeterProvider) Option

Provides a custom OpenTelemetry meter provider for complete control.

Parameters:

  • provider metric.MeterProvider - Custom meter provider

Example:

mp := sdkmetric.NewMeterProvider(...)
recorder := metrics.MustNew(
    metrics.WithMeterProvider(mp),
    metrics.WithServiceName("my-service"),
)
defer mp.Shutdown(context.Background())

Use Cases:

  • Manage meter provider lifecycle yourself
  • Multiple independent metrics configurations
  • Avoid global state

Note: When using WithMeterProvider, provider options (WithPrometheus, WithOTLP, WithStdout) are ignored.

WithGlobalMeterProvider

func WithGlobalMeterProvider() Option

Registers the meter provider as the global OpenTelemetry meter provider.

Example:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithGlobalMeterProvider(),  // Register globally
    metrics.WithServiceName("my-service"),
)

Default Behavior: Meter providers are NOT registered globally.

When to Use:

  • OpenTelemetry instrumentation libraries need global provider
  • Third-party libraries expect global meter provider
  • otel.GetMeterProvider() should return your provider

When NOT to Use:

  • Multiple services in same process
  • Avoid global state
  • Custom meter provider management

Configuration Examples

Production API

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithStrictPort(),
    metrics.WithServiceName("payment-api"),
    metrics.WithServiceVersion(version),
    metrics.WithLogger(slog.Default()),
    metrics.WithDurationBuckets(0.01, 0.1, 0.5, 1, 5, 10),
    metrics.WithMaxCustomMetrics(2000),
)

Development

recorder := metrics.MustNew(
    metrics.WithStdout(),
    metrics.WithServiceName("dev-api"),
    metrics.WithExportInterval(5 * time.Second),
)

OpenTelemetry Native

recorder := metrics.MustNew(
    metrics.WithOTLP(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")),
    metrics.WithServiceName(os.Getenv("SERVICE_NAME")),
    metrics.WithServiceVersion(os.Getenv("SERVICE_VERSION")),
    metrics.WithExportInterval(15 * time.Second),
    metrics.WithLogger(slog.Default()),
)

Embedded Metrics Server

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServerDisabled(),
    metrics.WithServiceName("api"),
)

handler, _ := recorder.Handler()

// Serve on application port
mux := http.NewServeMux()
mux.Handle("/metrics", handler)
mux.HandleFunc("/", appHandler)
http.ListenAndServe(":8080", mux)

Option Validation

The following validation occurs during New() or MustNew():

  • Provider Conflicts: Only one provider option (WithPrometheus, WithOTLP, WithStdout) can be used
  • Service Name: Cannot be empty (default: "rivaas-service")
  • Service Version: Cannot be empty (default: "1.0.0")
  • Port Format: Must be valid address format for Prometheus
  • Custom Metrics Limit: Must be at least 1

Defaults: If no provider is specified, defaults to Prometheus on :9090/metrics.

Validation Errors:

// Multiple providers - ERROR
recorder, err := metrics.New(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithOTLP("http://localhost:4318"),  // Error: conflicting providers
)

// Empty service name - ERROR
recorder, err := metrics.New(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName(""),  // Error: service name cannot be empty
)

// No options - OK (uses defaults)
recorder, err := metrics.New()  // Uses default Prometheus on :9090/metrics

Next Steps

5.3 - Middleware Options

HTTP middleware configuration options reference

Complete reference for MiddlewareOption functions used to configure the HTTP metrics middleware.

Overview

Middleware options configure which paths to exclude from metrics collection and which headers to record.

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health", "/metrics"),
    metrics.WithExcludePrefixes("/debug/"),
    metrics.WithExcludePatterns(`^/admin/.*`),
    metrics.WithHeaders("X-Request-ID"),
)(httpHandler)

Path Exclusion Options

WithExcludePaths

func WithExcludePaths(paths ...string) MiddlewareOption

Excludes exact paths from metrics collection.

Parameters:

  • paths ...string - Exact paths to exclude

Example:

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health", "/metrics", "/ready"),
)(mux)

Use Cases:

  • Health check endpoints.
  • Metrics endpoints.
  • Readiness and liveness probes.

Behavior:

  • Matches exact path only.
  • Case-sensitive.
  • Does not match path prefixes.

Examples:

// Excluded paths
/health           excluded
/metrics          excluded
/ready            excluded

// Not excluded (not exact matches)
/health/status    not excluded
/livez            not excluded
/api/metrics      not excluded

WithExcludePrefixes

func WithExcludePrefixes(prefixes ...string) MiddlewareOption

Excludes all paths with specific prefixes from metrics collection.

Parameters:

  • prefixes ...string - Path prefixes to exclude

Example:

handler := metrics.Middleware(recorder,
    metrics.WithExcludePrefixes("/debug/", "/internal/", "/_/"),
)(mux)

Use Cases:

  • Debug endpoints (/debug/pprof/, /debug/vars/)
  • Internal APIs (/internal/)
  • Administrative paths (/_/)

Behavior:

  • Matches any path starting with prefix
  • Case-sensitive
  • Include trailing slash for directory prefixes

Examples:

// With prefix "/debug/"
/debug/pprof/heap       excluded
/debug/vars             excluded
/debug/                 excluded

// Not excluded
/debuginfo              not excluded (no slash)
/api/debug              not excluded (doesn't start with prefix)

WithExcludePatterns

func WithExcludePatterns(patterns ...string) MiddlewareOption

Excludes paths matching regex patterns from metrics collection.

Parameters:

  • patterns ...string - Regular expression patterns

Example:

handler := metrics.Middleware(recorder,
    metrics.WithExcludePatterns(
        `^/v[0-9]+/internal/.*`,  // /v1/internal/*, /v2/internal/*
        `^/api/[0-9]+$`,           // /api/123, /api/456
        `^/admin/.*`,              // /admin/*
    ),
)(mux)

Use Cases:

  • Version-specific internal paths
  • High-cardinality routes (IDs in path)
  • Pattern-based exclusions

Behavior:

  • Uses Go’s regexp package
  • Matches full path
  • Case-sensitive (use (?i) for case-insensitive)

Examples:

// Pattern: `^/v[0-9]+/internal/.*`
/v1/internal/metrics    excluded
/v2/internal/debug      excluded

// Not excluded
/internal/api           not excluded (no version)
/api/v1/internal        not excluded (doesn't start with /v)

// Pattern: `^/api/[0-9]+$`
/api/123                excluded
/api/456                excluded

// Not excluded
/api/users              not excluded (not numeric)
/api/123/details        not excluded (has suffix)

Pattern Tips:

// Anchors
^      // Start of path
$      // End of path

// Character classes
[0-9]  // Any digit
[a-z]  // Any lowercase letter
.      // Any character
\d     // Any digit

// Quantifiers
*      // Zero or more
+      // One or more
?      // Zero or one
{n}    // Exactly n

// Grouping
(...)  // Group

// Case-insensitive
(?i)pattern  // Case-insensitive match

Combining Exclusions

Use multiple exclusion options together:

handler := metrics.Middleware(recorder,
    // Exact paths
    metrics.WithExcludePaths("/health", "/metrics", "/ready"),
    
    // Prefixes
    metrics.WithExcludePrefixes("/debug/", "/internal/", "/_/"),
    
    // Patterns
    metrics.WithExcludePatterns(
        `^/v[0-9]+/internal/.*`,
        `^/api/users/[0-9]+$`,  // User IDs in path
    ),
)(mux)

Evaluation Order:

  1. Exact paths (WithExcludePaths)
  2. Prefixes (WithExcludePrefixes)
  3. Patterns (WithExcludePatterns)

If any exclusion matches, the path is excluded.

Header Recording Options

WithHeaders

func WithHeaders(headers ...string) MiddlewareOption

Records specific HTTP headers as metric attributes.

Parameters:

  • headers ...string - Header names to record

Example:

handler := metrics.Middleware(recorder,
    metrics.WithHeaders("X-Request-ID", "X-Correlation-ID", "X-Client-Version"),
)(mux)

Behavior:

  • Headers recorded as metric attributes
  • Header names normalized (lowercase, hyphens to underscores)
  • Sensitive headers automatically filtered

Header Normalization:

// Original header → Metric attribute
X-Request-ID        x_request_id
X-Correlation-ID    x_correlation_id
Content-Type        content_type
User-Agent          user_agent

Example Metric:

http_requests_total{
    method="GET",
    path="/api/users",
    status="200",
    x_request_id="abc123",
    x_correlation_id="def456"
} 1

Sensitive Header Filtering

The middleware automatically filters sensitive headers, even if explicitly requested.

Always Filtered Headers:

  • Authorization
  • Cookie
  • Set-Cookie
  • X-API-Key
  • X-Auth-Token
  • Proxy-Authorization
  • WWW-Authenticate

Example:

// Only X-Request-ID will be recorded
// Authorization and Cookie are automatically filtered
handler := metrics.Middleware(recorder,
    metrics.WithHeaders(
        "Authorization",      // ✗ Filtered (sensitive)
        "X-Request-ID",       // ✓ Recorded
        "Cookie",             // ✗ Filtered (sensitive)
        "X-Correlation-ID",   // ✓ Recorded
    ),
)(mux)

Why Filter?

  • Prevent credential leaks in metrics
  • Avoid exposing API keys
  • Comply with security policies
  • Prevent compliance violations

Configuration Examples

Basic Health Check Exclusion

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health", "/ready"),
)(mux)

Development/Debug Exclusion

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health", "/metrics"),
    metrics.WithExcludePrefixes("/debug/", "/_/"),
)(mux)

High-Cardinality Path Exclusion

handler := metrics.Middleware(recorder,
    // Exclude paths with IDs to avoid high cardinality
    metrics.WithExcludePatterns(
        `^/api/users/[0-9]+$`,         // /api/users/123
        `^/api/orders/[a-z0-9-]+$`,    // /api/orders/abc-123
        `^/files/[^/]+$`,              // /files/{id}
    ),
)(mux)

Request Tracing

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health"),
    metrics.WithHeaders("X-Request-ID", "X-Correlation-ID", "X-Trace-ID"),
)(mux)

Production Configuration

handler := metrics.Middleware(recorder,
    // Exclude operational endpoints
    metrics.WithExcludePaths(
        "/health",
        "/ready",
        "/metrics",
        "/favicon.ico",
    ),
    
    // Exclude administrative paths
    metrics.WithExcludePrefixes(
        "/debug/",
        "/internal/",
        "/_/",
    ),
    
    // Exclude high-cardinality routes
    metrics.WithExcludePatterns(
        `^/api/v[0-9]+/internal/.*`,
        `^/api/users/[0-9]+$`,
        `^/api/orders/[a-z0-9-]+$`,
    ),
    
    // Record tracing headers
    metrics.WithHeaders(
        "X-Request-ID",
        "X-Correlation-ID",
        "X-Client-Version",
    ),
)(mux)

Best Practices

Path Exclusions

DO:

  • Exclude health and readiness checks
  • Exclude metrics endpoints
  • Exclude high-cardinality paths (IDs)
  • Exclude debug and administrative paths

DON’T:

  • Over-exclude (you need some metrics!)
  • Exclude business-critical endpoints
  • Use overly broad patterns

Header Recording

DO:

  • Record low-cardinality headers only
  • Use headers for request tracing
  • Consider privacy implications

DON’T:

  • Record sensitive headers (automatically filtered)
  • Record high-cardinality headers (user IDs, timestamps)
  • Record excessive headers (increases metric cardinality)

Cardinality Management

High cardinality leads to:

  • Excessive memory usage
  • Slow query performance
  • Storage bloat

Low Cardinality (Good):

// Headers with limited values
X-Client-Version: v1.0, v1.1, v2.0  (3 values)
X-Region: us-east-1, eu-west-1      (2 values)

High Cardinality (Bad):

// Headers with unbounded values
X-Request-ID: abc123, def456, ...   (millions of values)
X-Timestamp: 2025-01-18T10:30:00Z   (always unique)
X-User-ID: user123, user456, ...    (millions of values)

Performance Considerations

Path Evaluation Overhead

  • Exact paths: O(1) hash lookup
  • Prefixes: O(n) prefix checks (n = number of prefixes)
  • Patterns: O(n) regex matches (n = number of patterns)

Recommendation: Use exact paths when possible for best performance.

Header Recording Impact

Each header adds:

  • Additional metric attribute
  • Increased metric cardinality
  • Higher memory usage

Recommendation: Only record necessary headers.

Troubleshooting

Path Not Excluded

Check:

  1. Path is exact match (use WithExcludePaths)
  2. Prefix includes trailing slash
  3. Pattern uses correct regex syntax
  4. Pattern is anchored (^ and $)

Header Not Recorded

Check:

  1. Header name is correct (case-insensitive)
  2. Header is not in sensitive list
  3. Header is present in request

High Memory Usage

Check:

  1. Too many unique paths (exclude high-cardinality routes)
  2. Too many header combinations
  3. Recording high-cardinality headers

Next Steps

5.4 - Troubleshooting

Common issues and solutions for the metrics package

Solutions to common issues when using the metrics package.

Service Name Not Correct

Symptoms

  • service_name="rivaas-service" instead of your configured name
  • service_name="unknown_service:main" in target_info metric
  • Wrong service name in dashboards

Solutions

1. Use WithServiceName Option

Always specify your service name when creating the recorder:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-actual-service"),  // Set your service name
)

2. Check target_info Metric

The target_info metric shows OpenTelemetry resource information:

target_info{service_name="my-actual-service",service_version="1.0.0"} 1

If you see unknown_service:main, make sure you’re using the latest version of the metrics package.

3. Verify Configuration Order

Options can be passed in any order. The service name will be applied correctly:

// Both work the same
recorder := metrics.MustNew(
    metrics.WithServiceName("my-service"),
    metrics.WithPrometheus(":9090", "/metrics"),
)

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-service"),
)

4. Check Where Service Name Appears

The service name shows up in two places:

  1. Metric labels: Every metric has a service_name label

    http_requests_total{service_name="my-service",method="GET"} 42
    
  2. Target info: Resource metadata metric

    target_info{service_name="my-service",service_version="1.0.0"} 1
    

Metrics Not Appearing

OTLP Provider

Symptoms:

  • Metrics not visible in collector
  • No data in monitoring system
  • Silent failures

Solutions:

1. Call Start() Before Recording

The OTLP provider requires Start() to be called before recording metrics:

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithServiceName("my-service"),
)

// IMPORTANT: Call Start() before recording
if err := recorder.Start(ctx); err != nil {
    log.Fatal(err)
}

// Now recording works
_ = recorder.IncrementCounter(ctx, "requests_total")

2. Check OTLP Collector Reachability

Verify the collector is accessible:

# Test connectivity
curl http://localhost:4318/v1/metrics

# Check collector logs
docker logs otel-collector

3. Wait for Export Interval

OTLP exports metrics periodically (default: 30s):

// Reduce interval for testing
recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithExportInterval(5 * time.Second),
    metrics.WithServiceName("my-service"),
)

Or force immediate export:

if err := recorder.ForceFlush(ctx); err != nil {
    log.Printf("Failed to flush: %v", err)
}

4. Enable Logging

Add logging to see what’s happening:

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithLogger(slog.Default()),
    metrics.WithServiceName("my-service"),
)

Prometheus Provider

Symptoms:

  • Metrics endpoint returns 404
  • Empty metrics output
  • Server not accessible

Solutions:

1. Call Start() to Start Server

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("my-service"),
)

// Start the HTTP server
if err := recorder.Start(ctx); err != nil {
    log.Fatal(err)
}

2. Check Actual Address

If not using strict mode, server may use different port:

address := recorder.ServerAddress()
log.Printf("Metrics at: http://%s/metrics", address)

3. Verify Firewall/Network

Check if port is accessible:

# Test locally
curl http://localhost:9090/metrics

# Check from another machine
curl http://<server-ip>:9090/metrics

Stdout Provider

Symptoms:

  • No output to console
  • Metrics not visible

Solutions:

1. Wait for Export Interval

Stdout exports periodically (default: 30s):

recorder := metrics.MustNew(
    metrics.WithStdout(),
    metrics.WithExportInterval(5 * time.Second),  // Shorter interval
    metrics.WithServiceName("my-service"),
)

2. Force Flush

if err := recorder.ForceFlush(ctx); err != nil {
    log.Printf("Failed to flush: %v", err)
}

Port Conflicts

Symptoms

  • Error: address already in use
  • Metrics server fails to start
  • Different port than expected

Solutions

1. Use Strict Port Mode (Production)

Fail explicitly if port unavailable:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithStrictPort(),  // Fail if 9090 unavailable
    metrics.WithServiceName("my-service"),
)

2. Check Port Usage

Find what’s using the port:

# Linux/macOS
lsof -i :9090
netstat -tuln | grep 9090

# Windows
netstat -ano | findstr :9090

3. Use Dynamic Port (Testing)

Let the system choose an available port:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":0", "/metrics"),  // :0 = any available port
    metrics.WithServiceName("test-service"),
)
recorder.Start(ctx)

// Get actual port
address := recorder.ServerAddress()
log.Printf("Using port: %s", address)

4. Use Testing Utilities

For tests, use the testing utilities with automatic port allocation:

func TestMetrics(t *testing.T) {
    t.Parallel()
    recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
    // Automatically finds available port
}

Custom Metric Limit Reached

Symptoms

  • Error: custom metric limit reached
  • New metrics not created
  • Warning in logs

Solutions

1. Increase Limit

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithMaxCustomMetrics(5000),  // Increase from default 1000
    metrics.WithServiceName("my-service"),
)

2. Monitor Usage

Track how many custom metrics are created:

count := recorder.CustomMetricCount()
log.Printf("Custom metrics: %d/%d", count, maxLimit)

// Expose as a metric
_ = recorder.SetGauge(ctx, "custom_metrics_count", float64(count))

3. Review Metric Cardinality

Check if you’re creating too many unique metrics:

// BAD: High cardinality (unique per user)
_ = recorder.IncrementCounter(ctx, "user_"+userID+"_requests")

// GOOD: Low cardinality (use labels)
_ = recorder.IncrementCounter(ctx, "user_requests_total",
    attribute.String("user_type", userType),
)

4. Consolidate Metrics

Combine similar metrics:

// BAD: Many separate metrics
_ = recorder.IncrementCounter(ctx, "get_requests_total")
_ = recorder.IncrementCounter(ctx, "post_requests_total")
_ = recorder.IncrementCounter(ctx, "put_requests_total")

// GOOD: One metric with label
_ = recorder.IncrementCounter(ctx, "requests_total",
    attribute.String("method", "GET"),
)

What Counts as Custom Metric?

Counts:

  • Each unique metric name created with IncrementCounter, AddCounter, RecordHistogram, SetGauge

Does NOT count:

  • Built-in HTTP metrics
  • Different label combinations of same metric
  • Re-recording same metric name

Metrics Server Not Starting

Symptoms

  • Start() returns error
  • Server not accessible
  • No metrics endpoint

Solutions

1. Check Context

Ensure context is not canceled:

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

// Use context with Start
if err := recorder.Start(ctx); err != nil {
    log.Fatal(err)
}

2. Check Port Availability

See Port Conflicts section.

3. Enable Logging

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithLogger(slog.Default()),
    metrics.WithServiceName("my-service"),
)

4. Check Permissions

Ensure your process has permission to bind to the port (< 1024 requires root on Linux).

Invalid Metric Names

Symptoms

  • Error: invalid metric name
  • Metrics not recorded
  • Reserved prefix error

Solutions

1. Check Naming Rules

Metric names must:

  • Start with letter (a-z, A-Z)
  • Contain only: letters, numbers, underscores, dots, hyphens
  • Not use reserved prefixes: __, http_, router_
  • Maximum 255 characters

Valid:

_ = recorder.IncrementCounter(ctx, "orders_total")
_ = recorder.IncrementCounter(ctx, "api.v1.requests")
_ = recorder.IncrementCounter(ctx, "payment-success")

Invalid:

_ = recorder.IncrementCounter(ctx, "__internal")      // Reserved prefix
_ = recorder.IncrementCounter(ctx, "http_custom")     // Reserved prefix
_ = recorder.IncrementCounter(ctx, "router_gauge")    // Reserved prefix
_ = recorder.IncrementCounter(ctx, "1st_metric")      // Starts with number
_ = recorder.IncrementCounter(ctx, "my metric!")      // Invalid characters

2. Handle Errors

Check for naming errors:

if err := recorder.IncrementCounter(ctx, metricName); err != nil {
    log.Printf("Invalid metric name %q: %v", metricName, err)
}

High Memory Usage

Symptoms

  • Excessive memory consumption
  • Out of memory errors
  • Slow performance

Solutions

1. Reduce Metric Cardinality

Limit unique label combinations:

// BAD: High cardinality
_ = recorder.IncrementCounter(ctx, "requests_total",
    attribute.String("user_id", userID),        // Millions of values
    attribute.String("request_id", requestID),  // Always unique
)

// GOOD: Low cardinality
_ = recorder.IncrementCounter(ctx, "requests_total",
    attribute.String("user_type", userType),    // Few values
    attribute.String("region", region),          // Few values
)

2. Exclude High-Cardinality Paths

handler := metrics.Middleware(recorder,
    metrics.WithExcludePatterns(
        `^/api/users/[0-9]+$`,      // User IDs
        `^/api/orders/[a-z0-9-]+$`, // Order IDs
    ),
)(mux)

3. Reduce Histogram Buckets

// BAD: Too many buckets (15)
metrics.WithDurationBuckets(
    0.001, 0.005, 0.01, 0.025, 0.05,
    0.1, 0.25, 0.5, 1, 2.5,
    5, 10, 30, 60, 120,
)

// GOOD: Fewer buckets (7)
metrics.WithDurationBuckets(0.01, 0.1, 0.5, 1, 5, 10)

4. Monitor Custom Metrics

count := recorder.CustomMetricCount()
if count > 500 {
    log.Printf("WARNING: High custom metric count: %d", count)
}

Performance Issues

HTTP Middleware Overhead

Symptom: Slow request handling

Solution: Exclude high-traffic paths:

handler := metrics.Middleware(recorder,
    metrics.WithExcludePaths("/health"),  // Called frequently
    metrics.WithExcludePrefixes("/static/"),  // Static assets
)(mux)

Histogram Recording Slow

Symptom: High CPU usage

Solution: Reduce bucket count (see High Memory Usage).

Global State Issues

Symptoms

  • Multiple recorder instances conflict
  • Unexpected behavior with multiple services
  • Global meter provider issues

Solutions

By default, recorders do NOT set global meter provider:

// These work independently
recorder1 := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithServiceName("service-1"),
)

recorder2 := metrics.MustNew(
    metrics.WithStdout(),
    metrics.WithServiceName("service-2"),
)

2. Avoid WithGlobalMeterProvider

Only use WithGlobalMeterProvider() if you need:

  • OpenTelemetry instrumentation libraries to use your provider
  • otel.GetMeterProvider() to return your provider
// Only if needed
recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithGlobalMeterProvider(),  // Explicit opt-in
    metrics.WithServiceName("my-service"),
)

Thread Safety

All Recorder methods are thread-safe. No special handling needed for concurrent access:

// Safe to call from multiple goroutines
go func() {
    _ = recorder.IncrementCounter(ctx, "worker_1")
}()

go func() {
    _ = recorder.IncrementCounter(ctx, "worker_2")
}()

Shutdown Issues

Graceful Shutdown Not Working

Solution: Use proper timeout context:

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

if err := recorder.Shutdown(shutdownCtx); err != nil {
    log.Printf("Shutdown error: %v", err)
}

Metrics Not Flushed on Exit

Solution: Always defer Shutdown():

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithServiceName("my-service"),
)
recorder.Start(ctx)

defer func() {
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    recorder.Shutdown(shutdownCtx)
}()

Testing Issues

Port Conflicts in Parallel Tests

Solution: Use testing utilities with dynamic ports:

func TestHandler(t *testing.T) {
    t.Parallel()  // Safe with TestingRecorder
    
    // Uses stdout, no port needed
    recorder := metrics.TestingRecorder(t, "test-service")
    
    // Or with Prometheus (dynamic port)
    recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")
}

Server Not Ready

Solution: Wait for server:

recorder := metrics.TestingRecorderWithPrometheus(t, "test-service")

err := metrics.WaitForMetricsServer(t, recorder.ServerAddress(), 5*time.Second)
if err != nil {
    t.Fatal(err)
}

Getting Help

If you’re still experiencing issues:

  1. Check logs: Enable logging with WithLogger(slog.Default())
  2. Review configuration: Verify all options are correct
  3. Test connectivity: Ensure network access to endpoints
  4. Check version: Update to latest version
  5. File an issue: GitHub Issues

Quick Reference

Common Patterns

Production Setup:

recorder := metrics.MustNew(
    metrics.WithPrometheus(":9090", "/metrics"),
    metrics.WithStrictPort(),
    metrics.WithServiceName("my-api"),
    metrics.WithServiceVersion(version),
    metrics.WithLogger(slog.Default()),
)

OTLP Setup:

recorder := metrics.MustNew(
    metrics.WithOTLP("http://localhost:4318"),
    metrics.WithServiceName("my-service"),
)
// IMPORTANT: Call Start() before recording
recorder.Start(ctx)

Testing Setup:

func TestMetrics(t *testing.T) {
    t.Parallel()
    recorder := metrics.TestingRecorder(t, "test-service")
    // Test code...
}

Next Steps

6 - Tracing Package

API reference for rivaas.dev/tracing - Distributed tracing for Go applications

This is the API reference for the rivaas.dev/tracing package. For learning-focused documentation, see the Tracing Guide.

Package Information

Package Overview

The tracing package provides OpenTelemetry-based distributed tracing for Go applications with support for multiple exporters including Stdout, OTLP (gRPC and HTTP), and Noop.

Core Features

  • Multiple tracing providers (Stdout, OTLP, Noop)
  • Built-in HTTP middleware for request tracing
  • Manual span management with attributes and events
  • Context propagation for distributed tracing
  • Thread-safe operations
  • Span lifecycle hooks
  • Testing utilities

Architecture

The package is built on OpenTelemetry and provides a simplified interface for distributed tracing.

graph TD
    App[Application Code]
    Tracer[Tracer]
    Provider[Provider Layer]
    Noop[Noop]
    Stdout[Stdout]
    OTLP[OTLP gRPC/HTTP]
    Middleware[HTTP Middleware]
    Context[Context Propagation]
    
    App -->|Create Spans| Tracer
    Middleware -->|Auto-Trace| Tracer
    Tracer --> Provider
    Provider --> Noop
    Provider --> Stdout
    Provider --> OTLP
    Tracer --> Context
    Context -->|Extract/Inject| Middleware

Components

Main Package (rivaas.dev/tracing)

Core tracing functionality including:

  • Tracer - Main tracer for creating and managing spans
  • New() / MustNew() - Tracer initialization
  • Span management - Create, finish, add attributes/events
  • Middleware() - HTTP request tracing
  • ContextTracing - Helper for router context integration
  • Context helpers - Extract, inject, get trace IDs
  • Testing utilities

Quick API Index

Tracer Creation

tracer, err := tracing.New(options...)     // With error handling
tracer := tracing.MustNew(options...)      // Panics on error

Lifecycle Management

err := tracer.Start(ctx context.Context)   // Start OTLP providers
err := tracer.Shutdown(ctx context.Context) // Graceful shutdown

Span Management

// Create spans
ctx, span := tracer.StartSpan(ctx, "operation-name")
tracer.FinishSpan(span, statusCode)

// Add attributes
tracer.SetSpanAttribute(span, "key", value)

// Add events
tracer.AddSpanEvent(span, "event-name", attrs...)

Context Propagation

// Extract from incoming requests
ctx := tracer.ExtractTraceContext(ctx, req.Header)

// Inject into outgoing requests
tracer.InjectTraceContext(ctx, req.Header)

HTTP Middleware

handler := tracing.Middleware(tracer, options...)(httpHandler)
handler := tracing.MustMiddleware(tracer, options...)(httpHandler)

Context Helpers

traceID := tracing.TraceID(ctx)
spanID := tracing.SpanID(ctx)
tracing.SetSpanAttributeFromContext(ctx, "key", value)
tracing.AddSpanEventFromContext(ctx, "event-name", attrs...)

Testing Utilities

tracer := tracing.TestingTracer(t, options...)
tracer := tracing.TestingTracerWithStdout(t, options...)
middleware := tracing.TestingMiddleware(t, middlewareOptions...)

ContextTracing Helper

ct := tracing.NewContextTracing(ctx, tracer, span)
ct.SetSpanAttribute("key", value)
ct.AddSpanEvent("event-name", attrs...)
traceID := ct.TraceID()

Reference Pages

API Reference

Tracer type, span management, and context propagation.

View →

Options

Configuration options for providers and sampling.

View →

Middleware Options

HTTP middleware configuration and path exclusion.

View →

Troubleshooting

Common tracing issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Type Reference

Tracer

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

Main tracer for distributed tracing. Thread-safe for concurrent access.

Methods: See API Reference for complete method documentation.

Option

type Option func(*Tracer)

Configuration option function type used with New() and MustNew().

Available Options: See Options for all options.

MiddlewareOption

type MiddlewareOption func(*middlewareConfig)

HTTP middleware configuration option.

Available Options: See Middleware Options for all options.

Provider

type Provider string

const (
    NoopProvider     Provider = "noop"
    StdoutProvider   Provider = "stdout"
    OTLPProvider     Provider = "otlp"
    OTLPHTTPProvider Provider = "otlp-http"
)

Available tracing providers.

EventType

type EventType int

const (
    EventError   EventType = iota // Error events
    EventWarning                   // Warning events
    EventInfo                      // Informational events
    EventDebug                     // Debug events
)

Event severity levels for internal operational events.

Event

type Event struct {
    Type    EventType
    Message string
    Args    []any // slog-style key-value pairs
}

Internal operational event from the tracing package.

EventHandler

type EventHandler func(Event)

Processes internal operational events. Used with WithEventHandler option.

SpanStartHook

type SpanStartHook func(ctx context.Context, span trace.Span, req *http.Request)

Callback invoked when a request span is started.

SpanFinishHook

type SpanFinishHook func(span trace.Span, statusCode int)

Callback invoked when a request span is finished.

Common Patterns

Basic Usage

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithOTLP("localhost:4317"),
)
tracer.Start(context.Background())
defer tracer.Shutdown(context.Background())

ctx, span := tracer.StartSpan(ctx, "operation")
defer tracer.FinishSpan(span, http.StatusOK)

With HTTP Middleware

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithOTLP("localhost:4317"),
)
tracer.Start(context.Background())

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludePaths("/health"),
)(httpHandler)

http.ListenAndServe(":8080", handler)

Distributed Tracing

// Service A - inject trace context
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
tracer.InjectTraceContext(ctx, req.Header)
resp, _ := http.DefaultClient.Do(req)

// Service B - extract trace context
ctx = tracer.ExtractTraceContext(r.Context(), r.Header)
ctx, span := tracer.StartSpan(ctx, "operation")
defer tracer.FinishSpan(span, http.StatusOK)

Thread Safety

The Tracer type is thread-safe for:

  • All span management methods
  • Concurrent Start() and Shutdown() operations
  • Mixed tracing and lifecycle operations
  • Context propagation methods

Not thread-safe for:

  • Concurrent modification during initialization

Performance Notes

  • Request overhead (100% sampling): ~1.6 microseconds
  • Start/Finish span: ~160 nanoseconds
  • Set attribute: ~3 nanoseconds
  • Path exclusion (100 paths): ~9 nanoseconds

Best Practices:

  • Use sampling for high-traffic endpoints
  • Exclude health checks and metrics endpoints
  • Limit span attribute cardinality
  • Use path prefixes instead of regex when possible

Comparison with Metrics Package

The tracing package follows the same design pattern as the metrics package:

AspectMetrics PackageTracing Package
Main TypeRecorderTracer
Provider OptionsWithPrometheus(), WithOTLP()WithOTLP(), WithStdout(), WithNoop()
ConstructorNew(opts...) (*Recorder, error)New(opts...) (*Tracer, error)
Panic VersionMustNew(opts...) *RecorderMustNew(opts...) *Tracer
MiddlewareMiddleware(recorder, opts...)Middleware(tracer, opts...)
Panic MiddlewareMustMiddleware(recorder, opts...)MustMiddleware(tracer, opts...)
Path ExclusionMiddlewareOptionMiddlewareOption
Header RecordingMiddlewareOptionMiddlewareOption

Version Compatibility

The tracing package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

OpenTelemetry compatibility: Uses OpenTelemetry SDK v1.x

Next Steps

For learning-focused guides, see the Tracing Guide.

6.1 - API Reference

Complete API documentation for the Tracer type and all methods

Complete API reference for the Tracer type and all tracing methods.

Tracer Type

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

The main entry point for distributed tracing. Holds OpenTelemetry tracing configuration and runtime state. All operations on Tracer are thread-safe.

Important Notes

  • Immutable: Tracer is immutable after creation via New(). All configuration must be done through functional options.
  • Thread-safe: All methods are safe for concurrent use.
  • Global state: By default, does NOT set the global OpenTelemetry tracer provider. Use WithGlobalTracerProvider() option if needed.

Constructor Functions

New

func New(opts ...Option) (*Tracer, error)

Creates a new Tracer with the given options. Returns an error if the tracing provider fails to initialize.

Default configuration:

  • Service name: "rivaas-service".
  • Service version: "1.0.0".
  • Sample rate: 1.0 (100%).
  • Provider: NoopProvider.

Example:

tracer, err := tracing.New(
    tracing.WithServiceName("my-api"),
    tracing.WithOTLP("localhost:4317"),
    tracing.WithSampleRate(0.1),
)
if err != nil {
    log.Fatal(err)
}
defer tracer.Shutdown(context.Background())

MustNew

func MustNew(opts ...Option) *Tracer

Creates a new Tracer with the given options. Panics if the tracing provider fails to initialize. Use this when you want to panic on initialization errors.

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithStdout(),
)
defer tracer.Shutdown(context.Background())

Lifecycle Methods

Start

func (t *Tracer) Start(ctx context.Context) error

Initializes OTLP providers that require network connections. The context is used for the OTLP connection establishment. This method is idempotent; calling it multiple times is safe.

Required for: OTLP (gRPC and HTTP) providers
Optional for: Noop and Stdout providers (they initialize immediately in New())

Example:

tracer := tracing.MustNew(
    tracing.WithOTLP("localhost:4317"),
)

if err := tracer.Start(context.Background()); err != nil {
    log.Fatal(err)
}

Shutdown

func (t *Tracer) Shutdown(ctx context.Context) error

Gracefully shuts down the tracing system, flushing any pending spans. This should be called before the application exits to ensure all spans are exported. This method is idempotent - calling it multiple times is safe and will only perform shutdown once.

Example:

defer func() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := tracer.Shutdown(ctx); err != nil {
        log.Printf("Error shutting down tracer: %v", err)
    }
}()

Span Management Methods

StartSpan

func (t *Tracer) StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span)

Starts a new span with the given name and options. Returns a new context with the span attached and the span itself.

If tracing is disabled, returns the original context and a non-recording span. The returned span should always be ended, even if tracing is disabled.

Parameters:

  • ctx: Parent context
  • name: Span name (should be descriptive)
  • opts: Optional OpenTelemetry span start options

Returns:

  • New context with span attached
  • The created span

Example:

ctx, span := tracer.StartSpan(ctx, "database-query")
defer tracer.FinishSpan(span, http.StatusOK)

tracer.SetSpanAttribute(span, "db.query", "SELECT * FROM users")

FinishSpan

func (t *Tracer) FinishSpan(span trace.Span, statusCode int)

Completes the span with the given status code. Sets the span status based on the HTTP status code:

  • 2xx-3xx: Success (codes.Ok)
  • 4xx-5xx: Error (codes.Error)

This method is safe to call multiple times; subsequent calls are no-ops.

Parameters:

  • span: The span to finish
  • statusCode: HTTP status code (e.g., http.StatusOK)

Example:

defer tracer.FinishSpan(span, http.StatusOK)

SetSpanAttribute

func (t *Tracer) SetSpanAttribute(span trace.Span, key string, value any)

Adds an attribute to the span with type-safe handling.

Supported types:

  • string, int, int64, float64, bool: native OpenTelemetry handling
  • Other types: converted to string using fmt.Sprintf

This is a no-op if tracing is disabled, span is nil, or span is not recording.

Parameters:

  • span: The span to add the attribute to
  • key: Attribute key
  • value: Attribute value

Example:

tracer.SetSpanAttribute(span, "user.id", 12345)
tracer.SetSpanAttribute(span, "user.premium", true)
tracer.SetSpanAttribute(span, "user.name", "Alice")

AddSpanEvent

func (t *Tracer) AddSpanEvent(span trace.Span, name string, attrs ...attribute.KeyValue)

Adds an event to the span with optional attributes. Events represent important moments in a span’s lifetime.

This is a no-op if tracing is disabled, span is nil, or span is not recording.

Parameters:

  • span: The span to add the event to
  • name: Event name
  • attrs: Optional event attributes

Example:

import "go.opentelemetry.io/otel/attribute"

tracer.AddSpanEvent(span, "cache_hit",
    attribute.String("key", "user:123"),
    attribute.Int("ttl_seconds", 300),
)

Context Propagation Methods

ExtractTraceContext

func (t *Tracer) ExtractTraceContext(ctx context.Context, headers http.Header) context.Context

Extracts trace context from HTTP request headers. Returns a new context with the extracted trace information.

If no trace context is found in headers, returns the original context. Uses W3C Trace Context format by default.

Parameters:

  • ctx: Base context
  • headers: HTTP headers to extract from

Returns:

  • Context with extracted trace information

Example:

ctx := tracer.ExtractTraceContext(r.Context(), r.Header)
ctx, span := tracer.StartSpan(ctx, "operation")
defer tracer.FinishSpan(span, http.StatusOK)

InjectTraceContext

func (t *Tracer) InjectTraceContext(ctx context.Context, headers http.Header)

Injects trace context into HTTP headers. This allows trace context to propagate across service boundaries.

Uses W3C Trace Context format by default. This is a no-op if tracing is disabled.

Parameters:

  • ctx: Context containing trace information
  • headers: HTTP headers to inject into

Example:

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
tracer.InjectTraceContext(ctx, req.Header)
resp, _ := http.DefaultClient.Do(req)

Request Span Methods

These methods are used internally by the middleware but can also be used for custom HTTP handling.

StartRequestSpan

func (t *Tracer) StartRequestSpan(ctx context.Context, req *http.Request, path string, isStatic bool) (context.Context, trace.Span)

Starts a span for an HTTP request. This is used by the middleware to create request spans with standard attributes.

Parameters:

  • ctx: Request context
  • req: HTTP request
  • path: Request path
  • isStatic: Whether this is a static route

Returns:

  • Context with span
  • The created span

FinishRequestSpan

func (t *Tracer) FinishRequestSpan(span trace.Span, statusCode int)

Completes the span for an HTTP request. Sets the HTTP status code attribute and invokes the span finish hook if configured.

Parameters:

  • span: The span to finish
  • statusCode: HTTP response status code

Accessor Methods

IsEnabled

func (t *Tracer) IsEnabled() bool

Returns true if tracing is enabled.

ServiceName

func (t *Tracer) ServiceName() string

Returns the service name.

ServiceVersion

func (t *Tracer) ServiceVersion() string

Returns the service version.

GetTracer

func (t *Tracer) GetTracer() trace.Tracer

Returns the OpenTelemetry tracer.

GetPropagator

func (t *Tracer) GetPropagator() propagation.TextMapPropagator

Returns the OpenTelemetry propagator.

GetProvider

func (t *Tracer) GetProvider() Provider

Returns the current tracing provider.

Context Helper Functions

These are package-level functions for working with spans through context.

TraceID

func TraceID(ctx context.Context) string

Returns the current trace ID from the active span in the context. Returns an empty string if no active span or span context is invalid.

Example:

traceID := tracing.TraceID(ctx)
log.Printf("Processing request [trace=%s]", traceID)

SpanID

func SpanID(ctx context.Context) string

Returns the current span ID from the active span in the context. Returns an empty string if no active span or span context is invalid.

Example:

spanID := tracing.SpanID(ctx)
log.Printf("Processing request [span=%s]", spanID)

SetSpanAttributeFromContext

func SetSpanAttributeFromContext(ctx context.Context, key string, value any)

Adds an attribute to the current span from context. This is a no-op if tracing is not active.

Example:

func handleRequest(ctx context.Context) {
    tracing.SetSpanAttributeFromContext(ctx, "user.role", "admin")
    tracing.SetSpanAttributeFromContext(ctx, "user.id", 12345)
}

AddSpanEventFromContext

func AddSpanEventFromContext(ctx context.Context, name string, attrs ...attribute.KeyValue)

Adds an event to the current span from context. This is a no-op if tracing is not active.

Example:

import "go.opentelemetry.io/otel/attribute"

tracing.AddSpanEventFromContext(ctx, "cache_miss",
    attribute.String("key", "user:123"),
)

TraceContext

func TraceContext(ctx context.Context) context.Context

Returns the context as-is (it should already contain trace information). Provided for API consistency.

Event Types

EventType

type EventType int

const (
    EventError   EventType = iota // Error events
    EventWarning                   // Warning events
    EventInfo                      // Informational events
    EventDebug                     // Debug events
)

Event severity levels for internal operational events.

Event

type Event struct {
    Type    EventType
    Message string
    Args    []any // slog-style key-value pairs
}

Internal operational event from the tracing package. Events are used to report errors, warnings, and informational messages about the tracing system’s operation.

EventHandler

type EventHandler func(Event)

Processes internal operational events from the tracing package. Implementations can log events, send them to monitoring systems, or take custom actions based on event type.

Example:

tracing.WithEventHandler(func(e tracing.Event) {
    if e.Type == tracing.EventError {
        sentry.CaptureMessage(e.Message)
    }
    slog.Default().Info(e.Message, e.Args...)
})

DefaultEventHandler

func DefaultEventHandler(logger *slog.Logger) EventHandler

Returns an EventHandler that logs events to the provided slog.Logger. This is the default implementation used by WithLogger.

If logger is nil, returns a no-op handler that discards all events.

Hook Types

SpanStartHook

type SpanStartHook func(ctx context.Context, span trace.Span, req *http.Request)

Called when a request span is started. It receives the context, span, and HTTP request. This can be used for custom attribute injection, dynamic sampling, or integration with APM tools.

Example:

hook := func(ctx context.Context, span trace.Span, req *http.Request) {
    if tenantID := req.Header.Get("X-Tenant-ID"); tenantID != "" {
        span.SetAttributes(attribute.String("tenant.id", tenantID))
    }
}
tracer := tracing.MustNew(
    tracing.WithSpanStartHook(hook),
)

SpanFinishHook

type SpanFinishHook func(span trace.Span, statusCode int)

Called when a request span is finished. It receives the span and the HTTP status code. This can be used for custom metrics, logging, or post-processing.

Example:

hook := func(span trace.Span, statusCode int) {
    if statusCode >= 500 {
        metrics.IncrementServerErrors()
    }
}
tracer := tracing.MustNew(
    tracing.WithSpanFinishHook(hook),
)

ContextTracing Type

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

A helper type for router context integration that provides convenient access to tracing functionality within HTTP handlers.

NewContextTracing

func NewContextTracing(ctx context.Context, tracer *Tracer, span trace.Span) *ContextTracing

Creates a new context tracing helper. Panics if ctx is nil.

Parameters:

  • ctx: The request context
  • tracer: The Tracer instance
  • span: The current span

Example:

ct := tracing.NewContextTracing(ctx, tracer, span)

ContextTracing Methods

TraceID

func (ct *ContextTracing) TraceID() string

Returns the current trace ID. Returns an empty string if no valid span.

SpanID

func (ct *ContextTracing) SpanID() string

Returns the current span ID. Returns an empty string if no valid span.

SetSpanAttribute

func (ct *ContextTracing) SetSpanAttribute(key string, value any)

Adds an attribute to the current span. No-op if span is nil or not recording.

AddSpanEvent

func (ct *ContextTracing) AddSpanEvent(name string, attrs ...attribute.KeyValue)

Adds an event to the current span. No-op if span is nil or not recording.

TraceContext

func (ct *ContextTracing) TraceContext() context.Context

Returns the trace context.

GetSpan

func (ct *ContextTracing) GetSpan() trace.Span

Returns the current span.

GetTracer

func (ct *ContextTracing) GetTracer() *Tracer

Returns the underlying Tracer.

ContextTracing Example

func handleRequest(w http.ResponseWriter, r *http.Request, tracer *tracing.Tracer) {
    ctx := r.Context()
    span := trace.SpanFromContext(ctx)
    
    // Create context tracing helper
    ct := tracing.NewContextTracing(ctx, tracer, span)
    
    // Use helper methods
    ct.SetSpanAttribute("user.id", "123")
    ct.AddSpanEvent("processing_started")
    
    // Get trace info for logging
    log.Printf("Processing [trace=%s, span=%s]", ct.TraceID(), ct.SpanID())
}

Constants

Default Values

const (
    DefaultServiceName    = "rivaas-service"
    DefaultServiceVersion = "1.0.0"
    DefaultSampleRate     = 1.0
)

Default configuration values used when not explicitly set.

Provider Types

const (
    NoopProvider     Provider = "noop"
    StdoutProvider   Provider = "stdout"
    OTLPProvider     Provider = "otlp"
    OTLPHTTPProvider Provider = "otlp-http"
)

Available tracing providers.

Next Steps

6.2 - Tracer Options

All configuration options for Tracer initialization

Complete reference for all Option functions used to configure the Tracer.

Option Type

type Option func(*Tracer)

Configuration option function type used with New() and MustNew(). Options are applied during Tracer creation.

Service Configuration Options

WithServiceName

func WithServiceName(name string) Option

Sets the service name for tracing. This name appears in span attributes as service.name.

Parameters:

  • name: Service identifier like "user-api" or "order-service".

Default: "rivaas-service"

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("user-api"),
)

WithServiceVersion

func WithServiceVersion(version string) Option

Sets the service version for tracing. This version appears in span attributes as service.version.

Parameters:

  • version: Service version like "v1.2.3" or "dev".

Default: "1.0.0"

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("user-api"),
    tracing.WithServiceVersion("v1.2.3"),
)

Provider Options

Only one provider can be configured at a time. Configuring multiple providers results in a validation error.

WithNoop

func WithNoop() Option

Configures noop provider. This is the default. No traces are exported. Use for testing or when tracing is disabled.

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithNoop(),
)

WithStdout

func WithStdout() Option

Configures stdout provider for development/debugging. Traces are printed to standard output in pretty-printed JSON format.

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithStdout(),
)

WithOTLP

func WithOTLP(endpoint string, opts ...OTLPOption) Option

Configures OTLP gRPC provider with endpoint. Use this for production deployments with OpenTelemetry collectors.

Parameters:

  • endpoint: OTLP endpoint in format "host:port" (e.g., "localhost:4317")
  • opts: Optional OTLP-specific options (e.g., OTLPInsecure())

Requires: Call tracer.Start(ctx) before tracing

Example:

// Secure (TLS enabled by default)
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("collector.example.com:4317"),
)

// Insecure (local development)
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317", tracing.OTLPInsecure()),
)

WithOTLPHTTP

func WithOTLPHTTP(endpoint string) Option

Configures OTLP HTTP provider with endpoint. Use this when gRPC is not available or HTTP is preferred.

Parameters:

  • endpoint: OTLP HTTP endpoint with protocol (e.g., "http://localhost:4318", "https://collector:4318")

Requires: Call tracer.Start(ctx) before tracing

Example:

// HTTP (insecure - development)
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLPHTTP("http://localhost:4318"),
)

// HTTPS (secure - production)
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLPHTTP("https://collector.example.com:4318"),
)

OTLP Options

OTLPInsecure

func OTLPInsecure() OTLPOption

Enables insecure gRPC for OTLP. Default is false (uses TLS). Set to true for local development.

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317", tracing.OTLPInsecure()),
)

Sampling Options

WithSampleRate

func WithSampleRate(rate float64) Option

Sets the sampling rate (0.0 to 1.0). Values outside this range are clamped to valid bounds.

A rate of 1.0 samples all requests, 0.5 samples 50%, and 0.0 samples none. Sampling decisions are made per-request based on the configured rate.

Parameters:

  • rate: Sampling rate between 0.0 and 1.0

Default: 1.0 (100% sampling)

Example:

// Sample 10% of requests
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSampleRate(0.1),
)

Hook Options

WithSpanStartHook

func WithSpanStartHook(hook SpanStartHook) Option

Sets a callback that is invoked when a request span is started. The hook receives the context, span, and HTTP request, allowing custom attribute injection, dynamic sampling decisions, or integration with APM tools.

Type:

type SpanStartHook func(ctx context.Context, span trace.Span, req *http.Request)

Example:

startHook := func(ctx context.Context, span trace.Span, req *http.Request) {
    if tenantID := req.Header.Get("X-Tenant-ID"); tenantID != "" {
        span.SetAttributes(attribute.String("tenant.id", tenantID))
    }
}

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSpanStartHook(startHook),
)

WithSpanFinishHook

func WithSpanFinishHook(hook SpanFinishHook) Option

Sets a callback that is invoked when a request span is finished. The hook receives the span and HTTP status code, allowing custom metrics recording, logging, or post-processing.

Type:

type SpanFinishHook func(span trace.Span, statusCode int)

Example:

finishHook := func(span trace.Span, statusCode int) {
    if statusCode >= 500 {
        metrics.IncrementServerErrors()
    }
}

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSpanFinishHook(finishHook),
)

Logging Options

WithLogger

func WithLogger(logger *slog.Logger) Option

Sets the logger for internal operational events using the default event handler. This is a convenience wrapper around WithEventHandler that logs events to the provided slog.Logger.

Parameters:

  • logger: *slog.Logger for logging internal events

Example:

import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithLogger(logger),
)

WithEventHandler

func WithEventHandler(handler EventHandler) Option

Sets a custom event handler for internal operational events. Use this for advanced use cases like sending errors to Sentry, custom alerting, or integrating with non-slog logging systems.

Type:

type EventHandler func(Event)

Example:

eventHandler := func(e tracing.Event) {
    switch e.Type {
    case tracing.EventError:
        sentry.CaptureMessage(e.Message)
        myLogger.Error(e.Message, e.Args...)
    case tracing.EventWarning:
        myLogger.Warn(e.Message, e.Args...)
    case tracing.EventInfo:
        myLogger.Info(e.Message, e.Args...)
    case tracing.EventDebug:
        myLogger.Debug(e.Message, e.Args...)
    }
}

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithEventHandler(eventHandler),
)

Advanced Options

WithTracerProvider

func WithTracerProvider(provider trace.TracerProvider) Option

Allows you to provide a custom OpenTelemetry TracerProvider. When using this option, the package will NOT set the global otel.SetTracerProvider() by default. Use WithGlobalTracerProvider() if you want global registration.

Use cases:

  • Manage tracer provider lifecycle yourself
  • Need multiple independent tracing configurations
  • Want to avoid global state in your application

Important: When using WithTracerProvider, provider options (WithOTLP, WithStdout, etc.) are ignored since you’re managing the provider yourself. You are also responsible for calling Shutdown() on your provider.

Example:

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

tp := sdktrace.NewTracerProvider(
    // Your custom configuration
    sdktrace.WithSampler(sdktrace.AlwaysSample()),
)

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithTracerProvider(tp),
)

// You manage tp.Shutdown() yourself
defer tp.Shutdown(context.Background())

WithCustomTracer

func WithCustomTracer(tracer trace.Tracer) Option

Allows using a custom OpenTelemetry tracer. This is useful when you need specific tracer configuration or want to use a tracer from an existing OpenTelemetry setup.

Example:

tp := trace.NewTracerProvider(...)
customTracer := tp.Tracer("my-tracer")

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithCustomTracer(customTracer),
)

WithCustomPropagator

func WithCustomPropagator(propagator propagation.TextMapPropagator) Option

Allows using a custom OpenTelemetry propagator. This is useful for custom trace context propagation formats. By default, uses the global propagator from otel.GetTextMapPropagator() (W3C Trace Context).

Example:

import "go.opentelemetry.io/otel/propagation"

// Use W3C Trace Context explicitly
prop := propagation.TraceContext{}

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithCustomPropagator(prop),
)

Using B3 propagation:

import "go.opentelemetry.io/contrib/propagators/b3"

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithCustomPropagator(b3.New()),
)

WithGlobalTracerProvider

func WithGlobalTracerProvider() Option

Registers the tracer provider as the global OpenTelemetry tracer provider via otel.SetTracerProvider(). By default, tracer providers are not registered globally to allow multiple tracing configurations to coexist in the same process.

Use when:

  • You want otel.GetTracerProvider() to return your tracer
  • Integrating with libraries that use the global tracer
  • Single tracer for entire application

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317"),
    tracing.WithGlobalTracerProvider(), // Register globally
)

Option Combinations

Development Configuration

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithServiceVersion("dev"),
    tracing.WithStdout(),
    tracing.WithSampleRate(1.0),
    tracing.WithLogger(slog.Default()),
)

Production Configuration

tracer := tracing.MustNew(
    tracing.WithServiceName("user-api"),
    tracing.WithServiceVersion(os.Getenv("VERSION")),
    tracing.WithOTLP(os.Getenv("OTLP_ENDPOINT")),
    tracing.WithSampleRate(0.1),
    tracing.WithSpanStartHook(enrichSpan),
    tracing.WithSpanFinishHook(recordMetrics),
)

Testing Configuration

tracer := tracing.MustNew(
    tracing.WithServiceName("test-service"),
    tracing.WithServiceVersion("v1.0.0"),
    tracing.WithNoop(),
    tracing.WithSampleRate(1.0),
)

Validation Errors

Configuration is validated when calling New() or MustNew(). Common validation errors:

Multiple Providers

// ✗ Error: multiple providers configured
tracer, err := tracing.New(
    tracing.WithServiceName("my-service"),
    tracing.WithStdout(),
    tracing.WithOTLP("localhost:4317"), // Error!
)
// Returns: "validation errors: provider: multiple providers configured"

Solution: Only configure one provider.

Empty Service Name

// ✗ Error: service name cannot be empty
tracer, err := tracing.New(
    tracing.WithServiceName(""),
)
// Returns: "invalid configuration: serviceName: cannot be empty"

Solution: Always provide a service name.

Invalid Sample Rate

// Values are automatically clamped
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSampleRate(1.5), // Clamped to 1.0
)

Sample rates outside 0.0-1.0 are automatically clamped to valid bounds.

Complete Option Reference

OptionDescriptionDefault
WithServiceName(name)Set service name"rivaas-service"
WithServiceVersion(version)Set service version"1.0.0"
WithNoop()Noop providerYes (default)
WithStdout()Stdout provider-
WithOTLP(endpoint, opts...)OTLP gRPC provider-
WithOTLPHTTP(endpoint)OTLP HTTP provider-
WithSampleRate(rate)Sampling rate (0.0-1.0)1.0
WithSpanStartHook(hook)Span start callback-
WithSpanFinishHook(hook)Span finish callback-
WithLogger(logger)Set slog logger-
WithEventHandler(handler)Custom event handler-
WithTracerProvider(provider)Custom tracer provider-
WithCustomTracer(tracer)Custom tracer-
WithCustomPropagator(prop)Custom propagatorW3C Trace Context
WithGlobalTracerProvider()Register globallyNo

Next Steps

6.3 - Middleware Options

All configuration options for HTTP middleware

Complete reference for all MiddlewareOption functions used to configure the HTTP tracing middleware.

MiddlewareOption Type

type MiddlewareOption func(*middlewareConfig)

Configuration option function type used with Middleware() and MustMiddleware(). These options control HTTP request tracing behavior.

Path Exclusion Options

Exclude specific paths from tracing to reduce noise and overhead.

WithExcludePaths

func WithExcludePaths(paths ...string) MiddlewareOption

Excludes specific paths from tracing. Excluded paths will not create spans or record any tracing data. This is useful for health checks, metrics endpoints, etc.

Maximum of 1000 paths can be excluded to prevent unbounded growth.

Parameters:

  • paths: Exact paths to exclude (e.g., "/health", "/metrics")

Performance: O(1) hash map lookup

Example:

handler := tracing.Middleware(tracer,
    tracing.WithExcludePaths("/health", "/metrics", "/ready", "/live"),
)(mux)

WithExcludePrefixes

func WithExcludePrefixes(prefixes ...string) MiddlewareOption

Excludes paths with the given prefixes from tracing. This is useful for excluding entire path hierarchies like /debug/, /internal/, etc.

Parameters:

  • prefixes: Path prefixes to exclude (e.g., "/debug/", "/internal/")

Performance: O(n) where n = number of prefixes

Example:

handler := tracing.Middleware(tracer,
    tracing.WithExcludePrefixes("/debug/", "/internal/", "/.well-known/"),
)(mux)

Matches:

  • /debug/pprof
  • /debug/vars
  • /internal/health
  • /.well-known/acme-challenge

WithExcludePatterns

func WithExcludePatterns(patterns ...string) MiddlewareOption

Excludes paths matching the given regex patterns from tracing. The patterns are compiled once during configuration. Returns a validation error if any pattern fails to compile.

Parameters:

  • patterns: Regular expression patterns (e.g., "^/v[0-9]+/internal/.*")

Performance: O(p) where p = number of patterns

Validation: Invalid regex patterns cause the middleware to panic during initialization.

Example:

handler := tracing.Middleware(tracer,
    tracing.WithExcludePatterns(
        `^/v[0-9]+/internal/.*`,  // Version-prefixed internal routes
        `^/api/health.*`,          // Any health-related endpoint
        `^/debug/.*`,              // All debug routes
    ),
)(mux)

Matches:

  • /v1/internal/status
  • /v2/internal/debug
  • /api/health
  • /api/health/db
  • /debug/pprof/heap

Header Recording Options

WithHeaders

func WithHeaders(headers ...string) MiddlewareOption

Records specific request headers as span attributes. Header names are case-insensitive. Recorded as http.request.header.{name}.

Security: Sensitive headers (Authorization, Cookie, etc.) are automatically filtered out to prevent accidental exposure of credentials in traces.

Parameters:

  • headers: Header names to record (case-insensitive)

Recorded as: Lowercase header names (http.request.header.x-request-id)

Example:

handler := tracing.Middleware(tracer,
    tracing.WithHeaders("X-Request-ID", "X-Correlation-ID", "User-Agent"),
)(mux)

Span attributes:

  • http.request.header.x-request-id: "abc123"
  • http.request.header.x-correlation-id: "xyz789"
  • http.request.header.user-agent: "Mozilla/5.0..."

Sensitive Header Filtering

The following headers are automatically filtered and will never be recorded, even if explicitly included:

  • Authorization
  • Cookie
  • Set-Cookie
  • X-API-Key
  • X-Auth-Token
  • Proxy-Authorization
  • WWW-Authenticate

Example:

// Authorization is automatically filtered
handler := tracing.Middleware(tracer,
    tracing.WithHeaders(
        "X-Request-ID",
        "Authorization", // ← Filtered, won't be recorded
        "X-Correlation-ID",
    ),
)(mux)

Query Parameter Recording Options

Default Behavior

By default, all query parameters are recorded as span attributes.

WithRecordParams

func WithRecordParams(params ...string) MiddlewareOption

Specifies which URL query parameters to record as span attributes. Only parameters in this list will be recorded. This provides fine-grained control over which parameters are traced.

If this option is not used, all query parameters are recorded by default (unless WithoutParams is used).

Parameters:

  • params: Parameter names to record

Recorded as: http.request.param.{name}

Example:

handler := tracing.Middleware(tracer,
    tracing.WithRecordParams("user_id", "request_id", "page", "limit"),
)(mux)

Request: GET /api/users?page=2&limit=10&user_id=123&secret=xyz

Span attributes:

  • http.request.param.page: ["2"]
  • http.request.param.limit: ["10"]
  • http.request.param.user_id: ["123"]
  • secret is not recorded (not in whitelist)

WithExcludeParams

func WithExcludeParams(params ...string) MiddlewareOption

Specifies which URL query parameters to exclude from tracing. This is useful for blacklisting sensitive parameters while recording all others.

Parameters in this list will never be recorded, even if WithRecordParams includes them (blacklist takes precedence).

Parameters:

  • params: Parameter names to exclude

Example:

handler := tracing.Middleware(tracer,
    tracing.WithExcludeParams("password", "token", "api_key", "secret"),
)(mux)

Request: GET /api/users?page=2&password=secret123&user_id=123

Span attributes:

  • http.request.param.page: ["2"]
  • http.request.param.user_id: ["123"]
  • password is not recorded (blacklisted)

WithoutParams

func WithoutParams() MiddlewareOption

Disables recording URL query parameters as span attributes. By default, all query parameters are recorded. Use this option if parameters may contain sensitive data.

Example:

handler := tracing.Middleware(tracer,
    tracing.WithoutParams(),
)(mux)

No query parameters will be recorded regardless of the request.

Parameter Recording Precedence

When multiple parameter options are used:

  1. WithoutParams() - If set, no parameters are recorded
  2. WithExcludeParams() - Blacklist takes precedence over whitelist
  3. WithRecordParams() - Only whitelisted parameters are recorded
  4. Default - All parameters are recorded

Example:

// Whitelist with blacklist
handler := tracing.Middleware(tracer,
    tracing.WithRecordParams("page", "limit", "sort", "api_key"),
    tracing.WithExcludeParams("api_key", "token"), // Blacklist overrides
)(mux)

Result: page, limit, and sort are recorded, but api_key is excluded (blacklist wins).

Middleware Functions

Middleware

func Middleware(tracer *Tracer, opts ...MiddlewareOption) func(http.Handler) http.Handler

Creates a middleware function for standalone HTTP integration. Panics if any middleware option is invalid (e.g., invalid regex pattern).

Parameters:

  • tracer: Tracer instance
  • opts: Middleware configuration options

Returns: HTTP middleware function

Panics: If middleware options are invalid

Example:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithOTLP("localhost:4317"),
)

handler := tracing.Middleware(tracer,
    tracing.WithExcludePaths("/health", "/metrics"),
    tracing.WithHeaders("X-Request-ID"),
)(mux)

MustMiddleware

func MustMiddleware(tracer *Tracer, opts ...MiddlewareOption) func(http.Handler) http.Handler

Creates a middleware function for standalone HTTP integration. It panics if any middleware option is invalid (e.g., invalid regex pattern). This is a convenience wrapper around Middleware for consistency with MustNew.

Behavior: Identical to Middleware() - both panic on invalid options.

Example:

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludePaths("/health", "/metrics"),
    tracing.WithHeaders("X-Request-ID"),
)(mux)

Complete Examples

Minimal Middleware

// Trace everything with no filtering
handler := tracing.Middleware(tracer)(mux)

Production Middleware

handler := tracing.Middleware(tracer,
    // Exclude observability endpoints
    tracing.WithExcludePaths("/health", "/metrics", "/ready", "/live"),
    
    // Exclude debug endpoints
    tracing.WithExcludePrefixes("/debug/", "/internal/"),
    
    // Record correlation headers
    tracing.WithHeaders("X-Request-ID", "X-Correlation-ID"),
    
    // Whitelist safe parameters
    tracing.WithRecordParams("page", "limit", "sort", "filter"),
    
    // Blacklist sensitive parameters
    tracing.WithExcludeParams("password", "token", "api_key"),
)(mux)

Development Middleware

handler := tracing.Middleware(tracer,
    // Only exclude metrics
    tracing.WithExcludePaths("/metrics"),
    
    // Record all headers (except sensitive ones)
    tracing.WithHeaders("X-Request-ID", "X-Correlation-ID", "User-Agent"),
)(mux)

High-Security Middleware

handler := tracing.Middleware(tracer,
    // Exclude health checks
    tracing.WithExcludePaths("/health"),
    
    // No headers recorded
    // No query parameters recorded
    tracing.WithoutParams(),
)(mux)

Performance Considerations

Path Exclusion Performance

MethodComplexityPerformance
WithExcludePaths()O(1)~9ns per request (hash lookup)
WithExcludePrefixes()O(n)~9ns per request (n prefixes)
WithExcludePatterns()O(p)~20ns per request (p patterns)

Recommendation: Use exact paths when possible for best performance.

Memory Usage

  • Path exclusion: ~100 bytes per path
  • Header recording: ~50 bytes per header
  • Parameter recording: ~30 bytes per parameter name

Limits

  • Maximum excluded paths: 1000 (enforced by WithExcludePaths)
  • No limit on: Prefixes, patterns, headers, parameters

Validation Errors

Configuration is validated when calling Middleware() or MustMiddleware(). Invalid options cause a panic.

Invalid Regex Pattern

// ✗ Panics: invalid regex
handler := tracing.Middleware(tracer,
    tracing.WithExcludePatterns(`[invalid regex`),
)(mux)
// Panics: "middleware validation errors: excludePatterns: invalid regex..."

Solution: Ensure regex patterns are valid.

Option Reference Table

OptionDescriptionDefault Behavior
WithExcludePaths(paths...)Exclude exact pathsAll paths traced
WithExcludePrefixes(prefixes...)Exclude by prefixAll paths traced
WithExcludePatterns(patterns...)Exclude by regexAll paths traced
WithHeaders(headers...)Record headersNo headers recorded
WithRecordParams(params...)Whitelist paramsAll params recorded
WithExcludeParams(params...)Blacklist paramsNo params excluded
WithoutParams()Disable paramsAll params recorded

Best Practices

Always Exclude Health Checks

tracing.WithExcludePaths("/health", "/metrics", "/ready", "/live")

Health checks are high-frequency and low-value for tracing.

Use Exact Paths for Common Exclusions

// ✓ Good - fastest
tracing.WithExcludePaths("/health", "/metrics")

// ✗ Less optimal - slower
tracing.WithExcludePatterns("^/(health|metrics)$")

Blacklist Sensitive Parameters

tracing.WithExcludeParams(
    "password", "token", "api_key", "secret",
    "credit_card", "ssn", "access_token",
)

Record Correlation Headers

tracing.WithHeaders("X-Request-ID", "X-Correlation-ID", "X-Trace-ID")

Helps correlate traces with logs and other observability data.

Combine Exclusion Methods

handler := tracing.Middleware(tracer,
    tracing.WithExcludePaths("/health", "/metrics"),      // Exact
    tracing.WithExcludePrefixes("/debug/", "/internal/"), // Prefix
    tracing.WithExcludePatterns(`^/v[0-9]+/internal/.*`), // Regex
)(mux)

Next Steps

6.4 - Troubleshooting

Common issues and solutions for the tracing package

Common issues and solutions when using the tracing package.

Traces Not Appearing

Symptom

No traces appear in your tracing backend (Jaeger, Zipkin, etc.) even though tracing is configured.

Possible Causes & Solutions

1. OTLP Provider Not Started

Problem: OTLP providers require calling Start(ctx) before tracing.

Solution:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317"),
)

// ✓ Required for OTLP providers
if err := tracer.Start(context.Background()); err != nil {
    log.Fatal(err)
}

2. Sampling Rate Too Low

Problem: Sample rate is set too low. For example, 1% sampling means 99% of requests aren’t traced.

Solution: Increase sample rate or remove sampling for testing.

// Development - trace everything
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSampleRate(1.0), // 100% sampling
)

3. Wrong Provider Configured

Problem: Using Noop provider (no traces exported).

Solution: Verify provider configuration:

// ✗ Bad - no traces exported
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithNoop(), // No traces!
)

// ✓ Good - traces exported
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317"),
)

4. Paths Excluded from Tracing

Problem: Paths are excluded via middleware options.

Solution: Check middleware exclusions.

// Check if your paths are excluded
handler := tracing.Middleware(tracer,
    tracing.WithExcludePaths("/health", "/api/users"), // ← Is this excluding your endpoint?
)(mux)

5. Shutdown Called Too Early

Problem: Application exits before spans are exported.

Solution: Ensure proper shutdown with timeout:

defer func() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := tracer.Shutdown(ctx); err != nil {
        log.Printf("Error shutting down tracer: %v", err)
    }
}()

6. OTLP Endpoint Unreachable

Problem: OTLP collector is not running or unreachable.

Solution: Verify collector is running:

# Check if collector is listening
nc -zv localhost 4317  # OTLP gRPC
nc -zv localhost 4318  # OTLP HTTP

Check logs for connection errors:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317"),
    tracing.WithLogger(logger), // See connection errors
)

Context Propagation Issues

Symptom

Services create separate traces instead of one distributed trace.

Possible Causes & Solutions

1. Context Not Propagated

Problem: Context is not passed through the call chain.

Solution: Always pass context:

// ✓ Good - context propagates
func handler(ctx context.Context) {
    result := doWork(ctx)  // Pass context
}

// ✗ Bad - context lost
func handler(ctx context.Context) {
    result := doWork(context.Background())  // Lost!
}

2. Trace Context Not Injected

Problem: Trace context not injected into outgoing requests.

Solution: Always inject before making requests:

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)

// ✓ Required - inject trace context
tracer.InjectTraceContext(ctx, req.Header)

resp, _ := http.DefaultClient.Do(req)

3. Trace Context Not Extracted

Problem: Incoming requests don’t extract trace context.

Solution: Middleware automatically extracts, or do it manually:

// Automatic (with middleware)
handler := tracing.Middleware(tracer)(mux)

// Manual (without middleware)
func myHandler(w http.ResponseWriter, r *http.Request) {
    ctx := tracer.ExtractTraceContext(r.Context(), r.Header)
    // Use extracted context...
}

4. Different Propagators

Problem: Services use different propagation formats.

Solution: Ensure all services use the same propagator (default is W3C Trace Context):

// All services should use default (W3C) or same custom propagator
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    // Default propagator is W3C Trace Context
)

Performance Issues

Symptom

High CPU usage, increased latency, or memory consumption.

Possible Causes & Solutions

1. Too Much Sampling

Problem: Sampling 100% of high-traffic endpoints.

Solution: Reduce sample rate:

// For high-traffic services (> 1000 req/s)
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSampleRate(0.1), // 10% sampling
)

2. Not Excluding High-Frequency Endpoints

Problem: Tracing health checks and metrics endpoints.

Solution: Exclude them:

handler := tracing.Middleware(tracer,
    tracing.WithExcludePaths("/health", "/metrics", "/ready", "/live"),
)(mux)

3. Too Many Span Attributes

Problem: Adding excessive attributes to every span.

Solution: Only add essential attributes:

// ✓ Good - essential attributes
tracer.SetSpanAttribute(span, "user.id", userID)
tracer.SetSpanAttribute(span, "request.id", requestID)

// ✗ Bad - too many attributes
for k, v := range req.Header {
    tracer.SetSpanAttribute(span, k, v) // Don't do this!
}

4. Using Regex for Path Exclusion

Problem: Regex patterns are slower than exact paths.

Solution: Prefer exact paths or prefixes:

// ✓ Faster - O(1) hash lookup
tracing.WithExcludePaths("/health", "/metrics")

// ✗ Slower - O(p) regex matching
tracing.WithExcludePatterns("^/(health|metrics)$")

Configuration Errors

Multiple Providers Configured

Error: "validation errors: provider: multiple providers configured"

Problem: Attempting to configure multiple providers.

Solution: Only configure one provider:

// ✗ Error - multiple providers
tracer, err := tracing.New(
    tracing.WithServiceName("my-service"),
    tracing.WithStdout(),
    tracing.WithOTLP("localhost:4317"), // Error!
)

// ✓ Good - one provider
tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317"),
)

Empty Service Name

Error: "invalid configuration: serviceName: cannot be empty"

Problem: Service name not provided or empty string.

Solution: Always provide a service name:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"), // Required
)

Invalid Regex Pattern

Error: Middleware panics with "middleware validation errors: excludePatterns: invalid regex..."

Problem: Invalid regex pattern in WithExcludePatterns.

Solution: Validate regex patterns:

// ✗ Invalid regex
tracing.WithExcludePatterns(`[invalid`)

// ✓ Valid regex
tracing.WithExcludePatterns(`^/v[0-9]+/internal/.*`)

OTLP Connection Issues

TLS Certificate Errors

Problem: TLS certificate verification fails.

Solution: Use insecure connection for local development:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317", tracing.OTLPInsecure()),
)

For production, ensure proper TLS certificates are configured.

Connection Refused

Problem: Cannot connect to OTLP endpoint.

Solution:

  1. Verify collector is running:

    docker ps | grep otel-collector
    
  2. Check endpoint is correct:

    // Correct format: "host:port"
    tracing.WithOTLP("localhost:4317")
    
    // Not: "http://localhost:4317" (no protocol for gRPC)
    
  3. Check network connectivity:

    telnet localhost 4317
    

Wrong Endpoint for HTTP

Problem: Using gRPC endpoint for HTTP or vice versa.

Solution: Use correct provider and endpoint:

// OTLP gRPC (port 4317)
tracing.WithOTLP("localhost:4317")

// OTLP HTTP (port 4318, include protocol)
tracing.WithOTLPHTTP("http://localhost:4318")

Middleware Issues

Spans Not Created

Problem: Middleware doesn’t create spans for requests.

Solution: Ensure middleware is applied:

mux := http.NewServeMux()
mux.HandleFunc("/api/users", handleUsers)

// ✓ Middleware applied
handler := tracing.Middleware(tracer)(mux)
http.ListenAndServe(":8080", handler)

// ✗ Middleware not applied
http.ListenAndServe(":8080", mux) // No tracing!

Context Lost in Handlers

Problem: Context doesn’t contain trace information.

Solution: Use context from request:

func handleUsers(w http.ResponseWriter, r *http.Request) {
    // ✓ Good - use request context
    ctx := r.Context()
    traceID := tracing.TraceID(ctx)
    
    // ✗ Bad - creates new context
    ctx := context.Background() // Lost trace context!
}

Testing Issues

Tests Fail to Clean Up

Problem: Tests hang or don’t complete cleanup.

Solution: Use testing utilities:

func TestSomething(t *testing.T) {
    // ✓ Good - automatic cleanup
    tracer := tracing.TestingTracer(t)
    
    // ✗ Bad - manual cleanup required
    tracer, _ := tracing.New(tracing.WithNoop())
    defer tracer.Shutdown(context.Background())
}

Race Conditions in Tests

Problem: Race detector reports issues in parallel tests.

Solution: Use t.Parallel() correctly:

func TestParallel(t *testing.T) {
    t.Parallel() // Each test gets its own tracer
    
    tracer := tracing.TestingTracer(t)
    // Use tracer...
}

Debugging Tips

Enable Debug Logging

See internal events:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithLogger(logger),
)

Use Stdout Provider

See traces immediately:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithStdout(), // Print traces to console
)

Check Trace IDs

Verify trace context is propagated:

func handleRequest(ctx context.Context) {
    traceID := tracing.TraceID(ctx)
    log.Printf("Processing request [trace=%s]", traceID)
    
    if traceID == "" {
        log.Printf("WARNING: No trace context!")
    }
}

Verify Sampling

Log sampling decisions:

startHook := func(ctx context.Context, span trace.Span, req *http.Request) {
    if span.SpanContext().IsValid() {
        log.Printf("Request sampled: %s", req.URL.Path)
    } else {
        log.Printf("Request not sampled: %s", req.URL.Path)
    }
}

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithSpanStartHook(startHook),
)

Getting Help

Check Documentation

Check Logs

Enable debug logging to see internal events:

tracing.WithLogger(slog.Default())

Verify Configuration

Print configuration at startup:

tracer := tracing.MustNew(
    tracing.WithServiceName("my-service"),
    tracing.WithOTLP("localhost:4317"),
    tracing.WithSampleRate(0.1),
)

log.Printf("Tracer configured:")
log.Printf("  Service: %s", tracer.ServiceName())
log.Printf("  Version: %s", tracer.ServiceVersion())
log.Printf("  Provider: %s", tracer.GetProvider())
log.Printf("  Enabled: %v", tracer.IsEnabled())

Common Pitfalls Checklist

  • Called Start() for OTLP providers?
  • Shutdown with proper timeout?
  • Context propagated through call chain?
  • Trace context injected into outgoing requests?
  • Sample rate high enough to see traces?
  • Paths not excluded from tracing?
  • OTLP collector running and reachable?
  • All services using same propagator?
  • Only one provider configured?

Version Compatibility

Go Version

Minimum required: Go 1.25+

Error: go: module requires Go 1.25 or later

Solution: Upgrade Go version:

go version  # Check current version
# Upgrade to Go 1.25+

OpenTelemetry Version

The tracing package uses OpenTelemetry SDK v1.x. If you have conflicts with other dependencies:

go mod tidy
go get -u rivaas.dev/tracing

Next Steps

7 - Validation Package

Complete API reference for the rivaas.dev/validation package

This is the API reference for the rivaas.dev/validation package. For learning-focused documentation, see the Validation Guide.

Package Information

Overview

The validation package provides flexible validation for Go structs with support for multiple strategies: struct tags, JSON Schema, and custom interfaces. It’s designed for web applications with features like partial validation for PATCH requests, sensitive data redaction, and detailed error reporting.

import "rivaas.dev/validation"

type User struct {
    Email string `validate:"required,email"`
    Age   int    `validate:"min=18"`
}

err := validation.Validate(ctx, &user)

Key Features

  • Multiple Validation Strategies: Struct tags, JSON Schema, custom interfaces
  • Partial Validation: PATCH request support with presence tracking
  • Thread-Safe: Safe for concurrent use
  • Security: Built-in redaction, nesting limits, memory protection
  • Structured Errors: Field-level errors with codes and metadata
  • Extensible: Custom tags, validators, and error messages

Package Architecture

graph TB
    User[User Code] --> API[Public API]:::info
    
    API --> Validate[Validate Functions]
    API --> Validator[Validator Type]
    API --> Presence[Presence Tracking]
    
    Validate --> Strategy[Strategy Selection]:::warning
    Validator --> Strategy
    
    Strategy --> Tags[Struct Tags]
    Strategy --> Schema[JSON Schema]
    Strategy --> Interface[Custom Interfaces]
    
    Tags --> Errors[Error Collection]
    Schema --> Errors
    Interface --> Errors
    
    Errors --> ErrorType[Error/FieldError]:::danger
    
    Presence --> Partial[Partial Validation]
    Partial --> Strategy
    
    classDef default fill:#F8FAF9,stroke:#1E6F5C,color:#1F2A27
    classDef info fill:#D1ECF1,stroke:#17A2B8,color:#1F2A27
    classDef warning fill:#FFF3CD,stroke:#FFC107,color:#1F2A27
    classDef danger fill:#F8D7DA,stroke:#DC3545,color:#1F2A27

Quick Navigation

API Reference

Core types, functions, and validation methods.

View →

Options

Configuration options and validator settings.

View →

Interfaces

Custom validation interfaces and providers.

View →

Strategies

Validation strategy selection and priority.

View →

Troubleshooting

Common validation issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Core API

Package-Level Functions

Simple validation without creating a validator instance:

// Validate with default configuration
func Validate(ctx context.Context, v any, opts ...Option) error

// Validate only present fields (PATCH requests)
func ValidatePartial(ctx context.Context, v any, pm PresenceMap, opts ...Option) error

// Compute which fields are present in JSON
func ComputePresence(rawJSON []byte) (PresenceMap, error)

Validator Type

Create configured validator instances for reuse:

// Create validator (returns error on invalid config)
func New(opts ...Option) (*Validator, error)

// Create validator (panics on invalid config)
func MustNew(opts ...Option) *Validator

// Validator methods
func (v *Validator) Validate(ctx context.Context, val any, opts ...Option) error
func (v *Validator) ValidatePartial(ctx context.Context, val any, pm PresenceMap, opts ...Option) error

Error Types

Structured validation errors:

// Main error type with multiple field errors
type Error struct {
    Fields    []FieldError
    Truncated bool
}

// Individual field error
type FieldError struct {
    Path    string         // JSON path (e.g., "items.2.price")
    Code    string         // Error code (e.g., "tag.required")
    Message string         // Human-readable message
    Meta    map[string]any // Additional metadata
}

Interfaces

Implement these for custom validation:

// Simple custom validation
type ValidatorInterface interface {
    Validate() error
}

// Context-aware custom validation
type ValidatorWithContext interface {
    ValidateContext(context.Context) error
}

// JSON Schema provider
type JSONSchemaProvider interface {
    JSONSchema() (id, schema string)
}

// Redactor for sensitive fields
type Redactor func(path string) bool

Validation Strategies

The package supports three strategies with automatic selection:

Priority Order:

  1. Interface methods (Validate() / ValidateContext())
  2. Struct tags (validate:"...")
  3. JSON Schema (JSONSchemaProvider)
// Automatic strategy selection
err := validation.Validate(ctx, &user)

// Explicit strategy
err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

// Run all strategies
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

Configuration Options

Validator Creation Options

validator := validation.MustNew(
    validation.WithMaxErrors(10),              // Limit errors returned
    validation.WithMaxCachedSchemas(2048),     // Schema cache size
    validation.WithRedactor(redactorFunc),     // Redact sensitive fields
    validation.WithCustomTag("phone", phoneValidator), // Custom tag
    validation.WithMessages(messageMap),       // Custom error messages
    validation.WithMessageFunc("min", minFunc), // Dynamic messages
)

Per-Call Options

err := validator.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
    validation.WithPartial(true),
    validation.WithPresence(presenceMap),
    validation.WithMaxErrors(5),
    validation.WithCustomValidator(customFunc),
)

Usage Patterns

Basic Validation

type User struct {
    Email string `validate:"required,email"`
    Age   int    `validate:"min=18"`
}

user := User{Email: "test@example.com", Age: 25}
if err := validation.Validate(ctx, &user); err != nil {
    var verr *validation.Error
    if errors.As(err, &verr) {
        for _, fieldErr := range verr.Fields {
            fmt.Printf("%s: %s\n", fieldErr.Path, fieldErr.Message)
        }
    }
}

Partial Validation (PATCH)

rawJSON := []byte(`{"email": "new@example.com"}`)

presence, _ := validation.ComputePresence(rawJSON)
var req UpdateUserRequest
json.Unmarshal(rawJSON, &req)

err := validation.ValidatePartial(ctx, &req, presence)

Custom Validator

validator := validation.MustNew(
    validation.WithCustomTag("phone", func(fl validator.FieldLevel) bool {
        return phoneRegex.MatchString(fl.Field().String())
    }),
    validation.WithRedactor(func(path string) bool {
        return strings.Contains(path, "password")
    }),
)

err := validator.Validate(ctx, &user)

Performance Characteristics

  • First validation: ~500ns overhead for reflection
  • Subsequent validations: ~50ns overhead (cache lookup)
  • Schema caching: LRU with configurable size (default 1024)
  • Thread-safe: All operations safe for concurrent use
  • Zero allocation: Field paths cached per type

Integration

With net/http

func Handler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)
    
    if err := validation.Validate(r.Context(), &req); err != nil {
        http.Error(w, err.Error(), http.StatusUnprocessableEntity)
        return
    }
    // Process request
}

With rivaas.dev/router

func Handler(c *router.Context) error {
    var req CreateUserRequest
    c.BindJSON(&req)
    
    if err := validation.Validate(c.Request().Context(), &req); err != nil {
        return c.JSON(http.StatusUnprocessableEntity, err)
    }
    return c.JSON(http.StatusOK, processRequest(req))
}

With rivaas.dev/app

func Handler(c *app.Context) error {
    var req CreateUserRequest
    if err := c.Bind(&req); err != nil {
        return err // Automatically validated and handled
    }
    return c.JSON(http.StatusOK, processRequest(req))
}

Version Compatibility

The validation package follows semantic versioning:

  • v1.x: Stable API, backward compatible
  • v2.x: Major changes, may require code updates

See Also


For step-by-step guides and tutorials, see the Validation Guide.

For real-world examples, see the Examples page.

7.1 - API Reference

Core types, functions, and methods

Complete API reference for the validation package’s core types, functions, and methods.

Package-Level Functions

Validate

func Validate(ctx context.Context, v any, opts ...Option) error

Validates a value using the default validator. Returns nil if validation passes, or *Error if validation fails.

Parameters:

  • ctx - Context passed to ValidatorWithContext implementations.
  • v - The value to validate. Typically a pointer to a struct.
  • opts - Optional per-call configuration options.

Returns:

  • nil on success.
  • *Error with field-level errors on failure.

Example:

err := validation.Validate(ctx, &user)
if err != nil {
    var verr *validation.Error
    if errors.As(err, &verr) {
        // Handle validation errors
    }
}

ValidatePartial

func ValidatePartial(ctx context.Context, v any, pm PresenceMap, opts ...Option) error

Validates only fields present in the PresenceMap. Useful for PATCH requests where only provided fields should be validated.

Parameters:

  • ctx - Context for validation.
  • v - The value to validate.
  • pm - Map of present fields.
  • opts - Optional configuration options.

Example:

presence, _ := validation.ComputePresence(rawJSON)
err := validation.ValidatePartial(ctx, &req, presence)

ComputePresence

func ComputePresence(rawJSON []byte) (PresenceMap, error)

Analyzes raw JSON and returns a map of present field paths. Used for partial validation.

Parameters:

  • rawJSON - Raw JSON bytes.

Returns:

  • PresenceMap - Map of field paths to true.
  • error - If JSON is invalid.

Example:

rawJSON := []byte(`{"email": "test@example.com", "age": 25}`)
presence, err := validation.ComputePresence(rawJSON)
// presence = {"email": true, "age": true}

Validator Type

New

func New(opts ...Option) (*Validator, error)

Creates a new Validator with the given options. Returns an error if configuration is invalid.

Parameters:

  • opts - Configuration options

Returns:

  • *Validator - Configured validator instance
  • error - If configuration is invalid

Example:

validator, err := validation.New(
    validation.WithMaxErrors(10),
    validation.WithRedactor(redactor),
)
if err != nil {
    return fmt.Errorf("failed to create validator: %w", err)
}

MustNew

func MustNew(opts ...Option) *Validator

Creates a new Validator with the given options. Panics if configuration is invalid. Use in main() or init() where panic on startup is acceptable.

Parameters:

  • opts - Configuration options

Returns:

  • *Validator - Configured validator instance

Panics:

  • If configuration is invalid

Example:

var validator = validation.MustNew(
    validation.WithMaxErrors(10),
    validation.WithRedactor(redactor),
)

Validator.Validate

func (v *Validator) Validate(ctx context.Context, val any, opts ...Option) error

Validates a value using this validator’s configuration. Per-call options override the validator’s base configuration.

Parameters:

  • ctx - Context for validation
  • val - The value to validate
  • opts - Optional per-call configuration overrides

Returns:

  • nil on success
  • *Error on failure

Example:

err := validator.Validate(ctx, &user,
    validation.WithMaxErrors(5), // Override base config
)

Validator.ValidatePartial

func (v *Validator) ValidatePartial(ctx context.Context, val any, pm PresenceMap, opts ...Option) error

Validates only fields present in the PresenceMap using this validator’s configuration.

Parameters:

  • ctx - Context for validation
  • val - The value to validate
  • pm - Map of present fields
  • opts - Optional configuration overrides

Returns:

  • nil on success
  • *Error on failure

Error Types

Error

type Error struct {
    Fields    []FieldError
    Truncated bool
}

Main validation error type containing multiple field errors.

Fields:

  • Fields - Slice of field-level errors
  • Truncated - True if errors were truncated due to maxErrors limit

Methods:

func (e Error) Error() string
func (e Error) Unwrap() error                    // Returns ErrValidation
func (e Error) HTTPStatus() int                  // Returns 422
func (e Error) Code() string                     // Returns "validation_error"
func (e Error) Details() any                     // Returns Fields
func (e *Error) Add(path, code, message string, meta map[string]any)
func (e *Error) AddError(err error)
func (e Error) HasErrors() bool
func (e Error) HasCode(code string) bool
func (e Error) Has(path string) bool
func (e Error) GetField(path string) *FieldError
func (e *Error) Sort()

Example:

var verr *validation.Error
if errors.As(err, &verr) {
    fmt.Printf("Found %d errors\n", len(verr.Fields))
    
    if verr.Truncated {
        fmt.Println("(more errors exist)")
    }
    
    if verr.Has("email") {
        fmt.Println("Email field has an error")
    }
}

FieldError

type FieldError struct {
    Path    string
    Code    string
    Message string
    Meta    map[string]any
}

Individual field validation error.

Fields:

  • Path - JSON path to the field (e.g., "items.2.price")
  • Code - Stable error code (e.g., "tag.required", "schema.type")
  • Message - Human-readable error message
  • Meta - Additional metadata (tag, param, value, etc.)

Methods:

func (e FieldError) Error() string    // Returns "path: message"
func (e FieldError) Unwrap() error    // Returns ErrValidation
func (e FieldError) HTTPStatus() int  // Returns 422

Example:

for _, fieldErr := range verr.Fields {
    fmt.Printf("Field: %s\n", fieldErr.Path)
    fmt.Printf("Code: %s\n", fieldErr.Code)
    fmt.Printf("Message: %s\n", fieldErr.Message)
    
    if tag, ok := fieldErr.Meta["tag"].(string); ok {
        fmt.Printf("Tag: %s\n", tag)
    }
}

PresenceMap Type

type PresenceMap map[string]bool

Tracks which fields are present in a request body. Keys are JSON field paths.

Methods:

func (pm PresenceMap) Has(path string) bool
func (pm PresenceMap) HasPrefix(prefix string) bool
func (pm PresenceMap) LeafPaths() []string

Example:

presence := PresenceMap{
    "email": true,
    "address": true,
    "address.city": true,
}

if presence.Has("email") {
    // Email was provided
}

if presence.HasPrefix("address") {
    // At least one address field was provided
}

leaves := presence.LeafPaths()
// Returns: ["email", "address.city"]
// (address is excluded as it has children)

Strategy Type

type Strategy int

const (
    StrategyAuto Strategy = iota
    StrategyTags
    StrategyJSONSchema
    StrategyInterface
)

Defines the validation approach to use.

Constants:

  • StrategyAuto - Automatically select best strategy (default)
  • StrategyTags - Use struct tag validation
  • StrategyJSONSchema - Use JSON Schema validation
  • StrategyInterface - Use interface methods (Validate() / ValidateContext())

Example:

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Sentinel Errors

var (
    ErrValidation                 = errors.New("validation")
    ErrCannotValidateNilValue     = errors.New("cannot validate nil value")
    ErrCannotValidateInvalidValue = errors.New("cannot validate invalid value")
    ErrUnknownValidationStrategy  = errors.New("unknown validation strategy")
    ErrValidationFailed           = errors.New("validation failed")
    ErrInvalidType                = errors.New("invalid type")
)

Sentinel errors for error checking with errors.Is.

Example:

if errors.Is(err, validation.ErrValidation) {
    // This is a validation error
}

Type Definitions

Option

type Option func(*config)

Functional option for configuring validation. See Options for all available options.

Redactor

type Redactor func(path string) bool

Function that determines if a field should be redacted in error messages. Returns true if the field at the given path should have its value hidden.

Example:

redactor := func(path string) bool {
    return strings.Contains(path, "password") ||
           strings.Contains(path, "token")
}

validator := validation.MustNew(
    validation.WithRedactor(redactor),
)

MessageFunc

type MessageFunc func(param string, kind reflect.Kind) string

Generates dynamic error messages for parameterized validation tags. Receives the tag parameter and field’s reflect.Kind.

Example:

minMessage := func(param string, kind reflect.Kind) string {
    if kind == reflect.String {
        return fmt.Sprintf("must be at least %s characters", param)
    }
    return fmt.Sprintf("must be at least %s", param)
}

validator := validation.MustNew(
    validation.WithMessageFunc("min", minMessage),
)

Constants

const (
    defaultMaxCachedSchemas = 1024
    maxRecursionDepth      = 100
)
  • defaultMaxCachedSchemas - Default JSON Schema cache size
  • maxRecursionDepth - Maximum nesting depth for ComputePresence

Thread Safety

All types and functions in the validation package are safe for concurrent use by multiple goroutines:

  • Validator instances are thread-safe
  • Package-level functions use a shared thread-safe default validator
  • PresenceMap is read-only after creation (safe for concurrent reads)

Performance

  • First validation of a type: ~500ns overhead for reflection
  • Subsequent validations: ~50ns overhead (cache lookup)
  • Schema compilation: Cached with LRU eviction
  • Path computation: Cached per type
  • Zero allocations: For cached types

Next Steps

7.2 - Options

Configuration options for validators

Complete reference for all configuration options (With* functions) available in the validation package.

Option Types

Options can be used in two ways:

  1. Validator Creation: Pass to New() or MustNew(). Applies to all validations.
  2. Per-Call: Pass to Validate() or ValidatePartial(). Applies to that call only.
// Validator creation options
validator := validation.MustNew(
    validation.WithMaxErrors(10),
    validation.WithRedactor(redactor),
)

// Per-call options (override validator config)
err := validator.Validate(ctx, &req,
    validation.WithMaxErrors(5), // Overrides the 10 from creation
    validation.WithStrategy(validation.StrategyTags),
)

Strategy Options

WithStrategy

func WithStrategy(strategy Strategy) Option

Sets the validation strategy to use.

Values:

  • StrategyAuto - Automatically select best strategy. This is the default.
  • StrategyTags - Use struct tags only.
  • StrategyJSONSchema - Use JSON Schema only.
  • StrategyInterface - Use interface methods only.

Example:

err := validation.Validate(ctx, &req,
    validation.WithStrategy(validation.StrategyTags),
)

WithRunAll

func WithRunAll(runAll bool) Option

Runs all applicable validation strategies and aggregates errors. By default, validation stops at the first successful strategy.

Example:

err := validation.Validate(ctx, &req,
    validation.WithRunAll(true),
)

WithRequireAny

func WithRequireAny(require bool) Option

When used with WithRunAll(true), succeeds if at least one strategy passes (OR logic).

Example:

// Pass if ANY strategy succeeds
err := validation.Validate(ctx, &req,
    validation.WithRunAll(true),
    validation.WithRequireAny(true),
)

Partial Validation Options

WithPartial

func WithPartial(partial bool) Option

Enables partial validation mode for PATCH requests. Validates only present fields and ignores “required” constraints for absent fields.

Example:

err := validation.Validate(ctx, &req,
    validation.WithPartial(true),
    validation.WithPresence(presenceMap),
)

WithPresence

func WithPresence(presence PresenceMap) Option

Sets the presence map for partial validation. Tracks which fields were provided in the request body.

Example:

presence, _ := validation.ComputePresence(rawJSON)
err := validation.Validate(ctx, &req,
    validation.WithPresence(presence),
    validation.WithPartial(true),
)

Error Limit Options

WithMaxErrors

func WithMaxErrors(maxErrors int) Option

Limits the number of errors returned. Set to 0 for unlimited errors (default).

Example:

// Return at most 5 errors
err := validation.Validate(ctx, &req,
    validation.WithMaxErrors(5),
)

var verr *validation.Error
if errors.As(err, &verr) {
    if verr.Truncated {
        fmt.Println("More errors exist")
    }
}

WithMaxFields

func WithMaxFields(maxFields int) Option

Sets the maximum number of fields to validate in partial mode. Prevents pathological inputs with extremely large presence maps. Set to 0 to use the default (10000).

Example:

validator := validation.MustNew(
    validation.WithMaxFields(5000),
)

Cache Options

WithMaxCachedSchemas

func WithMaxCachedSchemas(maxCachedSchemas int) Option

Sets the maximum number of JSON schemas to cache. Uses LRU eviction when limit is reached. Set to 0 to use the default (1024).

Example:

validator := validation.MustNew(
    validation.WithMaxCachedSchemas(2048),
)

Security Options

WithRedactor

func WithRedactor(redactor Redactor) Option

Sets a redactor function to hide sensitive values in error messages. The redactor returns true if the field at the given path should be redacted.

Example:

redactor := func(path string) bool {
    return strings.Contains(path, "password") ||
           strings.Contains(path, "token") ||
           strings.Contains(path, "secret")
}

validator := validation.MustNew(
    validation.WithRedactor(redactor),
)

WithDisallowUnknownFields

func WithDisallowUnknownFields(disallow bool) Option

Rejects JSON with unknown fields (typo detection). When enabled, causes strict JSON binding to reject requests with fields not defined in the struct.

Example:

err := validation.Validate(ctx, &req,
    validation.WithDisallowUnknownFields(true),
)

Context Options

WithContext

func WithContext(ctx context.Context) Option

Overrides the context used for validation. Useful when you need a different context than the one passed to Validate().

Note: In most cases, you should pass the context directly to Validate(). This option exists for advanced use cases.

Example:

err := validator.Validate(requestCtx, &req,
    validation.WithContext(backgroundCtx),
)

Custom Validation Options

WithCustomSchema

func WithCustomSchema(id, schema string) Option

Sets a custom JSON Schema for validation. This overrides any schema provided by the JSONSchemaProvider interface.

Example:

customSchema := `{
    "type": "object",
    "properties": {
        "email": {"type": "string", "format": "email"}
    }
}`

err := validation.Validate(ctx, &req,
    validation.WithCustomSchema("custom-user", customSchema),
)

WithCustomValidator

func WithCustomValidator(fn func(any) error) Option

Sets a custom validation function that runs before any other validation strategies.

Example:

err := validation.Validate(ctx, &req,
    validation.WithCustomValidator(func(v any) error {
        req := v.(*UserRequest)
        if req.Age < 18 {
            return errors.New("must be 18 or older")
        }
        return nil
    }),
)

WithCustomTag

func WithCustomTag(name string, fn validator.Func) Option

Registers a custom validation tag for use in struct tags. Custom tags are registered when the validator is created.

Example:

phoneValidator := func(fl validator.FieldLevel) bool {
    return phoneRegex.MatchString(fl.Field().String())
}

validator := validation.MustNew(
    validation.WithCustomTag("phone", phoneValidator),
)

type User struct {
    Phone string `validate:"phone"`
}

Error Message Options

WithMessages

func WithMessages(messages map[string]string) Option

Sets static error messages for validation tags. Messages override the default English messages for specified tags.

Example:

validator := validation.MustNew(
    validation.WithMessages(map[string]string{
        "required": "cannot be empty",
        "email":    "invalid email format",
        "min":      "value too small",
    }),
)

WithMessageFunc

func WithMessageFunc(tag string, fn MessageFunc) Option

Sets a dynamic message generator for a parameterized tag. Use for tags like “min”, “max”, “len” that include parameters.

Example:

minMessage := func(param string, kind reflect.Kind) string {
    if kind == reflect.String {
        return fmt.Sprintf("must be at least %s characters", param)
    }
    return fmt.Sprintf("must be at least %s", param)
}

validator := validation.MustNew(
    validation.WithMessageFunc("min", minMessage),
)

Field Name Options

WithFieldNameMapper

func WithFieldNameMapper(mapper func(string) string) Option

Sets a function to transform field names in error messages. Useful for localization or custom naming conventions.

Example:

validator := validation.MustNew(
    validation.WithFieldNameMapper(func(name string) string {
        // Convert snake_case to Title Case
        return strings.Title(strings.ReplaceAll(name, "_", " "))
    }),
)

Options Summary

Validator Creation Options

Options that should be set when creating a validator (affect all validations):

OptionPurpose
WithMaxErrorsLimit total errors returned
WithMaxFieldsLimit fields in partial validation
WithMaxCachedSchemasSchema cache size
WithRedactorRedact sensitive fields
WithCustomTagRegister custom validation tag
WithMessagesCustom error messages
WithMessageFuncDynamic error messages
WithFieldNameMapperTransform field names

Per-Call Options

Options commonly used per-call (override validator config):

OptionPurpose
WithStrategyChoose validation strategy
WithRunAllRun all strategies
WithRequireAnyOR logic with WithRunAll
WithPartialEnable partial validation
WithPresenceSet presence map
WithMaxErrorsOverride error limit
WithCustomValidatorAdd custom validator
WithCustomSchemaOverride JSON Schema
WithDisallowUnknownFieldsReject unknown fields
WithContextOverride context

Usage Patterns

Creating Configured Validator

validator := validation.MustNew(
    // Security
    validation.WithRedactor(sensitiveRedactor),
    validation.WithMaxErrors(20),
    validation.WithMaxFields(5000),
    
    // Custom validation
    validation.WithCustomTag("phone", phoneValidator),
    validation.WithCustomTag("username", usernameValidator),
    
    // Error messages
    validation.WithMessages(map[string]string{
        "required": "is required",
        "email":    "must be a valid email",
    }),
)

Per-Call Overrides

// Use tags strategy only
err := validator.Validate(ctx, &req,
    validation.WithStrategy(validation.StrategyTags),
    validation.WithMaxErrors(5),
)

// Partial validation
err := validator.Validate(ctx, &req,
    validation.WithPartial(true),
    validation.WithPresence(presence),
)

// Custom validation
err := validator.Validate(ctx, &req,
    validation.WithCustomValidator(complexBusinessLogic),
)

Next Steps

7.3 - Interfaces

Custom validation interfaces

Complete reference for validation interfaces that can be implemented for custom validation logic.

ValidatorInterface

type ValidatorInterface interface {
    Validate() error
}

Implement this interface for simple custom validation without context.

When to Use

  • Simple validation rules that don’t need external data
  • Business logic validation
  • Cross-field validation within the struct

Implementation

type User struct {
    Email string
    Name  string
}

func (u *User) Validate() error {
    if !strings.Contains(u.Email, "@") {
        return errors.New("email must contain @")
    }
    if len(u.Name) < 2 {
        return errors.New("name must be at least 2 characters")
    }
    return nil
}

Returning Structured Errors

Return *validation.Error for detailed field-level errors:

func (u *User) Validate() error {
    var verr validation.Error
    
    if !strings.Contains(u.Email, "@") {
        verr.Add("email", "format", "must contain @", nil)
    }
    
    if len(u.Name) < 2 {
        verr.Add("name", "length", "must be at least 2 characters", nil)
    }
    
    if verr.HasErrors() {
        return &verr
    }
    return nil
}

Pointer vs Value Receivers

Both are supported:

// Pointer receiver (can modify struct)
func (u *User) Validate() error {
    u.Email = strings.ToLower(u.Email) // Normalize
    return validateEmail(u.Email)
}

// Value receiver (read-only)
func (u User) Validate() error {
    return validateEmail(u.Email)
}

Use pointer receivers when you need to modify the struct during validation (normalization, etc.).

ValidatorWithContext

type ValidatorWithContext interface {
    ValidateContext(context.Context) error
}

Implement this interface for context-aware validation that needs access to request-scoped data or external services.

When to Use

  • Database lookups (uniqueness checks, existence validation)
  • Tenant-specific validation rules
  • Rate limiting or quota checks
  • External service calls
  • Request-scoped data access

Implementation

type User struct {
    Username string
    Email    string
    TenantID string
}

func (u *User) ValidateContext(ctx context.Context) error {
    // Get services from context
    db := ctx.Value("db").(*sql.DB)
    tenant := ctx.Value("tenant").(string)
    
    // Tenant validation
    if u.TenantID != tenant {
        return errors.New("user does not belong to this tenant")
    }
    
    // Database validation
    var exists bool
    err := db.QueryRowContext(ctx,
        "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)",
        u.Username,
    ).Scan(&exists)
    
    if err != nil {
        return fmt.Errorf("failed to check username: %w", err)
    }
    
    if exists {
        return errors.New("username already taken")
    }
    
    return nil
}

Context Values

Access data from context:

func (u *User) ValidateContext(ctx context.Context) error {
    // Database connection
    db := ctx.Value("db").(*sql.DB)
    
    // Current user/tenant
    currentUser := ctx.Value("user_id").(string)
    tenant := ctx.Value("tenant").(string)
    
    // Request metadata
    requestID := ctx.Value("request_id").(string)
    
    // Use in validation logic
    return validateWithContext(db, u, tenant)
}

Cancellation Support

Respect context cancellation for long-running validations:

func (u *User) ValidateContext(ctx context.Context) error {
    // Check cancellation before expensive operation
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    
    // Expensive validation
    return checkUsernameUniqueness(ctx, u.Username)
}

JSONSchemaProvider

type JSONSchemaProvider interface {
    JSONSchema() (id, schema string)
}

Implement this interface to provide a JSON Schema for validation.

When to Use

  • Portable validation rules (shared with frontend/documentation)
  • Complex validation logic without code
  • RFC-compliant validation
  • Schema versioning

Implementation

type Product struct {
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Category string  `json:"category"`
}

func (p Product) JSONSchema() (id, schema string) {
    return "product-v1", `{
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
            },
            "price": {
                "type": "number",
                "minimum": 0,
                "exclusiveMinimum": true
            },
            "category": {
                "type": "string",
                "enum": ["electronics", "clothing", "books"]
            }
        },
        "required": ["name", "price", "category"],
        "additionalProperties": false
    }`
}

Schema ID

The ID is used for caching:

func (p Product) JSONSchema() (id, schema string) {
    return "product-v1", schemaString
    //     ^^^^^^^^^^^ Used as cache key
}

Use versioned IDs (e.g., "product-v1", "product-v2") to invalidate cache when schema changes.

Schema Formats

Supported formats:

  • email - Email address
  • uri / url - URL
  • hostname - DNS hostname
  • ipv4 / ipv6 - IP addresses
  • date - Date (YYYY-MM-DD)
  • date-time - RFC3339 date-time
  • uuid - UUID

Embedded Schemas

For complex schemas, consider embedding:

import _ "embed"

//go:embed user_schema.json
var userSchemaJSON string

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", userSchemaJSON
}

Redactor

type Redactor func(path string) bool

Function that determines if a field should be redacted in error messages.

When to Use

  • Protecting passwords, tokens, secrets
  • Hiding credit card numbers, SSNs
  • Redacting PII (personally identifiable information)
  • Compliance requirements (GDPR, PCI-DSS)

Implementation

func sensitiveFieldRedactor(path string) bool {
    pathLower := strings.ToLower(path)
    
    // Password fields
    if strings.Contains(pathLower, "password") {
        return true
    }
    
    // Tokens and secrets
    if strings.Contains(pathLower, "token") ||
       strings.Contains(pathLower, "secret") ||
       strings.Contains(pathLower, "key") {
        return true
    }
    
    // Payment information
    if strings.Contains(pathLower, "card") ||
       strings.Contains(pathLower, "cvv") ||
       strings.Contains(pathLower, "credit") {
        return true
    }
    
    return false
}

validator := validation.MustNew(
    validation.WithRedactor(sensitiveFieldRedactor),
)

Path-Based Redaction

Redact specific paths:

func pathRedactor(path string) bool {
    redactedPaths := map[string]bool{
        "user.password":          true,
        "payment.card_number":    true,
        "payment.cvv":            true,
        "auth.refresh_token":     true,
    }
    return redactedPaths[path]
}

Nested Field Redaction

func nestedRedactor(path string) bool {
    // Redact all fields under payment.*
    if strings.HasPrefix(path, "payment.") {
        return true
    }
    
    // Redact specific nested field
    if strings.HasPrefix(path, "user.credentials.") {
        return true
    }
    
    return false
}

Interface Priority

When multiple interfaces are implemented, they have different priorities:

Priority Order:

  1. ValidatorWithContext / ValidatorInterface (highest)
  2. Struct tags (validate:"...")
  3. JSONSchemaProvider (lowest)
type User struct {
    Email string `validate:"required,email"` // Priority 2
}

func (u User) JSONSchema() (id, schema string) {
    // Priority 3 (lowest)
    return "user-v1", `{...}`
}

func (u *User) Validate() error {
    // Priority 1 (highest) - this runs instead of tags/schema
    return customValidation(u.Email)
}

Override priority with explicit strategy:

// Skip Validate() method, use tags
err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Combining Interfaces

Run all strategies with WithRunAll:

type User struct {
    Email string `validate:"required,email"` // Struct tags
}

func (u User) JSONSchema() (id, schema string) {
    // JSON Schema
    return "user-v1", `{...}`
}

func (u *User) Validate() error {
    // Interface method
    return businessLogic(u)
}

// Run all three strategies
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

Best Practices

1. Choose the Right Interface

// Simple validation - ValidatorInterface
func (u *User) Validate() error {
    return validateEmail(u.Email)
}

// Needs external data - ValidatorWithContext
func (u *User) ValidateContext(ctx context.Context) error {
    db := ctx.Value("db").(*sql.DB)
    return checkUniqueness(ctx, db, u.Email)
}

2. Return Structured Errors

// Good
func (u *User) Validate() error {
    var verr validation.Error
    verr.Add("email", "invalid", "must be valid email", nil)
    return &verr
}

// Bad
func (u *User) Validate() error {
    return errors.New("email invalid")
}

3. Use Context Safely

func (u *User) ValidateContext(ctx context.Context) error {
    db, ok := ctx.Value("db").(*sql.DB)
    if !ok {
        return errors.New("database not available in context")
    }
    return validateWithDB(ctx, db, u)
}

4. Document Custom Validation

// ValidateContext validates the user against business rules:
// - Username must be unique within tenant
// - Email domain must be allowed for tenant
// - User must not exceed account limits
func (u *User) ValidateContext(ctx context.Context) error {
    // Implementation
}

Testing

Testing ValidatorInterface

func TestUserValidation(t *testing.T) {
    tests := []struct {
        name    string
        user    User
        wantErr bool
    }{
        {"valid", User{Email: "test@example.com"}, false},
        {"invalid", User{Email: "invalid"}, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.user.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Testing ValidatorWithContext

func TestUserValidationWithContext(t *testing.T) {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "db", mockDB)
    ctx = context.WithValue(ctx, "tenant", "test-tenant")
    
    user := User{Username: "testuser"}
    err := user.ValidateContext(ctx)
    
    if err != nil {
        t.Errorf("ValidateContext() error = %v", err)
    }
}

Next Steps

7.4 - Validation Strategies

Strategy selection and priority

Complete reference for validation strategies, automatic selection, and priority order.

Overview

The validation package supports three validation strategies that can be used individually or combined:

  1. Interface Methods - Validate() / ValidateContext()
  2. Struct Tags - validate:"..." tags
  3. JSON Schema - JSONSchemaProvider interface

Strategy Types

StrategyAuto

const StrategyAuto Strategy = iota

Automatically selects the best strategy based on the type (default behavior).

Priority Order:

  1. Interface methods (highest priority)
  2. Struct tags
  3. JSON Schema (lowest priority)

Example:

// Uses automatic strategy selection
err := validation.Validate(ctx, &user)

StrategyTags

const StrategyTags Strategy = ...

Uses struct tag validation with go-playground/validator.

Requirements:

  • Struct type
  • Fields with validate tags

Example:

type User struct {
    Email string `validate:"required,email"`
}

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

StrategyJSONSchema

const StrategyJSONSchema Strategy = ...

Uses JSON Schema validation (RFC-compliant).

Requirements:

  • Type implements JSONSchemaProvider interface, OR
  • Custom schema provided with WithCustomSchema

Example:

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}`
}

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyJSONSchema),
)

StrategyInterface

const StrategyInterface Strategy = ...

Uses custom interface methods (Validate() or ValidateContext()).

Requirements:

  • Type implements ValidatorInterface or ValidatorWithContext

Example:

func (u *User) Validate() error {
    return customValidation(u)
}

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyInterface),
)

Automatic Strategy Selection

Selection Process

When StrategyAuto is used (default), the validator checks strategies in priority order:

graph TD
    Start[Start Validation] --> CheckInterface{Implements<br/>ValidatorInterface or<br/>ValidatorWithContext?}
    
    CheckInterface -->|Yes| UseInterface[Use Interface Strategy]:::success
    CheckInterface -->|No| CheckTags{Has validate<br/>tags?}
    
    CheckTags -->|Yes| UseTags[Use Tags Strategy]:::warning
    CheckTags -->|No| CheckSchema{Implements<br/>JSONSchemaProvider?}
    
    CheckSchema -->|Yes| UseSchema[Use JSON Schema Strategy]:::info
    CheckSchema -->|No| DefaultTags[Default to Tags]
    
    UseInterface --> Done[Validate]
    UseTags --> Done
    UseSchema --> Done
    DefaultTags --> Done
    
    classDef default fill:#F8FAF9,stroke:#1E6F5C,color:#1F2A27
    classDef info fill:#D1ECF1,stroke:#17A2B8,color:#1F2A27
    classDef success fill:#D4EDDA,stroke:#28A745,color:#1F2A27
    classDef warning fill:#FFF3CD,stroke:#FFC107,color:#1F2A27

Applicability Checks

A strategy is considered “applicable” if:

Interface Strategy:

  • Type implements ValidatorInterface or ValidatorWithContext
  • Checks both value and pointer receivers

Tags Strategy:

  • Type is a struct
  • At least one field has a validate tag

JSON Schema Strategy:

  • Type implements JSONSchemaProvider, OR
  • Custom schema provided with WithCustomSchema

Priority Examples

// Example 1: Only interface method
type User struct {
    Email string
}

func (u *User) Validate() error {
    return validateEmail(u.Email)
}

// Uses: StrategyInterface (highest priority)
validation.Validate(ctx, &user)
// Example 2: Both interface and tags
type User struct {
    Email string `validate:"required,email"`
}

func (u *User) Validate() error {
    return customLogic(u.Email)
}

// Uses: StrategyInterface (interface has priority over tags)
validation.Validate(ctx, &user)
// Example 3: All three strategies
type User struct {
    Email string `validate:"required,email"`
}

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}`
}

func (u *User) Validate() error {
    return customLogic(u.Email)
}

// Uses: StrategyInterface (highest priority)
validation.Validate(ctx, &user)

Explicit Strategy Selection

Override automatic selection with WithStrategy:

type User struct {
    Email string `validate:"required,email"` // Has tags
}

func (u *User) Validate() error {
    return customLogic(u.Email) // Has interface method
}

// Force use of tags (skip interface method)
err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Running Multiple Strategies

WithRunAll

Run all applicable strategies and aggregate errors:

type User struct {
    Email string `validate:"required,email"` // Tags
}

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}` // JSON Schema
}

func (u *User) Validate() error {
    return customLogic(u.Email) // Interface
}

// Run all three strategies
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

// All errors from all strategies are collected
var verr *validation.Error
if errors.As(err, &verr) {
    // verr.Fields contains errors from all strategies
}

WithRequireAny

With WithRunAll, succeed if any one strategy passes (OR logic):

// Pass if ANY strategy succeeds
err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
    validation.WithRequireAny(true),
)

Use Cases:

  • Multiple validation approaches, any one is sufficient
  • Fallback validation strategies
  • Gradual migration between strategies

Strategy Comparison

StrategyAdvantagesDisadvantagesBest For
InterfaceMost flexible, full programmatic controlMore code, not declarativeComplex business logic, database checks
TagsConcise, declarative, well-documentedLimited to supported tagsStandard validation, simple rules
JSON SchemaPortable, language-independentVerbose, learning curveShared validation with frontend

Strategy Patterns

Pattern 1: Tags for Simple, Interface for Complex

type User struct {
    Email    string `validate:"required,email"`
    Username string `validate:"required,min=3,max=20"`
    Age      int    `validate:"required,min=18"`
}

func (u *User) ValidateContext(ctx context.Context) error {
    // Complex validation (database checks, etc.)
    db := ctx.Value("db").(*sql.DB)
    return checkUsernameUnique(ctx, db, u.Username)
}

// Tags validate format, interface validates business rules
validation.Validate(ctx, &user)

Pattern 2: Schema for API, Interface for Internal

func (u User) JSONSchema() (id, schema string) {
    // For external API documentation/validation
    return "user-v1", apiSchema
}

func (u *User) ValidateContext(ctx context.Context) error {
    // Internal business rules
    return validateInternal(ctx, u)
}

// External API: use schema
validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyJSONSchema),
)

// Internal: use interface
validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyInterface),
)

Pattern 3: Progressive Enhancement

// Start with tags
type User struct {
    Email string `validate:"required,email"`
}

// Add interface for complex validation later
func (u *User) ValidateContext(ctx context.Context) error {
    // Complex validation added over time
    return additionalValidation(ctx, u)
}

// Automatically uses interface (higher priority)
validation.Validate(ctx, &user)

Performance Considerations

Strategy Performance

Fastest to Slowest:

  1. Tags - Cached reflection, zero allocation after first use
  2. Interface - Direct method call, user code performance
  3. JSON Schema - Schema compilation (cached), RFC validation

Optimization Tips

// Fast: Use tags for simple validation
type User struct {
    Email string `validate:"required,email"`
}

// Slower: JSON Schema (first time, then cached)
func (u User) JSONSchema() (id, schema string) {
    return "user-v1", complexSchema
}

// Variable: Depends on your implementation
func (u *User) ValidateContext(ctx context.Context) error {
    // Keep this fast - runs every time
    return quickValidation(u)
}

Caching

  • Tags: Struct reflection cached per type
  • JSON Schema: Schemas cached by ID (LRU eviction)
  • Interface: No caching (direct method call)

Error Aggregation

When running multiple strategies:

err := validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

var verr *validation.Error
if errors.As(err, &verr) {
    // Errors are aggregated from all strategies
    for _, fieldErr := range verr.Fields {
        fmt.Printf("%s: %s (from %s strategy)\n",
            fieldErr.Path,
            fieldErr.Message,
            inferStrategy(fieldErr.Code), // tag.*, schema.*, etc.
        )
    }
    
    // Sort for consistent output
    verr.Sort()
}

Error codes indicate strategy:

  • tag.* - From struct tags
  • schema.* - From JSON Schema
  • Custom codes - From interface methods

Best Practices

1. Use Automatic Selection

// Good - let validator choose
validation.Validate(ctx, &user)

// Only override when necessary
validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

2. Single Strategy Per Type

// Good - clear which strategy is used
type User struct {
    Email string `validate:"required,email"`
}

// Confusing - multiple strategies compete
type User struct {
    Email string `validate:"required,email"`
}

func (u User) JSONSchema() (id, schema string) { ... }
func (u *User) Validate() error { ... }

3. Document Strategy Choice

// User validation uses struct tags for simplicity and performance.
// Email format and length are validated declaratively.
type User struct {
    Email string `validate:"required,email,max=255"`
}

4. Use WithRunAll Sparingly

// Most cases: automatic selection is sufficient
validation.Validate(ctx, &user)

// Only when you need to validate with multiple strategies
validation.Validate(ctx, &user,
    validation.WithRunAll(true),
)

Next Steps

7.5 - Troubleshooting

Common issues and solutions

Common issues, solutions, and debugging tips for the validation package.

Validation Not Running

Issue: Validation passes when it should fail

Symptom:

type User struct {
    Email string `validate:"required,email"`
}

user := User{Email: ""} // Should fail
err := validation.Validate(ctx, &user) // err is nil (unexpected)

Possible Causes:

  1. Struct tags not being checked

Check if a higher-priority strategy is being used:

func (u *User) Validate() error {
    return nil // This runs instead of tags
}

Solution: Remove interface method or use WithStrategy:

err := validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)
  1. Wrong tag name
// Wrong
Email string `validation:"required"` // Should be "validate"

// Correct
Email string `validate:"required"`
  1. Validating value instead of pointer
// May not work with pointer receivers
user := User{} // value
user.Validate() // Method might not be found

// Use pointer
user := &User{} // pointer
validation.Validate(ctx, user)

Partial Validation Issues

Issue: Required fields failing in PATCH requests

Symptom:

type UpdateUser struct {
    Email string `validate:"required,email"`
}

// PATCH with only age
err := validation.ValidatePartial(ctx, &req, presence)
// Error: email is required (but it wasn't provided)

Solution: Use omitempty instead of required for PATCH:

type UpdateUser struct {
    Email string `validate:"omitempty,email"` // Not "required"
}

Issue: Presence map not being respected

Symptom:

presence, _ := validation.ComputePresence(rawJSON)
err := validation.Validate(ctx, &req, // Missing WithPresence!
    validation.WithPartial(true),
)

Solution: Always pass presence map:

err := validation.Validate(ctx, &req,
    validation.WithPartial(true),
    validation.WithPresence(presence), // Add this
)

Custom Validation Issues

Issue: Custom tag not working

Symptom:

validator := validation.MustNew(
    validation.WithCustomTag("phone", phoneValidator),
)

type User struct {
    Phone string `validate:"phone"` // Not recognized
}

Possible Causes:

  1. Tag registered on wrong validator
// Registered on custom validator
validator := validation.MustNew(
    validation.WithCustomTag("phone", phoneValidator),
)

// But using package-level function (different validator)
validation.Validate(ctx, &user) // Doesn't have custom tag

Solution: Use the same validator:

validator.Validate(ctx, &user) // Use custom validator
  1. Tag function signature wrong
// Wrong
func phoneValidator(val string) bool { ... }

// Correct
func phoneValidator(fl validator.FieldLevel) bool { ... }

Issue: ValidateContext not being called

Symptom:

func (u *User) ValidateContext(ctx context.Context) error {
    fmt.Println("Never prints")
    return nil
}

Possible Causes:

  1. Wrong receiver type
// Method defined on value
func (u User) ValidateContext(ctx context.Context) error { ... }

// But validating pointer
user := &User{}
validation.Validate(ctx, user) // Method not found

Solution: Use pointer receiver:

func (u *User) ValidateContext(ctx context.Context) error { ... }
  1. Struct tags have priority

If auto-selection chooses tags, interface method isn’t called.

Solution: Explicitly use interface strategy:

validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyInterface),
)

Error Handling Issues

Issue: Can’t access field errors

Symptom:

err := validation.Validate(ctx, &user)
// How do I get field-level errors?

Solution: Use errors.As:

var verr *validation.Error
if errors.As(err, &verr) {
    for _, fieldErr := range verr.Fields {
        fmt.Printf("%s: %s\n", fieldErr.Path, fieldErr.Message)
    }
}

Issue: Sensitive data visible in errors

Symptom:

// Error message contains password value
email: invalid email (value: "password123")

Solution: Use redactor:

validator := validation.MustNew(
    validation.WithRedactor(func(path string) bool {
        return strings.Contains(path, "password")
    }),
)

Performance Issues

Issue: Validation is slow

Possible Causes:

  1. Creating validator on every request
// Bad - creates validator every time
func Handler(w http.ResponseWriter, r *http.Request) {
    validator := validation.MustNew(...) // Slow
    validator.Validate(ctx, &req)
}

Solution: Create once, reuse:

var validator = validation.MustNew(...)

func Handler(w http.ResponseWriter, r *http.Request) {
    validator.Validate(ctx, &req) // Fast
}
  1. JSON Schema not cached
func (u User) JSONSchema() (id, schema string) {
    return "", `{...}` // Empty ID = no caching
}

Solution: Use stable ID:

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}` // Cached
}
  1. Expensive ValidateContext
func (u *User) ValidateContext(ctx context.Context) error {
    // Expensive operation on every validation
    return checkWithExternalAPI(u.Email)
}

Solution: Optimize or cache:

func (u *User) ValidateContext(ctx context.Context) error {
    // Fast checks first
    if !basicValidation(u.Email) {
        return errors.New("invalid format")
    }
    
    // Expensive check last
    return checkWithExternalAPI(u.Email)
}

JSON Schema Issues

Issue: Schema validation not working

Symptom:

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}`
}

// But validation doesn't use schema

Possible Causes:

  1. Higher priority strategy exists
type User struct {
    Email string `validate:"email"` // Tags have higher priority
}

func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{...}` // Not used
}

Solution: Use explicit strategy:

validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyJSONSchema),
)
  1. Invalid JSON Schema
func (u User) JSONSchema() (id, schema string) {
    return "user-v1", `{ invalid json }` // Parse error
}

Solution: Validate schema syntax:

// Use online validator: https://www.jsonschemavalidator.net/

Context Issues

Issue: Context values not available

Symptom:

func (u *User) ValidateContext(ctx context.Context) error {
    db := ctx.Value("db") // db is nil
    // ...
}

Solution: Ensure values are in context:

ctx = context.WithValue(ctx, "db", db)
err := validation.Validate(ctx, &user)

Issue: Wrong context being used

Symptom:

err := validation.Validate(ctx1, &user,
    validation.WithContext(ctx2), // Overrides ctx1
)

Solution: Don’t use WithContext unless necessary:

// Just pass the right context
err := validation.Validate(correctCtx, &user)

Module and Import Issues

Issue: Cannot find module

go: finding module for package rivaas.dev/validation

Solution:

go mod tidy
go get rivaas.dev/validation

Issue: Version conflicts

require rivaas.dev/validation v1.0.0
// +incompatible

Solution: Update to compatible version:

go get rivaas.dev/validation@latest
go mod tidy

Common Error Messages

“cannot validate nil value”

Cause: Passing nil to Validate:

var user *User
validation.Validate(ctx, user) // Error: cannot validate nil value

Solution: Ensure value is not nil:

user := &User{Email: "test@example.com"}
validation.Validate(ctx, user)

“cannot validate invalid value”

Cause: Passing invalid reflect.Value:

var v interface{}
validation.Validate(ctx, v) // Error: cannot validate invalid value

Solution: Pass actual struct:

user := &User{}
validation.Validate(ctx, user)

“unknown validation strategy”

Cause: Invalid strategy value:

validation.Validate(ctx, &user,
    validation.WithStrategy(999), // Invalid
)

Solution: Use valid strategy constants:

validation.Validate(ctx, &user,
    validation.WithStrategy(validation.StrategyTags),
)

Debugging Tips

1. Check which strategy is being used

// Temporarily force each strategy to see which works
strategies := []validation.Strategy{
    validation.StrategyInterface,
    validation.StrategyTags,
    validation.StrategyJSONSchema,
}

for _, strategy := range strategies {
    err := validation.Validate(ctx, &user,
        validation.WithStrategy(strategy),
    )
    fmt.Printf("%v: %v\n", strategy, err)
}

2. Enable all error reporting

err := validation.Validate(ctx, &user,
    validation.WithMaxErrors(0), // Unlimited
)

3. Check struct tags

import "reflect"

t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    fmt.Printf("%s: %s\n", field.Name, field.Tag.Get("validate"))
}

4. Test interface implementation

var _ validation.ValidatorInterface = (*User)(nil) // Compile-time check
var _ validation.ValidatorWithContext = (*User)(nil)
var _ validation.JSONSchemaProvider = (*User)(nil)

Getting Help

If you’re still stuck:

  1. Check documentation: User Guide
  2. Review examples: Examples
  3. Check pkg.go.dev: API Documentation
  4. GitHub Issues: Report a bug
  5. Discussions: Ask a question

Next Steps

8 - App

API reference for the Rivaas App package - a batteries-included web framework with integrated observability.

This is the API reference for the rivaas.dev/app package. For learning-focused documentation, see the App Guide.

Overview

The App package provides a high-level, opinionated framework built on top of the Rivaas router. It includes:

  • Integrated observability (metrics, tracing, logging)
  • Lifecycle management with hooks
  • Graceful shutdown handling
  • Health and debug endpoints
  • OpenAPI spec generation
  • Request binding and validation

Package Information

  • Import Path: rivaas.dev/app
  • Go Version: 1.25+
  • License: Apache 2.0

Architecture

┌─────────────────────────────────────────┐
│           Application Layer             │
│  (app package)                          │
│                                         │
│  • Configuration Management             │
│  • Lifecycle Hooks                      │
│  • Observability Integration            │
│  • Server Management                    │
│  • Request Binding/Validation           │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│           Router Layer                  │
│  (router package)                       │
│                                         │
│  • HTTP Routing                         │
│  • Middleware Chain                     │
│  • Request Context                      │
│  • Path Parameters                      │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│        Standard Library                 │
│  (net/http)                             │
└─────────────────────────────────────────┘

Quick Reference

Core Types

  • App - Main application type
  • Context - Request context with app-level features
  • HandlerFunc - Handler function type

Key Functions

  • New() - Create a new app (returns error)
  • MustNew() - Create a new app (panics on error)

Configuration

API Reference

Resources

App Type

The main application type that wraps the router with app-level features.

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

Creating Apps

// Returns (*App, error) for error handling
a, err := app.New(
    app.WithServiceName("my-api"),
    app.WithServiceVersion("v1.0.0"),
)
if err != nil {
    log.Fatal(err)
}

// Panics on error (like regexp.MustCompile)
a := app.MustNew(
    app.WithServiceName("my-api"),
)

HTTP Methods

Register routes for HTTP methods:

a.GET(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.POST(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.PUT(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.DELETE(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.PATCH(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.HEAD(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.OPTIONS(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
a.Any(path string, handler HandlerFunc, opts ...RouteOption) *route.Route

Server Management

a.Start(ctx context.Context) error
a.StartTLS(ctx context.Context, certFile, keyFile string) error
a.StartMTLS(ctx context.Context, serverCert tls.Certificate, opts ...MTLSOption) error

Lifecycle Hooks

a.OnStart(fn func(context.Context) error)
a.OnReady(fn func())
a.OnShutdown(fn func(context.Context))
a.OnStop(fn func())
a.OnRoute(fn func(*route.Route))

See Lifecycle Hooks for details.

Context Type

Request context that extends router.Context with app-level features.

type Context struct {
    *router.Context
    // contains filtered or unexported fields
}

Request Binding

c.Bind(out any, opts ...BindOption) error
c.MustBind(out any, opts ...BindOption) bool
c.BindOnly(out any, opts ...BindOption) error
c.Validate(v any, opts ...validation.Option) error

Error Handling

c.Fail(err error)
c.FailStatus(status int, err error)
c.NotFound(err error)
c.BadRequest(err error)
c.Unauthorized(err error)
c.Forbidden(err error)
c.Conflict(err error)
c.Gone(err error)
c.UnprocessableEntity(err error)
c.TooManyRequests(err error)
c.InternalError(err error)
c.ServiceUnavailable(err error)

Logging

c.Logger() *slog.Logger

See Context API for complete reference.

HandlerFunc

Handler function type that receives an app Context.

type HandlerFunc func(*Context)

Example:

func handler(c *app.Context) {
    c.JSON(http.StatusOK, data)
}

a.GET("/", handler)

Next Steps

8.1 - API Reference

Complete API reference for the App package.

Core Functions

New

func New(opts ...Option) (*App, error)

Creates a new App instance with the given options. Returns an error if configuration is invalid.

Parameters:

  • opts - Configuration options

Returns:

  • *App - The app instance
  • error - Configuration validation errors

Example:

a, err := app.New(
    app.WithServiceName("my-api"),
    app.WithServiceVersion("v1.0.0"),
)
if err != nil {
    log.Fatal(err)
}

MustNew

func MustNew(opts ...Option) *App

Creates a new App instance or panics on error. Use for initialization in main() functions.

Parameters:

  • opts - Configuration options

Returns:

  • *App - The app instance

Panics: If configuration is invalid

Example:

a := app.MustNew(
    app.WithServiceName("my-api"),
)

App Methods

HTTP Method Shortcuts

func (a *App) GET(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) POST(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) PUT(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) DELETE(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) PATCH(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) HEAD(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) OPTIONS(path string, handler HandlerFunc, opts ...RouteOption) *route.Route
func (a *App) Any(path string, handler HandlerFunc, opts ...RouteOption) *route.Route

Register routes for HTTP methods.

Middleware

func (a *App) Use(middleware ...HandlerFunc)

Adds middleware to the app. Middleware executes for all routes registered after Use().

Route Groups

func (a *App) Group(prefix string, middleware ...HandlerFunc) *Group
func (a *App) Version(version string) *VersionGroup

Create route groups and version groups.

Static Files

func (a *App) Static(prefix, root string)
func (a *App) File(path, filepath string)
func (a *App) StaticFS(prefix string, fs http.FileSystem)
func (a *App) NoRoute(handler HandlerFunc)

Serve static files and set custom 404 handler.

Server Management

func (a *App) Start(ctx context.Context) error
func (a *App) StartTLS(ctx context.Context, certFile, keyFile string) error
func (a *App) StartMTLS(ctx context.Context, serverCert tls.Certificate, opts ...MTLSOption) error

Start HTTP, HTTPS, or mTLS servers with graceful shutdown.

Lifecycle Hooks

func (a *App) OnStart(fn func(context.Context) error)
func (a *App) OnReady(fn func())
func (a *App) OnShutdown(fn func(context.Context))
func (a *App) OnStop(fn func())
func (a *App) OnRoute(fn func(*route.Route))

Register lifecycle hooks. See Lifecycle Hooks for details.

Accessors

func (a *App) Router() *router.Router
func (a *App) Metrics() *metrics.Recorder
func (a *App) Tracing() *tracing.Tracer
func (a *App) Readiness() *ReadinessManager
func (a *App) ServiceName() string
func (a *App) ServiceVersion() string
func (a *App) Environment() string

Access underlying components and configuration.

Route Management

func (a *App) Route(name string) (*route.Route, bool)
func (a *App) Routes() []*route.Route
func (a *App) URLFor(routeName string, params map[string]string, query map[string][]string) (string, error)
func (a *App) MustURLFor(routeName string, params map[string]string, query map[string][]string) string

Route lookup and URL generation. Router must be frozen (after Start()).

Metrics

func (a *App) GetMetricsHandler() (http.Handler, error)
func (a *App) GetMetricsServerAddress() string

Access metrics handler and server address.

Logging

func (a *App) BaseLogger() *slog.Logger

Returns the application’s base logger. Never returns nil.

Testing

func (a *App) Test(req *http.Request, opts ...TestOption) (*http.Response, error)
func (a *App) TestJSON(method, path string, body any, opts ...TestOption) (*http.Response, error)

Test routes without starting a server.

Helper Functions

ExpectJSON

func ExpectJSON(t testingT, resp *http.Response, statusCode int, out any)

Test helper that asserts response status and decodes JSON.

Generic Binding

func Bind[T any](c *Context, opts ...BindOption) (T, error)
func MustBind[T any](c *Context, opts ...BindOption) (T, bool)
func BindOnly[T any](c *Context, opts ...BindOption) (T, error)
func BindPatch[T any](c *Context, opts ...BindOption) (T, error)
func MustBindPatch[T any](c *Context, opts ...BindOption) (T, bool)
func BindStrict[T any](c *Context, opts ...BindOption) (T, error)
func MustBindStrict[T any](c *Context, opts ...BindOption) (T, bool)

Type-safe binding with generics. These functions provide a more concise API compared to the Context methods.

Types

HandlerFunc

type HandlerFunc func(*Context)

Handler function that receives an app Context.

TestOption

type TestOption func(*testConfig)

func WithTimeout(d time.Duration) TestOption
func WithContext(ctx context.Context) TestOption

Options for testing.

Next Steps

8.2 - Configuration Options

App-level configuration options reference.

Service Configuration

WithServiceName

func WithServiceName(name string) Option

Sets the service name used in observability metadata. This includes metrics, traces, and logs. If empty, validation fails.

Default: "rivaas-app"

WithServiceVersion

func WithServiceVersion(version string) Option

Sets the service version used in observability and API documentation. Must be non-empty or validation fails.

Default: "1.0.0"

WithEnvironment

func WithEnvironment(env string) Option

Sets the environment mode. Valid values: "development", "production". Invalid values cause validation to fail.

Default: "development"

Server Configuration

WithServer

func WithServer(opts ...ServerOption) Option

Configures server settings. See Server Options for sub-options.

Observability

WithObservability

func WithObservability(opts ...ObservabilityOption) Option

Configures all observability components (metrics, tracing, logging). See Observability Options for sub-options.

Endpoints

WithHealthEndpoints

func WithHealthEndpoints(opts ...HealthOption) Option

Enables health endpoints. See Health Options for sub-options.

WithDebugEndpoints

func WithDebugEndpoints(opts ...DebugOption) Option

Enables debug endpoints. See Debug Options for sub-options.

Middleware

WithMiddleware

func WithMiddleware(middlewares ...HandlerFunc) Option

Adds middleware during app initialization. Multiple calls accumulate.

WithoutDefaultMiddleware

func WithoutDefaultMiddleware() Option

Disables default middleware (recovery). Use when you want full control over middleware.

Router

WithRouter

func WithRouter(opts ...router.Option) Option

Passes router options to the underlying router. Multiple calls accumulate.

OpenAPI

WithOpenAPI

func WithOpenAPI(opts ...openapi.Option) Option

Enables OpenAPI specification generation. Service name and version are automatically injected from app-level configuration.

Error Formatting

WithErrorFormatter

func WithErrorFormatter(formatter errors.Formatter) Option

Configures a single error formatter for all error responses.

WithErrorFormatters

func WithErrorFormatters(formatters map[string]errors.Formatter) Option

Configures multiple error formatters with content negotiation based on Accept header.

WithDefaultErrorFormat

func WithDefaultErrorFormat(mediaType string) Option

Sets the default format when no Accept header matches. Only used with WithErrorFormatters.

Complete Example

a, err := app.New(
    // Service
    app.WithServiceName("orders-api"),
    app.WithServiceVersion("v2.0.0"),
    app.WithEnvironment("production"),
    
    // Server
    app.WithServer(
        app.WithReadTimeout(10 * time.Second),
        app.WithWriteTimeout(15 * time.Second),
        app.WithShutdownTimeout(30 * time.Second),
    ),
    
    // Observability
    app.WithObservability(
        app.WithLogging(logging.WithJSONHandler()),
        app.WithMetrics(),
        app.WithTracing(tracing.WithOTLP("localhost:4317")),
    ),
    
    // Health endpoints
    app.WithHealthEndpoints(
        app.WithReadinessCheck("database", dbCheck),
    ),
    
    // OpenAPI
    app.WithOpenAPI(
        openapi.WithSwaggerUI(true, "/docs"),
    ),
)

Next Steps

8.3 - Server Options

Server configuration options reference.

Server Options

These options are used with WithServer():

app.WithServer(
    app.WithReadTimeout(10 * time.Second),
    app.WithWriteTimeout(15 * time.Second),
)

Timeout Options

WithReadTimeout

func WithReadTimeout(d time.Duration) ServerOption

Maximum time to read entire request (including body). Must be positive.

Default: 10s

WithWriteTimeout

func WithWriteTimeout(d time.Duration) ServerOption

Maximum time to write response. Must be positive. Should be >= ReadTimeout.

Default: 10s

WithIdleTimeout

func WithIdleTimeout(d time.Duration) ServerOption

Maximum time to wait for next request on keep-alive connection. Must be positive.

Default: 60s

WithReadHeaderTimeout

func WithReadHeaderTimeout(d time.Duration) ServerOption

Maximum time to read request headers. Must be positive.

Default: 2s

WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) ServerOption

Graceful shutdown timeout. Must be at least 1 second.

Default: 30s

Size Options

WithMaxHeaderBytes

func WithMaxHeaderBytes(n int) ServerOption

Maximum request header size in bytes. Must be at least 1KB (1024 bytes).

Default: 1MB (1048576 bytes)

Validation

Configuration is automatically validated:

  • All timeouts must be positive
  • ReadTimeout should not exceed WriteTimeout
  • ShutdownTimeout must be at least 1 second
  • MaxHeaderBytes must be at least 1KB

Invalid configuration causes app.New() to return an error.

8.4 - Observability Options

Observability configuration options reference (metrics, tracing, logging).

Observability Options

These options are used with WithObservability():

app.WithObservability(
    app.WithLogging(logging.WithJSONHandler()),
    app.WithMetrics(),
    app.WithTracing(tracing.WithOTLP("localhost:4317")),
)

You can also configure observability using environment variables. See Environment Variables Guide for details.

Component Options

WithLogging

func WithLogging(opts ...logging.Option) ObservabilityOption

Enables structured logging with slog. Service name/version automatically injected.

Environment variable alternative:

export RIVAAS_LOG_LEVEL=info      # debug, info, warn, error
export RIVAAS_LOG_FORMAT=json     # json, text, console

WithMetrics

func WithMetrics(opts ...metrics.Option) ObservabilityOption

Enables metrics collection (Prometheus by default). Service name/version automatically injected.

Environment variable alternative:

export RIVAAS_METRICS_EXPORTER=prometheus  # or otlp, stdout
export RIVAAS_METRICS_ADDR=:9090          # Optional: custom Prometheus address
export RIVAAS_METRICS_PATH=/metrics        # Optional: custom Prometheus path

WithTracing

func WithTracing(opts ...tracing.Option) ObservabilityOption

Enables distributed tracing. Service name/version automatically injected.

Environment variable alternative:

export RIVAAS_TRACING_EXPORTER=otlp        # or otlp-http, stdout
export RIVAAS_TRACING_ENDPOINT=localhost:4317  # Required for otlp/otlp-http

Metrics Server Options

WithMetricsOnMainRouter

func WithMetricsOnMainRouter(path string) ObservabilityOption

Mounts metrics endpoint on the main HTTP server (default: separate server).

WithMetricsSeparateServer

func WithMetricsSeparateServer(addr, path string) ObservabilityOption

Configures separate metrics server address and path.

Default: :9090/metrics

Path Filtering

WithExcludePaths

func WithExcludePaths(paths ...string) ObservabilityOption

Excludes exact paths from observability.

WithExcludePrefixes

func WithExcludePrefixes(prefixes ...string) ObservabilityOption

Excludes path prefixes from observability.

WithExcludePatterns

func WithExcludePatterns(patterns ...string) ObservabilityOption

Excludes paths matching regex patterns from observability.

WithoutDefaultExclusions

func WithoutDefaultExclusions() ObservabilityOption

Disables default path exclusions (/health*, /metrics, /debug/*).

Access Logging

WithAccessLogging

func WithAccessLogging(enabled bool) ObservabilityOption

Enables or disables access logging.

Default: true

WithLogOnlyErrors

func WithLogOnlyErrors() ObservabilityOption

Logs only errors and slow requests (reduces log volume).

Default: false in development, true in production

In production, this is automatically enabled to reduce log volume. Normal successful requests are not logged, but errors (status >= 400) and slow requests are always logged.

WithSlowThreshold

func WithSlowThreshold(d time.Duration) ObservabilityOption

Marks requests as slow if they exceed this duration.

Default: 1s

Example

app.WithObservability(
    // Components
    app.WithLogging(logging.WithJSONHandler()),
    app.WithMetrics(metrics.WithPrometheus(":9090", "/metrics")),
    app.WithTracing(tracing.WithOTLP("localhost:4317")),
    
    // Path filtering
    app.WithExcludePaths("/livez", "/readyz"),
    app.WithExcludePrefixes("/internal/"),
    
    // Access logging
    app.WithLogOnlyErrors(),
    app.WithSlowThreshold(500 * time.Millisecond),
)

8.5 - Health Options

Health endpoint configuration options reference.

Health Options

These options are used with WithHealthEndpoints():

app.WithHealthEndpoints(
    app.WithReadinessCheck("database", dbCheck),
    app.WithHealthTimeout(800 * time.Millisecond),
)

Path Configuration

WithHealthPrefix

func WithHealthPrefix(prefix string) HealthOption

Mounts health endpoints under a prefix.

Default: "" (root)

WithLivezPath

func WithLivezPath(path string) HealthOption

Custom liveness probe path.

Default: "/livez"

WithReadyzPath

func WithReadyzPath(path string) HealthOption

Custom readiness probe path.

Default: "/readyz"

Check Configuration

WithHealthTimeout

func WithHealthTimeout(d time.Duration) HealthOption

Timeout for each health check.

Default: 1s

WithLivenessCheck

func WithLivenessCheck(name string, fn CheckFunc) HealthOption

Adds a liveness check. Liveness checks should be dependency-free and fast.

WithReadinessCheck

func WithReadinessCheck(name string, fn CheckFunc) HealthOption

Adds a readiness check. Readiness checks verify external dependencies.

CheckFunc

type CheckFunc func(context.Context) error

Health check function that returns nil if healthy, error if unhealthy.

Example

app.WithHealthEndpoints(
    app.WithHealthPrefix("/_system"),
    app.WithHealthTimeout(800 * time.Millisecond),
    app.WithLivenessCheck("process", func(ctx context.Context) error {
        return nil
    }),
    app.WithReadinessCheck("database", func(ctx context.Context) error {
        return db.PingContext(ctx)
    }),
)

// Endpoints:
// GET /_system/livez - Liveness (200 if all checks pass)
// GET /_system/readyz - Readiness (204 if all checks pass)

8.6 - Debug Options

Debug endpoint configuration options reference.

Debug Options

These options are used with WithDebugEndpoints():

app.WithDebugEndpoints(
    app.WithPprofIf(os.Getenv("PPROF_ENABLED") == "true"),
)

Path Configuration

WithDebugPrefix

func WithDebugPrefix(prefix string) DebugOption

Mounts debug endpoints under a custom prefix.

Default: "/debug"

pprof Options

WithPprof

func WithPprof() DebugOption

Enables pprof endpoints unconditionally.

WithPprofIf

func WithPprofIf(condition bool) DebugOption

Conditionally enables pprof endpoints based on a boolean condition.

Available Endpoints

When pprof is enabled:

  • GET /debug/pprof/ - Main pprof index
  • GET /debug/pprof/cmdline - Command line
  • GET /debug/pprof/profile - CPU profile
  • GET /debug/pprof/symbol - Symbol lookup
  • GET /debug/pprof/trace - Execution trace
  • GET /debug/pprof/allocs - Memory allocations
  • GET /debug/pprof/block - Block profile
  • GET /debug/pprof/goroutine - Goroutine profile
  • GET /debug/pprof/heap - Heap profile
  • GET /debug/pprof/mutex - Mutex profile
  • GET /debug/pprof/threadcreate - Thread creation profile

Security Warning

⚠️ Never enable pprof in production without proper authentication. Debug endpoints expose sensitive runtime information.

Example

// Development: enable unconditionally
app.WithDebugEndpoints(
    app.WithPprof(),
)

// Production: enable conditionally
app.WithDebugEndpoints(
    app.WithDebugPrefix("/_internal/debug"),
    app.WithPprofIf(os.Getenv("PPROF_ENABLED") == "true"),
)

8.7 - Context API

Context methods for request handling.

Request Binding

Bind

func (c *Context) Bind(out any, opts ...BindOption) error

Binds request data and validates it. This is the main method for handling requests.

Reads data from all sources (path, query, headers, cookies, JSON, forms) based on struct tags. Then validates the data using the configured strategy.

Returns: Error if binding or validation fails.

MustBind

func (c *Context) MustBind(out any, opts ...BindOption) bool

Binds and validates, automatically sending error responses on failure.

Use this when you want simple error handling. If binding or validation fails, it sends the error response and returns false.

Returns: True if successful, false if error was sent.

BindOnly

func (c *Context) BindOnly(out any, opts ...BindOption) error

Binds request data without validation.

Use this when you need to process data before validating it.

Returns: Error if binding fails.

Validate

func (c *Context) Validate(v any, opts ...validation.Option) error

Validates a struct using the configured strategy.

Use this after BindOnly() when you need fine-grained control.

Returns: Validation error if validation fails.

Error Handling

All error handling methods automatically format the error response and abort the handler chain. No further handlers will run after calling these methods.

Fail

func (c *Context) Fail(err error)

Sends a formatted error response using the configured formatter. The HTTP status code is determined from the error (if it implements HTTPStatus() int) or defaults to 500.

Parameters:

  • err: The error to send. If nil, the method returns without doing anything.

Behavior:

  • Formats the error using content negotiation
  • Writes the HTTP response
  • Aborts the handler chain

FailStatus

func (c *Context) FailStatus(status int, err error)

Sends an error response with an explicit HTTP status code.

Parameters:

  • status: The HTTP status code to use
  • err: The error to send

Behavior:

  • Wraps the error with the specified status code
  • Formats and sends the response
  • Aborts the handler chain

NotFound

func (c *Context) NotFound(err error)

Sends a 404 Not Found error response.

Parameters:

  • err: The error to send, or nil for a generic “Not Found” message

BadRequest

func (c *Context) BadRequest(err error)

Sends a 400 Bad Request error response.

Parameters:

  • err: The error to send, or nil for a generic “Bad Request” message

Unauthorized

func (c *Context) Unauthorized(err error)

Sends a 401 Unauthorized error response.

Parameters:

  • err: The error to send, or nil for a generic “Unauthorized” message

Forbidden

func (c *Context) Forbidden(err error)

Sends a 403 Forbidden error response.

Parameters:

  • err: The error to send, or nil for a generic “Forbidden” message

Conflict

func (c *Context) Conflict(err error)

Sends a 409 Conflict error response.

Parameters:

  • err: The error to send, or nil for a generic “Conflict” message

Gone

func (c *Context) Gone(err error)

Sends a 410 Gone error response.

Parameters:

  • err: The error to send, or nil for a generic “Gone” message

UnprocessableEntity

func (c *Context) UnprocessableEntity(err error)

Sends a 422 Unprocessable Entity error response.

Parameters:

  • err: The error to send, or nil for a generic “Unprocessable Entity” message

TooManyRequests

func (c *Context) TooManyRequests(err error)

Sends a 429 Too Many Requests error response.

Parameters:

  • err: The error to send, or nil for a generic “Too Many Requests” message

InternalError

func (c *Context) InternalError(err error)

Sends a 500 Internal Server Error response.

Parameters:

  • err: The error to send, or nil for a generic “Internal Server Error” message

ServiceUnavailable

func (c *Context) ServiceUnavailable(err error)

Sends a 503 Service Unavailable error response.

Parameters:

  • err: The error to send, or nil for a generic “Service Unavailable” message

Logging

To log from a handler with trace correlation, pass the request context to the standard library’s context-aware logging functions. For example: slog.InfoContext(c.RequestContext(), "msg", ...) or slog.ErrorContext(c.RequestContext(), "msg", ...). When the app is configured with observability (logging and tracing), trace_id and span_id are injected automatically from the active OpenTelemetry span.

Presence

Presence

func (c *Context) Presence() validation.PresenceMap

Returns the presence map for the current request (tracks which fields were present in JSON).

ResetBinding

func (c *Context) ResetBinding()

Resets binding metadata (useful for testing).

Router Context

The app Context embeds router.Context, providing access to all router features:

  • c.Request - HTTP request
  • c.Response - HTTP response writer
  • c.Param(name) - Path parameter
  • c.Query(name) - Query parameter
  • c.JSON(status, data) - Send JSON response
  • c.String(status, text) - Send text response
  • c.HTML(status, html) - Send HTML response
  • And more…

See Router Context API for complete router context reference.

8.8 - Lifecycle Hooks

Lifecycle hook APIs and execution order.

Hook Methods

OnStart

func (a *App) OnStart(fn func(context.Context) error)

Called before server starts. Hooks run sequentially and stop on first error.

Use for: Database connections, migrations, initialization that must succeed.

OnReady

func (a *App) OnReady(fn func())

Called after server starts listening. Hooks run asynchronously and don’t block startup.

Use for: Warmup tasks, service discovery registration.

OnShutdown

func (a *App) OnShutdown(fn func(context.Context))

Called during graceful shutdown. Hooks run in LIFO order with shutdown timeout.

Use for: Closing connections, flushing buffers, cleanup that must complete within timeout.

OnStop

func (a *App) OnStop(fn func())

Called after shutdown completes. Hooks run in best-effort mode and panics are caught.

Use for: Final cleanup that doesn’t need timeout.

OnRoute

func (a *App) OnRoute(fn func(*route.Route))

Called when a route is registered. Disabled after router freeze.

Use for: Route validation, logging, documentation generation.

OnReload

func (a *App) OnReload(fn func(context.Context) error)

Called when the application receives a reload signal (SIGHUP) or when Reload() is called programmatically. SIGHUP signal handling is automatically enabled when you register this hook.

If no OnReload hooks are registered, SIGHUP is ignored on Unix so the process keeps running (e.g. kill -HUP does not terminate it).

Hooks run sequentially and stop on first error. Errors are logged but don’t crash the server.

Use for: Reloading configuration, rotating certificates, flushing caches, updating runtime settings.

Platform: SIGHUP works on Unix/Linux/macOS. On Windows, use programmatic Reload().

Reload

func (a *App) Reload(ctx context.Context) error

Manually triggers all registered OnReload hooks. Useful for admin endpoints or Windows where SIGHUP isn’t available.

Returns an error if any hook fails, but the server continues running with the old configuration.

Execution Flow

1. app.Start(ctx) called
2. OnStart hooks execute (sequential, stop on error)
3. Server starts listening
4. OnReady hooks execute (async, non-blocking)
5. Server handles requests...
   → OnReload hooks execute when SIGHUP received (sequential, logged on error)
6. Context canceled (SIGTERM/SIGINT)
7. OnShutdown hooks execute (LIFO order, with timeout)
8. Server shutdown complete
9. OnStop hooks execute (best-effort, no timeout)
10. Process exits

Hook Characteristics

HookOrderError HandlingTimeoutAsync
OnStartSequentialStop on first errorNoNo
OnReady-Panic caught and loggedNoYes
OnReloadSequentialStop on first error, loggedNoNo
OnShutdownLIFOErrors ignoredYes (shutdown timeout)No
OnStop-Panic caught and loggedNoNo
OnRouteSequential-NoNo

Example

a := app.MustNew()

// OnStart: Initialize (sequential, stops on error)
a.OnStart(func(ctx context.Context) error {
    return db.Connect(ctx)
})

// OnReady: Post-startup (async, non-blocking)
a.OnReady(func() {
    consul.Register("my-service", ":8080")
})

// OnReload: Reload configuration (sequential, logged on error)
a.OnReload(func(ctx context.Context) error {
    cfg, err := loadConfig("config.yaml")
    if err != nil {
        return err
    }
    applyConfig(cfg)
    return nil
})

// OnShutdown: Graceful cleanup (LIFO, with timeout)
a.OnShutdown(func(ctx context.Context) {
    db.Close()
})

// OnStop: Final cleanup (best-effort)
a.OnStop(func() {
    cleanupTempFiles()
})

8.9 - Troubleshooting

Common issues and solutions for the App package.

Configuration Errors

Validation Errors

Problem: app.New() returns validation errors.

Solution: Check error message for specific field. Common issues:

  • Empty service name or version.
  • Invalid environment. Must be “development” or “production”.
  • ReadTimeout greater than WriteTimeout.
  • ShutdownTimeout less than 1 second.
  • MaxHeaderBytes less than 1KB.

Example:

a, err := app.New(
    app.WithServiceName(""),  // ❌ Empty
)
// Error: "serviceName must not be empty"

Import Errors

Problem: Cannot import rivaas.dev/app.

Solution:

go get rivaas.dev/app
go mod tidy

Ensure Go 1.25+ is installed.

Server Issues

Port Already in Use

Problem: Server fails to start with “address already in use”.

Solution: Check if port is in use:

lsof -i :8080
# Or
netstat -an | grep 8080

Kill the process or use a different port.

Routes Not Registering

Problem: Routes return 404 even though registered.

Solution:

  • Ensure routes registered before Start().
  • Check paths match exactly. They are case-sensitive.
  • Verify HTTP method matches.
  • Router freezes on startup. Can’t add routes after.

Graceful Shutdown Not Working

Problem: Server doesn’t shut down cleanly.

Solution:

  • Increase shutdown timeout: WithShutdownTimeout(60 * time.Second).
  • Check OnShutdown hooks complete quickly.
  • Verify handlers respect context cancellation.

Observability Issues

Metrics Not Appearing

Problem: Metrics endpoint returns 404.

Solution:

  • Ensure metrics enabled: WithMetrics()
  • Check metrics address: a.GetMetricsServerAddress()
  • Default is separate server on :9090/metrics
  • Use WithMetricsOnMainRouter("/metrics") to mount on main router

Tracing Not Working

Problem: No traces appear in backend.

Solution:

  • Verify tracing enabled: WithTracing()
  • Check OTLP endpoint configuration
  • Ensure tracing backend is running and accessible
  • Verify network connectivity
  • Check logs for tracing initialization errors

Logs Not Appearing

Problem: No logs are written.

Solution:

  • Ensure logging enabled: WithLogging()
  • Check log level configuration
  • Verify logger handler is correct (JSON, Console, etc.)
  • Use c.Logger() in handlers, not package-level logger

Middleware Issues

Middleware Not Executing

Problem: Middleware functions aren’t being called.

Solution:

  • Ensure middleware added before routes
  • Check middleware calls c.Next()
  • Verify middleware isn’t returning early
  • Default recovery middleware is included automatically

Authentication Failing

Problem: Auth middleware not working correctly.

Solution:

  • Check header/token extraction logic
  • Verify middleware order (auth should run early)
  • Ensure c.Next() is called on success
  • Test middleware in isolation

Testing Issues

Test Hangs

Problem: a.Test() never returns.

Solution:

  • Set timeout: a.Test(req, app.WithTimeout(5*time.Second))
  • Check for infinite loops in handler
  • Verify middleware calls c.Next()

Test Fails with Panic

Problem: Test panics instead of returning error.

Solution:

  • Use recover() in test or
  • Check that handler doesn’t panic
  • Recovery middleware catches panics in real server

Health Check Issues

Health Checks Always Failing

Problem: /livez or /readyz always returns 503.

Solution:

  • Check health check functions return nil on success
  • Verify dependencies (database, cache) are accessible
  • Check health timeout is sufficient
  • Test health checks independently

Health Checks Never Complete

Problem: Health checks timeout.

Solution:

  • Increase timeout: WithHealthTimeout(2 * time.Second)
  • Check dependencies respond within timeout
  • Verify no deadlocks in check functions
  • Use context timeout in check functions

Debugging Tips

Enable Development Mode

app.WithEnvironment("development")

Enables verbose logging and route table display.

Check Observability Status

if a.Metrics() != nil {
    fmt.Println("Metrics:", a.GetMetricsServerAddress())
}
if a.Tracing() != nil {
    fmt.Println("Tracing enabled")
}

Use Test Helpers

resp, err := a.Test(req)  // Test without starting server

Enable GC Tracing

GODEBUG=gctrace=1 go run main.go

Getting Help

9 - Router Package

Complete API reference for the rivaas.dev/router package.

This is the API reference for the rivaas.dev/router package. For learning-focused documentation, see the Router Guide.

Overview

The rivaas.dev/router package provides a high-performance HTTP router with comprehensive features:

  • Radix tree routing with bloom filters
  • Compiled route tables for performance
  • Built-in middleware support
  • Request binding and validation
  • OpenTelemetry native tracing
  • API versioning
  • Content negotiation

Package Structure

rivaas.dev/router/
├── router.go          # Core router and route registration
├── context.go         # Request context with pooling
├── route.go           # Route definitions and constraints
├── middleware/        # Built-in middleware
│   ├── accesslog/    # Structured access logging
│   ├── cors/         # CORS handling
│   ├── recovery/     # Panic recovery
│   └── ...           # More middleware
└── ...

Quick API Index

Core Types

Route Registration

  • HTTP Methods: GET(), POST(), PUT(), DELETE(), PATCH(), OPTIONS(), HEAD()
  • Route Groups: Group(prefix), Version(version)
  • Middleware: Use(middleware...)
  • Static Files: Static(), StaticFile(), StaticFS()

Request Handling

  • Parameters: Param(), Query(), PostForm()
  • Headers: Header(), GetHeader()
  • Cookies: Cookie(), SetCookie()

Response Rendering

  • JSON: JSON(), PureJSON(), IndentedJSON(), SecureJSON()
  • Other: YAML(), String(), HTML(), Data()
  • Files: ServeFile(), Download(), DataFromReader()

Configuration

Performance

Routing Performance

  • Sub-microsecond routing: 119ns per operation
  • High throughput: 8.4M+ requests/second
  • Memory efficient: 16 bytes per request, 1 allocation
  • Context pooling: Automatic context reuse
  • Lock-free operations: Atomic operations for concurrent access

Optimization Features

  • Compiled routes: Pre-compiled routes for static and dynamic paths
  • Bloom filters: Fast negative lookups for static routes
  • First-segment index: ASCII-only route narrowing (O(1) lookup)
  • Parameter storage: Array-based for ≤8 params, map for >8
  • Type caching: Reflection information cached per struct type

Thread Safety

All router operations are concurrent-safe:

  • Route registration can occur from multiple goroutines
  • Route trees use atomic operations for concurrent access
  • Context pooling is thread-safe
  • Middleware execution is goroutine-safe

Next Steps

9.1 - API Reference

Core types and methods for the router package.

Router

router.New(opts ...Option) *Router

Creates a new router instance.

r := router.New()

// With options
r := router.New(
    router.WithTracing(),
    router.WithTracingServiceName("my-api"),
)

HTTP Method Handlers

Register routes for HTTP methods:

r.GET(path string, handlers ...HandlerFunc) *Route
r.POST(path string, handlers ...HandlerFunc) *Route
r.PUT(path string, handlers ...HandlerFunc) *Route
r.DELETE(path string, handlers ...HandlerFunc) *Route
r.PATCH(path string, handlers ...HandlerFunc) *Route
r.OPTIONS(path string, handlers ...HandlerFunc) *Route
r.HEAD(path string, handlers ...HandlerFunc) *Route

Example:

r.GET("/users", listUsersHandler)
r.POST("/users", createUserHandler)
r.GET("/users/:id", getUserHandler)

Middleware

r.Use(middleware ...HandlerFunc)

Adds global middleware to the router.

r.Use(Logger(), Recovery())

Route Groups

r.Group(prefix string, middleware ...HandlerFunc) *Group

Creates a new route group with the specified prefix and optional middleware.

api := r.Group("/api/v1")
api.Use(Auth())
api.GET("/users", listUsers)

API Versioning

r.Version(version string) *Group

Creates a version-specific route group.

v1 := r.Version("v1")
v1.GET("/users", listUsersV1)

Static Files

r.Static(relativePath, root string)
r.StaticFile(relativePath, filepath string)
r.StaticFS(relativePath string, fs http.FileSystem)

Example:

r.Static("/assets", "./public")
r.StaticFile("/favicon.ico", "./static/favicon.ico")

Route Introspection

r.Routes() []RouteInfo

Returns all registered routes for inspection.

Route

Constraints

Apply validation constraints to route parameters:

route.WhereInt(param string) *Route
route.WhereFloat(param string) *Route
route.WhereUUID(param string) *Route
route.WhereDate(param string) *Route
route.WhereDateTime(param string) *Route
route.WhereEnum(param string, values ...string) *Route
route.WhereRegex(param, pattern string) *Route

Example:

r.GET("/users/:id", getUserHandler).WhereInt("id")
r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")
r.GET("/status/:state", getStatusHandler).WhereEnum("state", "active", "pending")

Group

Route groups support the same methods as Router, with the group’s prefix automatically prepended.

group.GET(path string, handlers ...HandlerFunc) *Route
group.POST(path string, handlers ...HandlerFunc) *Route
group.Use(middleware ...HandlerFunc)
group.Group(prefix string, middleware ...HandlerFunc) *Group

HandlerFunc

type HandlerFunc func(*Context)

Handler function signature for route handlers and middleware.

Example:

func handler(c *router.Context) {
    c.JSON(200, map[string]string{"message": "Hello"})
}

Next Steps

9.2 - Router Options

Configuration options for Router initialization.

Router options are passed to router.New() or router.MustNew() to configure the router.

Router Creation

// With error handling
r, err := router.New(opts...)
if err != nil {
    log.Fatalf("Failed to create router: %v", err)
}

// Panics on invalid configuration. Use at startup.
r := router.MustNew(opts...)

Versioning Options

WithVersioning(opts ...version.Option)

Configures API versioning support using functional options from the version package.

import "rivaas.dev/router/version"

r := router.MustNew(
    router.WithVersioning(
        version.WithHeaderDetection("X-API-Version"),
        version.WithDefault("v1"),
    ),
)

With multiple detection strategies:

r := router.MustNew(
    router.WithVersioning(
        version.WithPathDetection("/api/v{version}"),
        version.WithHeaderDetection("X-API-Version"),
        version.WithQueryDetection("v"),
        version.WithDefault("v2"),
        version.WithResponseHeaders(),
        version.WithSunsetEnforcement(),
    ),
)

Diagnostic Options

WithDiagnostics(handler DiagnosticHandler)

Sets a diagnostic handler for informational events like header injection attempts or configuration warnings.

import "log/slog"

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    slog.Warn(e.Message, "kind", e.Kind, "fields", e.Fields)
})

r := router.MustNew(router.WithDiagnostics(handler))

With metrics:

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    metrics.Increment("router.diagnostics", "kind", string(e.Kind))
})

Server Options

WithH2C(enable bool)

Enables HTTP/2 Cleartext (h2c) support.

r := router.MustNew(router.WithH2C(true))

WithServerTimeouts(readHeader, read, write, idle time.Duration)

Configures HTTP server timeouts to prevent slowloris attacks and resource exhaustion.

Defaults (if not set):

  • ReadHeaderTimeout: 5s
  • ReadTimeout: 15s
  • WriteTimeout: 30s
  • IdleTimeout: 60s
r := router.MustNew(router.WithServerTimeouts(
    10*time.Second,  // ReadHeaderTimeout
    30*time.Second,  // ReadTimeout
    60*time.Second,  // WriteTimeout
    120*time.Second, // IdleTimeout
))

Performance Options

WithRouteCompilation(enabled bool) / WithoutRouteCompilation()

Controls compiled route matching. When enabled (default), routes are pre-compiled for faster lookup.

// Enabled by default
r := router.MustNew(router.WithRouteCompilation(true))

// Disable for debugging
r := router.MustNew(router.WithoutRouteCompilation())

WithBloomFilterSize(size uint64)

Sets the bloom filter size for compiled routes. Larger sizes reduce false positives.

Default: 1000
Recommended: 2-3x the number of static routes

r := router.MustNew(router.WithBloomFilterSize(2000)) // For ~1000 routes

WithBloomFilterHashFunctions(numFuncs int)

Sets the number of hash functions for bloom filters.

Default: 3
Range: 1-10 (clamped)

r := router.MustNew(router.WithBloomFilterHashFunctions(4))

WithCancellationCheck(enabled bool) / WithoutCancellationCheck()

Controls context cancellation checking in the middleware chain. When enabled (default), the router checks for canceled contexts between handlers.

// Enabled by default
r := router.MustNew(router.WithCancellationCheck(true))

// Disable if you handle cancellation manually
r := router.MustNew(router.WithoutCancellationCheck())

Complete Example

package main

import (
    "log/slog"
    "net/http"
    "os"
    "time"
    
    "rivaas.dev/router"
    "rivaas.dev/router/version"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Diagnostic handler
    diagHandler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
        logger.Warn(e.Message, "kind", e.Kind, "fields", e.Fields)
    })
    
    // Create router with options
    r := router.MustNew(
        // Versioning
        router.WithVersioning(
            version.WithHeaderDetection("API-Version"),
            version.WithDefault("v1"),
        ),
        
        // Server configuration
        router.WithServerTimeouts(
            10*time.Second,
            30*time.Second,
            60*time.Second,
            120*time.Second,
        ),
        
        // Performance tuning
        router.WithBloomFilterSize(2000),
        
        // Diagnostics
        router.WithDiagnostics(diagHandler),
    )
    
    r.GET("/", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    http.ListenAndServe(":8080", r)
}

Observability Options

import (
    "rivaas.dev/app"
    "rivaas.dev/tracing"
    "rivaas.dev/metrics"
)

application := app.New(
    app.WithServiceName("my-api"),
    app.WithObservability(
        app.WithTracing(tracing.WithSampleRate(0.1)),
        app.WithMetrics(metrics.WithPrometheus()),
        app.WithExcludePaths("/health", "/metrics"),
    ),
)

Next Steps

9.3 - Context API

Complete reference for Context methods.

The Context provides access to request/response and utility methods.

Request Information

URL Parameters

c.Param(key string) string

Returns URL parameter value from the route path.

// Route: /users/:id
userID := c.Param("id")
c.AllParams() map[string]string

Returns all URL path parameters as a map.

Query Parameters

c.Query(key string) string
c.QueryDefault(key, defaultValue string) string
c.AllQueries() map[string]string
// GET /search?q=golang&page=2
query := c.Query("q")           // "golang"
page := c.QueryDefault("page", "1") // "2"
all := c.AllQueries()           // map[string]string{"q": "golang", "page": "2"}

Form Data

c.FormValue(key string) string
c.FormValueDefault(key, defaultValue string) string

Returns form parameter from POST request body.

// POST with form data
username := c.FormValue("username")
role := c.FormValueDefault("role", "user")

Headers

c.Request.Header.Get(key string) string
c.RequestHeaders() map[string]string
c.ResponseHeaders() map[string]string

Request Binding

Content Type Validation

c.RequireContentType(allowed ...string) bool
c.RequireContentTypeJSON() bool
if !c.RequireContentTypeJSON() {
    return // 415 Unsupported Media Type already sent
}

Streaming

// Stream JSON array items
router.StreamJSONArray[T](c *Context, each func(T) error, maxItems int) error

// Stream NDJSON (newline-delimited JSON)
router.StreamNDJSON[T](c *Context, each func(T) error) error
err := router.StreamJSONArray(c, func(item User) error {
    return processUser(item)
}, 10000) // Max 10k items

Response Methods

JSON Responses

c.JSON(code int, obj any) error
c.IndentedJSON(code int, obj any) error
c.PureJSON(code int, obj any) error      // No HTML escaping
c.SecureJSON(code int, obj any, prefix ...string) error
c.ASCIIJSON(code int, obj any) error     // All non-ASCII escaped

Other Formats

c.YAML(code int, obj any) error
c.String(code int, value string) error
c.Stringf(code int, format string, values ...any) error
c.HTML(code int, html string) error

Binary & Streaming

c.Data(code int, contentType string, data []byte) error
c.DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) error

File Serving

c.ServeFile(filepath string)

Status & No Content

c.Status(code int)
c.NoContent()

Error Responses

c.WriteErrorResponse(status int, message string)
c.NotFound()
c.MethodNotAllowed(allowed []string)

Headers

c.Header(key, value string)

Sets a response header with automatic security sanitization (newlines stripped).

URL Information

c.Hostname() string    // Host without port
c.Port() string        // Port number
c.Scheme() string      // "http" or "https"
c.BaseURL() string     // scheme + host
c.FullURL() string     // Complete URL with query string

Client Information

c.ClientIP() string      // Real client IP (respects trusted proxies)
c.ClientIPs() []string   // All IPs from X-Forwarded-For chain
c.IsHTTPS() bool         // Request over HTTPS
c.IsLocalhost() bool     // Request from localhost
c.IsXHR() bool           // XMLHttpRequest (AJAX)
c.Subdomains(offset ...int) []string

Content Type Detection

c.IsJSON() bool      // Content-Type is application/json
c.IsXML() bool       // Content-Type is application/xml or text/xml
c.AcceptsJSON() bool // Accept header includes application/json
c.AcceptsHTML() bool // Accept header includes text/html

Content Negotiation

c.Accepts(offers ...string) string
c.AcceptsCharsets(offers ...string) string
c.AcceptsEncodings(offers ...string) string
c.AcceptsLanguages(offers ...string) string
// Accept: application/json, text/html;q=0.9
best := c.Accepts("json", "html", "xml") // "json"

// Accept-Language: en-US, fr;q=0.8
lang := c.AcceptsLanguages("en", "fr", "de") // "en"

Caching

c.IsFresh() bool  // Response still fresh in client cache
c.IsStale() bool  // Client cache is stale
if c.IsFresh() {
    c.Status(http.StatusNotModified) // 304
    return
}

Redirects

c.Redirect(code int, location string)
c.Redirect(http.StatusFound, "/login")
c.Redirect(http.StatusMovedPermanently, "https://newdomain.com")

Cookies

c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
c.GetCookie(name string) (string, error)

File Uploads

c.File(name string) (*File, error)
c.Files(name string) ([]*File, error)

File methods:

file.Bytes() ([]byte, error)
file.Open() (io.ReadCloser, error)
file.Save(dst string) error
file.Ext() string
file, err := c.File("avatar")
if err != nil {
    return c.JSON(400, map[string]string{"error": "avatar required"})
}
file.Save("./uploads/" + uuid.New().String() + file.Ext())

Middleware Control

c.Next()           // Execute next handler in chain
c.Abort()          // Stop handler chain
c.IsAborted() bool // Check if chain was aborted

Error Collection

c.Error(err error)      // Collect error without writing response
c.Errors() []error      // Get all collected errors
c.HasErrors() bool      // Check if errors were collected

Note: router.Context.Error() collects errors without writing a response or aborting the handler chain. This is useful for gathering multiple errors before deciding how to respond.

To send an error response immediately, use app.Context.Fail() which formats the error, writes the response, and stops the handler chain.

if err := validateUser(c); err != nil {
    c.Error(err)
}
if err := validateEmail(c); err != nil {
    c.Error(err)
}

if c.HasErrors() {
    c.JSON(400, map[string]any{"errors": c.Errors()})
    return
}

Context Access

c.RequestContext() context.Context  // Request's context.Context
c.TraceContext() context.Context    // OpenTelemetry trace context

Tracing & Metrics

Tracing

c.TraceID() string
c.SpanID() string
c.Span() trace.Span
c.SetSpanAttribute(key string, value any)
c.AddSpanEvent(name string, attrs ...attribute.KeyValue)

Metrics

c.RecordMetric(name string, value float64, attributes ...attribute.KeyValue)
c.IncrementCounter(name string, attributes ...attribute.KeyValue)
c.SetGauge(name string, value float64, attributes ...attribute.KeyValue)

Versioning

c.Version() string           // Current API version ("v1", "v2", etc.)
c.IsVersion(version string) bool
c.RoutePattern() string      // Matched route pattern ("/users/:id")

Complete Example

func handler(c *router.Context) {
    // Parameters
    id := c.Param("id")
    query := c.Query("q")
    
    // Headers
    auth := c.Request.Header.Get("Authorization")
    c.Header("X-Custom", "value")
    
    // Strict binding (for full binding, use binding package)
    var req CreateRequest
    if err := c.BindStrict(&req, router.BindOptions{MaxBytes: 1 << 20}); err != nil {
        return // Error response already written
    }
    
    // Tracing
    c.SetSpanAttribute("user.id", id)
    
    // Logging (pass request context for trace correlation)
    slog.InfoContext(c.RequestContext(), "processing request", "user_id", id)
    
    // Response
    if err := c.JSON(200, map[string]string{
        "id":    id,
        "query": query,
    }); err != nil {
        slog.ErrorContext(c.RequestContext(), "failed to write response", "error", err)
    }
}

Next Steps

9.4 - Router Performance

Comprehensive benchmark comparison between rivaas/router and other popular Go web frameworks, with methodology and reproduction instructions.

This page contains detailed performance benchmarks comparing rivaas/router against other popular Go web frameworks. The benchmarks measure pure routing dispatch overhead by using direct writes (via io.WriteString) in all handlers to eliminate string concatenation allocations.

Benchmark Methodology

Test Environment

  • Go Version: 1.25
  • CPU: AMD EPYC 7763 64-Core Processor
  • OS: linux/amd64
  • Last Updated: 2026-02-19

Frameworks Compared

The following frameworks are included in the comparison:

Test Scenarios

All frameworks are tested with the same three route patterns:

  1. Static route: GET /
  2. One parameter: GET /users/:id
  3. Two parameters: GET /users/:id/posts/:post_id

Handler Implementation

To ensure fair comparison and isolate routing overhead, all handlers use direct writes rather than string concatenation:

// Instead of this (causes one string allocation):
w.Write([]byte("User: " + id))

// Handlers do this (zero allocations for supported frameworks):
io.WriteString(w, "User: ")
io.WriteString(w, id)

This eliminates the handler allocation cost, so the measured time represents:

  • Route tree traversal and matching
  • Parameter extraction
  • Context setup
  • Response writer overhead (framework-specific)

Measurement Notes

  • Fiber v2/v3: Measured via net/http adaptor (fiberadaptor.FiberApp) for compatibility with httptest.ResponseRecorder. The adaptor adds overhead but is necessary for the standard test harness.
  • Hertz: Measured using ut.PerformRequest(h.Engine, ...) (Hertz’s native test API) because Hertz does not implement http.Handler. Numbers are not directly comparable to httptest-based frameworks due to different measurement approach.
  • Beego: May log “init global config instance failed” when conf/app.conf is missing; this is safe to ignore in benchmarks.

Benchmark Results

Static Route (/)

This scenario measures the overhead of dispatching a request to a static route with no parameters.

Frameworkns/opB/opallocs/opNotes
Gin61.600Zero alloc
Rivaas65.300Zero alloc
Echo80.181
StdMux82.800Zero alloc
Chi284.03682
Beego640.23604
Hertz1738.0344824via ut.PerformRequest
Fiber2203.0197620via http adaptor
FiberV37493.03309615via http adaptor

Scenario: / — Lower is better. All handlers use direct writes (io.WriteString) to minimize overhead and isolate routing cost.

Key Observations:

  • Rivaas, Gin, and StdMux achieve zero allocations with direct writes
  • Echo has 1 allocation from its internal context
  • Chi, Fiber, Hertz, and Beego have framework-specific overhead

One Parameter (/users/:id)

This scenario measures routing + parameter extraction for a single dynamic segment.

Frameworkns/opB/opallocs/opNotes
Gin96.500Zero alloc
Echo140.9162
Rivaas141.000Zero alloc
StdMux240.0161
Chi368.13682
Beego1038.04006
Hertz2070.0354427via ut.PerformRequest
Fiber2294.0206120via http adaptor
FiberV37826.03311216via http adaptor

Scenario: /users/:id — Lower is better. All handlers use direct writes (io.WriteString) to minimize overhead and isolate routing cost.

Key Observations:

  • Rivaas and Gin maintain zero allocations even with parameter extraction
  • StdMux has 1 allocation from r.PathValue()
  • Echo has 2 allocations (context + param storage)

Two Parameters (/users/:id/posts/:post_id)

This scenario tests routing with multiple dynamic segments.

Frameworkns/opB/opallocs/opNotes
Gin166.300Zero alloc
Rivaas216.300Zero alloc
Echo239.6324
StdMux431.4482
Chi465.73682
Beego1368.04488
Hertz2313.0366429via ut.PerformRequest
Fiber2370.0207620via http adaptor
FiberV38136.03312818via http adaptor

Scenario: /users/:id/posts/:post_id — Lower is better. All handlers use direct writes (io.WriteString) to minimize overhead and isolate routing cost.

Key Observations:

  • Rivaas and Gin continue to show zero allocations
  • StdMux scales linearly (2 allocs for 2 params)
  • Echo scales with each additional parameter

How to Reproduce

The benchmarks are located in the router/benchmarks directory of the rivaas repository.

Running All Benchmarks

cd router/benchmarks
go test -bench=. -benchmem

Running a Specific Scenario

# Static route only
go test -bench=BenchmarkStatic -benchmem

# One parameter only
go test -bench=BenchmarkOneParam -benchmem

# Two parameters only
go test -bench=BenchmarkTwoParams -benchmem

Running a Specific Framework

# Rivaas only
go test -bench='/(Rivaas)$' -benchmem

# Gin only
go test -bench='/(Gin)$' -benchmem

Multiple Runs for Statistical Analysis

Use -count to run benchmarks multiple times and benchstat to compare:

go test -bench=. -benchmem -count=5 > results.txt
go install golang.org/x/perf/cmd/benchstat@latest
benchstat results.txt

Understanding the Results

Metrics Explained

  • ns/op: Nanoseconds per operation (lower is better)
  • B/op: Bytes allocated per operation (lower is better)
  • allocs/op: Number of allocations per operation (lower is better)

Why Zero Allocations Matter

Each allocation has a cost:

  • Time: Allocating memory takes time (~30-50ns for small allocations)
  • GC pressure: More allocations mean more garbage collection work
  • Scalability: At high request rates (millions/sec), eliminating allocations significantly reduces CPU and memory usage

Rivaas achieves zero allocations for routing and parameter extraction by:

  • Pre-allocating context pools
  • Using array-based parameter storage for ≤8 params
  • Avoiding string concatenation in hot paths
  • Efficient radix tree implementation with minimal allocations

Continuous Benchmarking

The rivaas repository uses continuous benchmarking to detect performance regressions:

  • Pull Requests: Every PR runs Rivaas-only benchmarks and compares against a baseline. If performance regresses beyond a threshold, the PR check fails.
  • Releases: Full framework comparison runs on every release tag and updates this page automatically.

See the benchmarks.yml workflow for implementation details.


See Also

9.5 - Route Constraints

Type-safe parameter validation with route constraints.

Route constraints provide parameter validation that maps to OpenAPI schema types.

Typed Constraints

WhereInt(param string) *Route

Validates parameter as integer (OpenAPI: type: integer, format: int64).

r.GET("/users/:id", getUserHandler).WhereInt("id")

Matches:

  • /users/123
  • /users/abc

WhereFloat(param string) *Route

Validates parameter as float (OpenAPI: type: number, format: double).

r.GET("/prices/:amount", getPriceHandler).WhereFloat("amount")

Matches:

  • /prices/19.99
  • /prices/abc

WhereUUID(param string) *Route

Validates parameter as UUID (OpenAPI: type: string, format: uuid).

r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")

Matches:

  • /entities/550e8400-e29b-41d4-a716-446655440000
  • /entities/not-a-uuid

WhereDate(param string) *Route

Validates parameter as date (OpenAPI: type: string, format: date).

r.GET("/orders/:date", getOrderHandler).WhereDate("date")

Matches:

  • /orders/2024-01-18
  • /orders/invalid-date

WhereDateTime(param string) *Route

Validates parameter as date-time (OpenAPI: type: string, format: date-time).

r.GET("/events/:timestamp", getEventHandler).WhereDateTime("timestamp")

Matches:

  • /events/2024-01-18T10:30:00Z
  • /events/invalid

WhereEnum(param string, values ...string) *Route

Validates parameter against enum values (OpenAPI: enum).

r.GET("/status/:state", getStatusHandler).WhereEnum("state", "active", "pending", "deleted")

Matches:

  • /status/active
  • /status/invalid

Regex Constraints

WhereRegex(param, pattern string) *Route

Custom regex validation (OpenAPI: pattern).

// Alphanumeric only
r.GET("/slugs/:slug", getSlugHandler).WhereRegex("slug", `[a-zA-Z0-9]+`)

// Email validation
r.GET("/users/:email", getUserByEmailHandler).WhereRegex("email", `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)

Multiple Constraints

Apply multiple constraints to the same route:

r.GET("/posts/:id/:slug", getPostHandler).
    WhereInt("id").
    WhereRegex("slug", `[a-zA-Z0-9-]+`)

Common Patterns

RESTful IDs

// Integer IDs
r.GET("/users/:id", getUserHandler).WhereInt("id")
r.PUT("/users/:id", updateUserHandler).WhereInt("id")
r.DELETE("/users/:id", deleteUserHandler).WhereInt("id")

// UUID IDs
r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")

Slugs and Identifiers

// Alphanumeric slugs
r.GET("/posts/:slug", getPostBySlugHandler).WhereRegex("slug", `[a-z0-9-]+`)

// Category identifiers
r.GET("/categories/:name", getCategoryHandler).WhereRegex("name", `[a-zA-Z0-9_-]+`)

Status and States

// Enum validation for states
r.GET("/orders/:status", getOrdersByStatusHandler).WhereEnum("status", "pending", "processing", "shipped", "delivered")

Complete Example

package main

import (
    "net/http"
    "rivaas.dev/router"
)

func main() {
    r := router.New()
    
    // Integer constraint
    r.GET("/users/:id", getUserHandler).WhereInt("id")
    
    // UUID constraint
    r.GET("/entities/:uuid", getEntityHandler).WhereUUID("uuid")
    
    // Enum constraint
    r.GET("/status/:state", getStatusHandler).WhereEnum("state", "active", "inactive", "pending")
    
    // Regex constraint
    r.GET("/posts/:slug", getPostHandler).WhereRegex("slug", `[a-z0-9-]+`)
    
    // Multiple constraints
    r.GET("/articles/:id/:slug", getArticleHandler).
        WhereInt("id").
        WhereRegex("slug", `[a-z0-9-]+`)
    
    http.ListenAndServe(":8080", r)
}

func getUserHandler(c *router.Context) {
    c.JSON(200, map[string]string{"user_id": c.Param("id")})
}

func getEntityHandler(c *router.Context) {
    c.JSON(200, map[string]string{"uuid": c.Param("uuid")})
}

func getStatusHandler(c *router.Context) {
    c.JSON(200, map[string]string{"state": c.Param("state")})
}

func getPostHandler(c *router.Context) {
    c.JSON(200, map[string]string{"slug": c.Param("slug")})
}

func getArticleHandler(c *router.Context) {
    c.JSON(200, map[string]string{
        "id":   c.Param("id"),
        "slug": c.Param("slug"),
    })
}

Next Steps

9.6 - Middleware Reference

Built-in middleware catalog with configuration options.

The router includes production-ready middleware in sub-packages. Each middleware uses functional options for configuration.

Security

Security Headers

Package: rivaas.dev/router/middleware/security

import "rivaas.dev/router/middleware/security"

r.Use(security.New(
    security.WithHSTS(true),
    security.WithFrameDeny(true),
    security.WithContentTypeNosniff(true),
    security.WithXSSProtection(true),
))

CORS

Package: rivaas.dev/router/middleware/cors

import "rivaas.dev/router/middleware/cors"

r.Use(cors.New(
    cors.WithAllowedOrigins("https://example.com"),
    cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
    cors.WithAllowedHeaders("Content-Type", "Authorization"),
    cors.WithAllowCredentials(true),
    cors.WithMaxAge(3600),
))

Basic Auth

Package: rivaas.dev/router/middleware/basicauth

import "rivaas.dev/router/middleware/basicauth"

admin := r.Group("/admin")
admin.Use(basicauth.New(
    basicauth.WithCredentials("admin", "secret"),
    basicauth.WithRealm("Admin Area"),
))

Observability

Access Log

Package: rivaas.dev/router/middleware/accesslog

import (
    "log/slog"
    "rivaas.dev/router/middleware/accesslog"
)

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r.Use(accesslog.New(
    accesslog.WithLogger(logger),
    accesslog.WithExcludePaths("/health", "/metrics"),
    accesslog.WithSampleRate(0.1),
    accesslog.WithSlowThreshold(500 * time.Millisecond),
))

Request ID

Package: rivaas.dev/router/middleware/requestid

Generates unique, time-ordered request IDs for distributed tracing and log correlation.

import "rivaas.dev/router/middleware/requestid"

// UUID v7 by default (36 chars, time-ordered, RFC 9562)
r.Use(requestid.New())

// Use ULID for shorter IDs (26 chars)
r.Use(requestid.New(requestid.WithULID()))

// Custom header name
r.Use(requestid.New(requestid.WithHeader("X-Correlation-ID")))

// Get request ID in handlers
func handler(c *router.Context) {
    id := requestid.Get(c)
}

ID Formats:

  • UUID v7 (default): 018f3e9a-1b2c-7def-8000-abcdef123456
  • ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAV

Reliability

Recovery

Package: rivaas.dev/router/middleware/recovery

import "rivaas.dev/router/middleware/recovery"

r.Use(recovery.New(
    recovery.WithPrintStack(true),
    recovery.WithLogger(logger),
))

Timeout

Package: rivaas.dev/router/middleware/timeout

import "rivaas.dev/router/middleware/timeout"

r.Use(timeout.New(
    timeout.WithDuration(30 * time.Second),
    timeout.WithMessage("Request timeout"),
))

Rate Limit

Package: rivaas.dev/router/middleware/ratelimit

import "rivaas.dev/router/middleware/ratelimit"

r.Use(ratelimit.New(
    ratelimit.WithRequestsPerSecond(1000),
    ratelimit.WithBurst(100),
    ratelimit.WithKeyFunc(func(c *router.Context) string {
        return c.ClientIP() // Rate limit by IP
    }),
    ratelimit.WithLogger(logger),
))

Body Limit

Package: rivaas.dev/router/middleware/bodylimit

import "rivaas.dev/router/middleware/bodylimit"

r.Use(bodylimit.New(
    bodylimit.WithLimit(10 * 1024 * 1024), // 10MB
))

Performance

Compression

Package: rivaas.dev/router/middleware/compression

import "rivaas.dev/router/middleware/compression"

r.Use(compression.New(
    compression.WithLevel(compression.DefaultCompression),
    compression.WithMinSize(1024), // Don't compress <1KB
    compression.WithLogger(logger),
))

Other

Method Override

Package: rivaas.dev/router/middleware/methodoverride

import "rivaas.dev/router/middleware/methodoverride"

r.Use(methodoverride.New(
    methodoverride.WithHeader("X-HTTP-Method-Override"),
))

Trailing Slash

Package: rivaas.dev/router/middleware/trailingslash

import "rivaas.dev/router/middleware/trailingslash"

r.Use(trailingslash.New(
    trailingslash.WithRedirectCode(301),
))

Middleware Ordering

Recommended middleware order:

r := router.New()

// 1. Request ID
r.Use(requestid.New())

// 2. AccessLog
r.Use(accesslog.New())

// 3. Recovery
r.Use(recovery.New())

// 4. Security/CORS
r.Use(security.New())
r.Use(cors.New())

// 5. Body Limit
r.Use(bodylimit.New())

// 6. Rate Limit
r.Use(ratelimit.New())

// 7. Timeout
r.Use(timeout.New())

// 8. Authentication
r.Use(auth.New())

// 9. Compression (last)
r.Use(compression.New())

Complete Example

package main

import (
    "log/slog"
    "net/http"
    "os"
    "time"
    
    "rivaas.dev/router"
    "rivaas.dev/router/middleware/accesslog"
    "rivaas.dev/router/middleware/cors"
    "rivaas.dev/router/middleware/recovery"
    "rivaas.dev/router/middleware/requestid"
    "rivaas.dev/router/middleware/security"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    r := router.New()
    
    // Observability
    r.Use(requestid.New())
    r.Use(accesslog.New(
        accesslog.WithLogger(logger),
        accesslog.WithExcludePaths("/health"),
    ))
    
    // Reliability
    r.Use(recovery.New())
    
    // Security
    r.Use(security.New())
    r.Use(cors.New(
        cors.WithAllowedOrigins("*"),
        cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
    ))
    
    r.GET("/", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    http.ListenAndServe(":8080", r)
}

Next Steps

9.7 - Diagnostics

Diagnostic event types and handling.

The router emits optional diagnostic events for security concerns and configuration issues.

Event Types

DiagXFFSuspicious

Suspicious X-Forwarded-For chain detected (>10 IPs).

Fields:

  • chain (string) - The full X-Forwarded-For header value
  • count (int) - Number of IPs in the chain
handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    if e.Kind == router.DiagXFFSuspicious {
        log.Printf("Suspicious XFF chain: %s (count: %d)", 
            e.Fields["chain"], e.Fields["count"])
    }
})

DiagHeaderInjection

Header injection attempt blocked and sanitized.

Fields:

  • header (string) - Header name
  • value (string) - Original value
  • sanitized (string) - Sanitized value

DiagInvalidProto

Invalid X-Forwarded-Proto value.

Fields:

  • proto (string) - Invalid protocol value

DiagHighParamCount

Route has >8 parameters (uses map storage instead of array).

Fields:

  • method (string) - HTTP method
  • path (string) - Route path
  • param_count (int) - Number of parameters

DiagH2CEnabled

H2C enabled (development warning).

Fields:

  • None

Enabling Diagnostics

import "log/slog"

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    slog.Warn(e.Message, "kind", e.Kind, "fields", e.Fields)
})

r := router.New(router.WithDiagnostics(handler))

Handler Examples

With Logging

import "log/slog"

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    slog.Warn(e.Message, 
        "kind", e.Kind, 
        "fields", e.Fields,
    )
})

With Metrics

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    metrics.Increment("router.diagnostics", 
        "kind", string(e.Kind),
    )
})

With OpenTelemetry

import (
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/trace"
)

handler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
    span := trace.SpanFromContext(ctx)
    if span.IsRecording() {
        attrs := []attribute.KeyValue{
            attribute.String("diagnostic.kind", string(e.Kind)),
        }
        for k, v := range e.Fields {
            attrs = append(attrs, attribute.String(k, fmt.Sprint(v)))
        }
        span.AddEvent(e.Message, trace.WithAttributes(attrs...))
    }
})

Complete Example

package main

import (
    "log/slog"
    "net/http"
    "os"
    
    "rivaas.dev/router"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Diagnostic handler
    diagHandler := router.DiagnosticHandlerFunc(func(e router.DiagnosticEvent) {
        logger.Warn(e.Message,
            "kind", e.Kind,
            "fields", e.Fields,
        )
    })
    
    // Create router with diagnostics
    r := router.New(router.WithDiagnostics(diagHandler))
    
    r.GET("/", func(c *router.Context) {
        c.JSON(200, map[string]string{"message": "Hello"})
    })
    
    http.ListenAndServe(":8080", r)
}

Best Practices

  1. Log diagnostic events for security monitoring
  2. Track metrics for diagnostic event frequency
  3. Alert on suspicious patterns (e.g., repeated XFF warnings)
  4. Don’t ignore warnings - they indicate potential issues

Next Steps

9.8 - Troubleshooting

Common issues and solutions for the router package.

This guide helps you troubleshoot common issues with the Rivaas Router.

Quick Reference

IssueSolutionExample
404 Route Not FoundCheck route syntax and order.r.GET("/users/:id", handler)
Middleware Not RunningRegister before routes.r.Use(middleware); r.GET("/path", handler)
Parameters Not WorkingUse :param syntax.r.GET("/users/:id", handler)
CORS IssuesAdd CORS middleware.r.Use(cors.New())
Memory LeaksDon’t store context references.Extract data immediately.
Slow PerformanceUse route groups.api := r.Group("/api")

Common Issues

Route Not Found (404 errors)

Problem: Routes not matching as expected.

Solutions:

// ✅ Correct: Use :param syntax
r.GET("/users/:id", handler)

// ❌ Wrong: Don't use {param} syntax
r.GET("/users/{id}", handler)

// ✅ Correct: Static route
r.GET("/users/me", currentUserHandler)

// Check route registration order
r.GET("/users/me", currentUserHandler)      // Register specific routes first
r.GET("/users/:id", getUserHandler)         // Then parameter routes

Middleware Not Executing

Problem: Middleware doesn’t run for routes.

Solution: Register middleware before routes.

// ✅ Correct: Middleware before routes
r.Use(Logger())
r.GET("/api/users", handler)

// ❌ Wrong: Routes before middleware
r.GET("/api/users", handler)
r.Use(Logger()) // Too late!

// ✅ Correct: Group middleware
api := r.Group("/api")
api.Use(Auth())
api.GET("/users", handler)

Parameter Constraints Not Working

Problem: Invalid parameters still match routes.

Solution: Apply constraints to routes.

// ✅ Correct: Integer constraint
r.GET("/users/:id", handler).WhereInt("id")

// ✅ Correct: Custom regex
r.GET("/files/:name", handler).WhereRegex("name", `[a-zA-Z0-9.-]+`)

// ❌ Wrong: No constraint (matches anything)
r.GET("/users/:id", handler) // Matches "/users/abc"

Memory Leaks

Problem: Growing memory usage.

Solution: Never store Context references.

// ❌ Wrong: Storing context
var globalContext *router.Context
func handler(c *router.Context) {
    globalContext = c // Memory leak!
}

// ✅ Correct: Extract data immediately
func handler(c *router.Context) {
    userID := c.Param("id")
    // Use userID, not c
    processUser(userID)
}

// ✅ Correct: Copy data for async operations
func handler(c *router.Context) {
    userID := c.Param("id")
    go func(id string) {
        processAsync(id)
    }(userID)
}

CORS Issues

Problem: CORS errors in browser.

Solution: Add CORS middleware.

import "rivaas.dev/router/middleware/cors"

r.Use(cors.New(
    cors.WithAllowedOrigins("https://example.com"),
    cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
    cors.WithAllowedHeaders("Content-Type", "Authorization"),
))

Slow Performance

Problem: Routes are slow.

Solutions:

// ✅ Use route groups
api := r.Group("/api")
api.GET("/users", handler)
api.GET("/posts", handler)

// ✅ Minimize middleware
r.Use(Recovery()) // Essential only

// ✅ Apply constraints for parameter validation
r.GET("/users/:id", handler).WhereInt("id")

// ❌ Don't parse parameters manually
func handler(c *router.Context) {
    // id, err := strconv.Atoi(c.Param("id")) // Slow
    id := c.Param("id") // Fast
}

Validation Errors

Problem: Validation not working.

Solutions:

// ✅ Register custom tags in init()
func init() {
    router.RegisterTag("custom", validatorFunc)
}

// ✅ Use app.Context for binding and validation
func createUser(c *app.Context) {
    var req CreateUserRequest
    if !c.MustBind(&req) {
        return
    }
}

// ✅ Partial validation for PATCH
func updateUser(c *app.Context) {
    req, ok := app.MustBindPatch[UpdateUserRequest](c)
    if !ok {
        return
    }
}

FAQ

Can I use standard HTTP middleware?

Yes! Adapt existing middleware:

func adaptMiddleware(next http.Handler) router.HandlerFunc {
    return func(c *router.Context) {
        next.ServeHTTP(c.Writer, c.Request)
    }
}

Is the router production-ready?

Yes. The router is production-ready with:

  • 84.8% code coverage
  • Comprehensive test suite
  • Zero race conditions
  • 8.4M+ req/s throughput

How do I handle CORS?

Use the built-in CORS middleware:

import "rivaas.dev/router/middleware/cors"

r.Use(cors.New(
    cors.WithAllowedOrigins("*"),
    cors.WithAllowedMethods("GET", "POST", "PUT", "DELETE"),
))

Why are my parameters not working?

Check the parameter syntax:

// ✅ Correct
r.GET("/users/:id", handler)
id := c.Param("id")

// ❌ Wrong syntax
r.GET("/users/{id}", handler) // Use :id instead

How do I debug routing issues?

Use route introspection:

routes := r.Routes()
for _, route := range routes {
    fmt.Printf("%s %s -> %s\n", route.Method, route.Path, route.HandlerName)
}

Getting Help

Next Steps