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 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 containerNew() / 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
Complete documentation of the Config struct and all methods including:
- Configuration lifecycle methods
- All getter method signatures
- Error types and handling
- Nil-safety guarantees
Comprehensive list of all configuration options:
- Source options (
WithFile, WithEnv, WithConsul, etc.) - Validation options (
WithBinding, WithValidator, WithJSONSchema) - Dumper options (
WithFileDumper, WithDumper)
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
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
- 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:
- Loads data from all sources sequentially
- Merges data hierarchically (later sources override earlier ones)
- Runs JSON Schema validation (if configured)
- Runs custom validation functions (if configured)
- Binds to struct (if configured)
- 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:
Parameters:
c - Config instancekey - 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
}
| Operation | Complexity | Notes |
|---|
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_KEY → keyPREFIX_SECTION_KEY → section.keyPREFIX_A_B_C → a.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 pathcodecType - 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 bytescodecType - 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 pathcodecType - 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():
- Load and merge all sources
- JSON Schema validation (if configured)
- Custom validation functions (if configured)
- Struct binding (if configured)
- 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.
JSON Codec
Type: codec.TypeJSON
Import: rivaas.dev/config/codec
Handles JSON format encoding and decoding.
Capabilities:
File extensions:
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:
File extensions:
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:
File extensions:
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" → truefalse, "false", "False", "FALSE", 0, "0" → false
Example:
debug := cfg.Bool("debug") // Uses BoolCaster internally
Integer Casters
Convert values to integer types.
| Type | Codec | Target Type |
|---|
codec.TypeCasterInt | Int | int |
codec.TypeCasterInt8 | Int8 | int8 |
codec.TypeCasterInt16 | Int16 | int16 |
codec.TypeCasterInt32 | Int32 | int32 |
codec.TypeCasterInt64 | Int64 | int64 |
Supported inputs:
- Integer values:
42, 100 - String integers:
"42", "100" - Float values:
42.0 → 42 - 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.
| Type | Codec | Target Type |
|---|
codec.TypeCasterUint | Uint | uint |
codec.TypeCasterUint8 | Uint8 | uint8 |
codec.TypeCasterUint16 | Uint16 | uint16 |
codec.TypeCasterUint32 | Uint32 | uint32 |
codec.TypeCasterUint64 | Uint64 | uint64 |
Supported inputs:
- Positive integers:
42, 100 - String integers:
"42", "100"
Float Casters
Convert values to floating-point types.
| Type | Codec | Target Type |
|---|
codec.TypeCasterFloat32 | Float32 | float32 |
codec.TypeCasterFloat64 | Float64 | float64 |
Supported inputs:
- Float values:
3.14, 2.5 - String floats:
"3.14", "2.5" - Integer values:
42 → 42.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):
time.RFC3339 - "2006-01-02T15:04:05Z07:00"time.RFC3339Nano - "2006-01-02T15:04:05.999999999Z07:00""2006-01-02" - Date only- Unix timestamp (integer)
Duration Caster
Type: codec.TypeCasterDuration
Converts values to time.Duration.
Supported inputs:
- Duration strings:
"30s", "5m", "1h", "2h30m" - Integer nanoseconds:
30000000000 → 30s - Float seconds:
2.5 → 2.5s
Example:
timeout := cfg.Duration("timeout") // "30s" → 30 * time.Second
Duration units:
ns - nanosecondsus or µs - microsecondsms - millisecondss - secondsm - minutesh - hours
Codec Capabilities Table
| Codec | Encode | Decode | Auto-Detect | Extensions |
|---|
| JSON | ✅ | ✅ | ✅ | .json |
| YAML | ✅ | ✅ | ✅ | .yaml, .yml |
| TOML | ✅ | ✅ | ✅ | .toml |
| EnvVar | ❌ | ✅ | ❌ | - |
| Bool | ✅ | ✅ | ❌ | - |
| Int* | ✅ | ✅ | ❌ | - |
| Uint* | ✅ | ✅ | ❌ | - |
| Float* | ✅ | ✅ | ❌ | - |
| String | ✅ | ✅ | ❌ | - |
| Time | ✅ | ✅ | ❌ | - |
| Duration | ✅ | ✅ | ❌ | - |
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:
- Check file extension
- Look up registered decoder for that extension
- 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
cfg := config.MustNew(
config.WithFile("config.yaml"), // YAML
config.WithFile("secrets.json"), // JSON
config.WithFile("extra.toml"), // TOML
)
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 := cfg.Duration("timeout") // 30 * time.Second
String to Int
port := cfg.Int("port") // 8080
String to Bool
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)
}
- 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:
- 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"),
)
- Check working directory: Verify your application’s working directory.
wd, _ := os.Getwd()
fmt.Printf("Working directory: %s\n", wd)
- 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
}
Problem: File extension doesn’t match a known format.
config error in source[0] during load: no decoder registered for extension .conf
Solutions:
- Use explicit format:
cfg := config.MustNew(
config.WithFileAs("config.conf", codec.TypeYAML),
)
- 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:
- Validate YAML/JSON syntax: Use online validators or linters
- Check indentation: YAML is indentation-sensitive
- 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:
- Pass pointer to struct:
// Wrong
cfg := config.MustNew(config.WithBinding(myConfig))
// Correct
cfg := config.MustNew(config.WithBinding(&myConfig))
- 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"`
}
- 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
}
- 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:
- 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
}
- 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:
- 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
}
Provide helpful error messages: Include the actual value in error
Check validation order: Validation runs after binding
Environment Variable Issues
Environment Variables Not Loading
Problem: Environment variables are not being picked up.
Solutions:
- 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
)
- Verify environment variables are set:
- 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:
- Understand naming convention:
| Environment Variable | Config Path |
|---|
MYAPP_SERVER_PORT | server.port |
MYAPP_FOO_BAR_BAZ | foo.bar.baz |
MYAPP_FOO__BAR | foo.bar (double underscore) |
- Check case sensitivity: Environment variables are converted to lowercase
export MYAPP_SERVER_PORT=8080 # Becomes: server.port
- 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:
Check schema requirements: Ensure configuration meets schema constraints
Debug with schema validator: Use online JSON Schema validators
Provide all required fields:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"required": ["server", "database"]
}
Custom Validation Errors
Problem: Custom validation function fails.
Solutions:
- 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
})
- Check data types: Values in map might not be expected type
// Type assertion with check
if port, ok := data["port"].(int); ok {
// Use port
}
Slow Configuration Loading
Problem: Configuration loading takes too long.
Solutions:
Reduce source count: Combine configuration files when possible
Avoid remote sources in hot paths: Cache remote configuration
Profile loading:
start := time.Now()
err := cfg.Load(context.Background())
log.Printf("Config load time: %v", time.Since(start))
- Load once: Load configuration during initialization, not per-request
Memory Usage
Problem: High memory usage.
Solutions:
Don’t keep multiple Config instances: Reuse single instance
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:
- Use absolute paths in Docker:
cfg := config.MustNew(
config.WithFile("/app/config/config.yaml"),
)
- Set working directory in Dockerfile:
WORKDIR /app
COPY config.yaml .
- 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:
- Use environment variables for secrets (not config files)
- Use secret management systems (Vault, AWS Secrets Manager)
- Never commit secrets to version control
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:
- Use
WithFileDumper() to see merged config - Print values after loading
- 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 errorGetOr[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
}
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:
- Check the Configuration Guide
- Review API Reference
- Search GitHub Issues
- 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.
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:#1F2A27Quick Navigation
Core types, functions, and interfaces for request binding.
View →
Configuration options and binding settings.
View →
YAML, TOML, MessagePack, and Protocol Buffers support.
View →
Common issues and solutions for binding problems.
View →
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
| Category | Types |
|---|
| Primitives | string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool |
| Time | time.Time, time.Duration |
| Network | net.IP, net.IPNet, url.URL |
| Regex | regexp.Regexp |
| Collections | []T, map[string]T |
| Pointers | *T for any supported type |
| Nested | Nested 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),
)
Control binding behavior with struct tags:
| Tag | Purpose | Example |
|---|
json | JSON body field | json:"field_name" |
query | Query parameter | query:"param_name" |
form | Form data | form:"field_name" |
header | HTTP header | header:"X-Header-Name" |
path | Path parameter | path:"param_name" |
cookie | HTTP cookie | cookie:"cookie_name" |
default | Default value | default:"value" |
validate | Validation rules | validate:"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:
| Package | Format | Import Path |
|---|
yaml | YAML | rivaas.dev/binding/yaml |
toml | TOML | rivaas.dev/binding/toml |
msgpack | MessagePack | rivaas.dev/binding/msgpack |
proto | Protocol Buffers | rivaas.dev/binding/proto |
- 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
External Links
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())
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)
Cookie
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) errorFormTo(values url.Values, target interface{}, opts ...Option) errorMultipartTo(form *multipart.Form, target interface{}, opts ...Option) errorHeaderTo(headers http.Header, target interface{}, opts ...Option) errorCookieTo(cookies []*http.Cookie, target interface{}, opts ...Option) errorPathTo(params map[string]string, target interface{}, opts ...Option) errorXMLTo(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 unmarshalingjson.Unmarshaler: For custom JSON unmarshalingxml.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:
- Function-level options override binder-level options
- 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())
// 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-Package | Format | Import Path |
|---|
yaml | YAML | rivaas.dev/binding/yaml |
toml | TOML | rivaas.dev/binding/toml |
msgpack | MessagePack | rivaas.dev/binding/msgpack |
proto | Protocol Buffers | rivaas.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
Enables strict YAML parsing. Fails on unknown fields or duplicate keys.
Example:
config, err := yaml.YAML[Config](data, yaml.WithStrict())
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.
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.
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...
}
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:
| Package | Dependency |
|---|
yaml | gopkg.in/yaml.v3 |
toml | github.com/BurntSushi/toml |
msgpack | github.com/vmihailenco/msgpack/v5 |
proto | google.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
Approximate performance for a typical struct (10 fields):
| Format | Speed (ns/op) | Allocs | Use Case |
|---|
| JSON | 800 | 3 | Web APIs, human-readable |
| MessagePack | 500 | 2 | High performance, binary |
| Protocol Buffers | 400 | 2 | Strongly typed, cross-language |
| YAML | 1,200 | 5 | Configuration files |
| TOML | 1,000 | 4 | Configuration files |
Best Practices
- 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
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:
Field is unexported
// Wrong - unexported field
type Request struct {
name string `json:"name"` // Won't bind
}
// Correct
type Request struct {
Name string `json:"name"`
}
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
}
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"`
}
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:
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}
Use string type and convert manually
type User struct {
AgeStr string `json:"age"`
}
user, err := binding.JSON[User](data)
age, _ := strconv.Atoi(user.AgeStr)
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:
Malformed JSON
{"name": "test" // Missing closing brace
Solution: Validate JSON syntax
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)
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:
Add field to struct
type User struct {
Name string `json:"name"`
Extra string `json:"extra"`
}
Remove strict mode
user, err := binding.JSON[User](data) // Ignores extra fields
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:
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),
)
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)
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:
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
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
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 memoryJSONReader: 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:
- Check the examples: Binding Guide
- Review API docs: API Reference
- Search GitHub issues: rivaas-dev/rivaas/issues
- 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 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
Logger and ContextLogger types with all methods.
View →
Configuration options for handlers and output.
View →
Test helpers and mocking utilities.
View →
Common logging issues and solutions.
View →
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 levelsLogRequest, LogError, LogDuration - Convenience methodsSetLevel, Level - Dynamic level managementShutdown - 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 correlationTraceID, SpanID - Access trace informationLogger - 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)
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)
- 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 messageargs - 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 messageargs - 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 messageargs - 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 messageargs - 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 requestextra - 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 logmsg - Log messageextra - 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 messagestart - Operation start timeextra - 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 messageerr - Error to logincludeStack - Whether to capture and include stack traceextra - 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:
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:
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)
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)),
)
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:
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:
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 unconditionallyThereafter (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 entrieserror - 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 instanceopts - 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 entrieserror - 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 entryerror - 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 keyvalue - 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 instancelevel - Expected log levelmsg - Expected messageattrs - 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 timestampLevel - Log level (“DEBUG”, “INFO”, “WARN”, “ERROR”)Message - Log messageAttrs - 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 writeinner - 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:
- Logger shutdown: Check if logger was shut down.
if !logger.IsEnabled() {
fmt.Println("Logger is shut down")
}
- Wrong output: Verify output destination.
logger := logging.MustNew(
logging.WithOutput(os.Stdout), // Not stderr
)
- 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:
passwordtokensecretapi_keyauthorization
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:
- Tracing not initialized:
// Initialize tracing
tracer := tracing.MustNew(
tracing.WithOTLP("localhost:4317"),
)
defer tracer.Shutdown(context.Background())
- 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")
- 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
}
High CPU Usage
Problem: Logging causes high CPU usage.
Possible causes:
- 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))
- 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
)
- 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:
- 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
- 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)
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:
- Logger not set on router:
r := router.MustNew()
logger := logging.MustNew(logging.WithJSONHandler())
r.SetLogger(logger) // Must set logger
- Middleware not applied:
import "rivaas.dev/router/middleware/accesslog"
r.Use(accesslog.New()) // Apply middleware
- 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:
- Check the API Reference for method details
- Review Examples for patterns
- See Best Practices for recommendations
- 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 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 containerNew() / 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 warningWarnings type - Warning collectionWarningCode type - Type-safe warning codesWarningCategory type - Warning categories
Validator (rivaas.dev/openapi/validate)
Standalone specification validator:
Validator type - Validates OpenAPI specificationsValidate() - Validate against specific versionValidateAuto() - 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
Core types, HTTP method constructors, and generation API.
View →
API-level configuration for info, servers, and security.
View →
Operation-level configuration for endpoints.
View →
Customize the Swagger UI interface.
View →
Warning system and diagnostic codes.
View →
Common issues and solutions.
View →
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
- 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) OperationPOST(path string, ...OperationOption) OperationPUT(path string, ...OperationOption) OperationPATCH(path string, ...OperationOption) OperationDELETE(path string, ...OperationOption) OperationHEAD(path string, ...OperationOption) OperationOPTIONS(path string, ...OperationOption) OperationTRACE(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 instanceerror - 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:
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 pathopts - 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),
)
HEAD
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 cancellationoperations - Variable number of Operation instances
Returns:
*Result - Generation result with JSON, YAML, and warningserror - 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")
func WithContact(name, url, email string) Option
Sets contact information.
Parameters:
name - Contact nameurl - Contact URLemail - 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 nameurl - 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 nameidentifier - 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 URLdescription - 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 namedefaultValue - Default valueenumValues - Allowed valuesdescription - 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 nameparamName - Parameter name (e.g., “X-API-Key”, “api_key”)location - Where the key is located: InHeader, InQuery, or InCookiedescription - 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 namedescription - Scheme descriptionflows - 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 nameopenIDConnectURL - OpenID Connect discovery URLdescription - 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 namescopes - 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 namedescription - 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 URLdescription - 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 servedopts - 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).
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 schemaexamples - 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 coderesponseType - 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
func WithTags(tags ...string) OperationOption
Adds tags to the operation for organization.
Parameters:
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
| Option | Description |
|---|
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 detailsDocExpansionNone - 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 snippetslanguages - Snippet languages to show
Languages:
SnippetCurlBash - curl for bash/sh shellsSnippetCurlPowerShell - curl for PowerShellSnippetCurlCmd - 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)
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)
func WithUITagsSorter(sorter TagsSorter) UIOption
Sets tag sorting method.
Parameters:
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 accentsSyntaxThemeArta - Dark theme with orange accentsSyntaxThemeMonokai - Dark theme with vibrant colorsSyntaxThemeNord - Dark theme with cool blue tonesSyntaxThemeObsidian - Dark theme with green accentsSyntaxThemeTomorrowNight - Dark theme with muted colorsSyntaxThemeIdea - 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 codeMessage() - Returns human-readable messagePath() - Returns location in spec (e.g., “info.summary”)Category() - Returns warning category
Warnings Collection
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-safe warning code constant.
Downlevel Warning Codes
Warnings generated when using 3.1 features with a 3.0 target:
| Constant | Code Value | Description |
|---|
WarnDownlevelWebhooks | DOWNLEVEL_WEBHOOKS | Webhooks dropped (3.0 doesn’t support them) |
WarnDownlevelInfoSummary | DOWNLEVEL_INFO_SUMMARY | info.summary dropped (3.0 doesn’t support it) |
WarnDownlevelLicenseIdentifier | DOWNLEVEL_LICENSE_IDENTIFIER | license.identifier dropped |
WarnDownlevelMutualTLS | DOWNLEVEL_MUTUAL_TLS | mutualTLS security scheme dropped |
WarnDownlevelConstToEnum | DOWNLEVEL_CONST_TO_ENUM | JSON Schema const converted to enum |
WarnDownlevelConstToEnumConflict | DOWNLEVEL_CONST_TO_ENUM_CONFLICT | const conflicted with existing enum |
WarnDownlevelPathItems | DOWNLEVEL_PATH_ITEMS | $ref in pathItems was expanded |
WarnDownlevelPatternProperties | DOWNLEVEL_PATTERN_PROPERTIES | patternProperties dropped |
WarnDownlevelUnevaluatedProperties | DOWNLEVEL_UNEVALUATED_PROPERTIES | unevaluatedProperties dropped |
WarnDownlevelContentEncoding | DOWNLEVEL_CONTENT_ENCODING | contentEncoding dropped |
WarnDownlevelContentMediaType | DOWNLEVEL_CONTENT_MEDIA_TYPE | contentMediaType dropped |
WarnDownlevelMultipleExamples | DOWNLEVEL_MULTIPLE_EXAMPLES | Multiple 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:
| Constant | Code Value | Description |
|---|
WarnDeprecationExampleSingular | DEPRECATION_EXAMPLE_SINGULAR | Using deprecated singular example field |
const (
WarnDeprecationExampleSingular WarningCode = "DEPRECATION_EXAMPLE_SINGULAR"
)
Warning Categories
WarningCategory Type
type WarningCategory string
Category grouping for warnings.
Category Constants
| Category | Description |
|---|
CategoryDownlevel | 3.1 to 3.0 conversion feature losses (spec is still valid) |
CategoryDeprecation | Deprecated feature usage (feature still works but is discouraged) |
CategoryUnknown | Unrecognized 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:
| Feature | 3.0 Behavior |
|---|
info.summary | Dropped (warning) |
license.identifier | Dropped (warning) |
const in schemas | Converted to enum with single value |
examples (multiple) | Converted to single example |
webhooks | Dropped (warning) |
mutualTLS security | Dropped (warning) |
Options:
- 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
- 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
)
- 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:
- Missing required fields:
// Missing version
openapi.MustNew(
openapi.WithTitle("My API", ""), // Version required
)
- Invalid URLs:
// Invalid server URL
openapi.WithServer("not-a-url", "Server")
- Invalid enum values:
type Request struct {
Status string `json:"status" enum:"active"` // Missing comma-separated values
}
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:
- Disable validation in production:
api := openapi.MustNew(
openapi.WithTitle("API", "1.0.0"),
openapi.WithValidation(false), // Disable for production
)
- 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
}
- 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:
- Check the pkg.go.dev documentation
- Review examples
- Search GitHub issues
- 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 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 --> StdoutComponents
Main Package (rivaas.dev/metrics)
Core metrics collection including:
Recorder - Main metrics recorderNew() / 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
Recorder type, lifecycle methods, and custom metrics API.
View →
Configuration options for providers and service metadata.
View →
HTTP middleware configuration and path exclusion.
View →
Common metrics issues and solutions.
View →
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
- 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:
| Metric | Type | Description |
|---|
http_request_duration_seconds | Histogram | Request duration distribution |
http_requests_total | Counter | Total requests by method, path, status |
http_requests_active | Gauge | Currently active requests |
http_request_size_bytes | Histogram | Request body size distribution |
http_response_size_bytes | Histogram | Response body size distribution |
http_errors_total | Counter | HTTP errors by status code |
custom_metric_failures_total | Counter | Failed custom metric creations |
target_info | Gauge | OpenTelemetry 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 operationname 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 operationname 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 operationname string - Metric name (must be valid)value float64 - Value to recordattrs ...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 operationname string - Metric name (must be valid)value float64 - Value to setattrs ...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 handlererror - 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 recorderopts ...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 histogramhttp_requests_total - Request counterhttp_requests_active - Active requests gaugehttp_request_size_bytes - Request size histogramhttp_response_size_bytes - Response size histogramhttp_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 instanceserviceName string - Service name for metricsopts ...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 instanceserviceName string - Service name for metricsopts ...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
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 loggingaddress 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
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 eventsEventWarning - Warning eventsEventInfo - Informational eventsEventDebug - 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:
- Exact paths (
WithExcludePaths) - Prefixes (
WithExcludePrefixes) - Patterns (
WithExcludePatterns)
If any exclusion matches, the path is excluded.
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
The middleware automatically filters sensitive headers, even if explicitly requested.
Always Filtered Headers:
AuthorizationCookieSet-CookieX-API-KeyX-Auth-TokenProxy-AuthorizationWWW-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
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)
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.
Each header adds:
- Additional metric attribute
- Increased metric cardinality
- Higher memory usage
Recommendation: Only record necessary headers.
Troubleshooting
Path Not Excluded
Check:
- Path is exact match (use
WithExcludePaths) - Prefix includes trailing slash
- Pattern uses correct regex syntax
- Pattern is anchored (
^ and $)
Check:
- Header name is correct (case-insensitive)
- Header is not in sensitive list
- Header is present in request
High Memory Usage
Check:
- Too many unique paths (exclude high-cardinality routes)
- Too many header combinations
- 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 nameservice_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:
Metric labels: Every metric has a service_name label
http_requests_total{service_name="my-service",method="GET"} 42
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)
}
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
1. Use Default Behavior (Recommended)
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:
- Check logs: Enable logging with
WithLogger(slog.Default()) - Review configuration: Verify all options are correct
- Test connectivity: Ensure network access to endpoints
- Check version: Update to latest version
- 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 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| MiddlewareComponents
Main Package (rivaas.dev/tracing)
Core tracing functionality including:
Tracer - Main tracer for creating and managing spansNew() / MustNew() - Tracer initialization- Span management - Create, finish, add attributes/events
Middleware() - HTTP request tracingContextTracing - 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
Tracer type, span management, and context propagation.
View →
Configuration options for providers and sampling.
View →
HTTP middleware configuration and path exclusion.
View →
Common tracing issues and solutions.
View →
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
- 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:
| Aspect | Metrics Package | Tracing Package |
|---|
| Main Type | Recorder | Tracer |
| Provider Options | WithPrometheus(), WithOTLP() | WithOTLP(), WithStdout(), WithNoop() |
| Constructor | New(opts...) (*Recorder, error) | New(opts...) (*Tracer, error) |
| Panic Version | MustNew(opts...) *Recorder | MustNew(opts...) *Tracer |
| Middleware | Middleware(recorder, opts...) | Middleware(tracer, opts...) |
| Panic Middleware | MustMiddleware(recorder, opts...) | MustMiddleware(tracer, opts...) |
| Path Exclusion | MiddlewareOption | MiddlewareOption |
| Header Recording | MiddlewareOption | MiddlewareOption |
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 contextname: 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 finishstatusCode: 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 tokey: Attribute keyvalue: 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 toname: Event nameattrs: 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
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 contextheaders: 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 informationheaders: 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 contextreq: HTTP requestpath: Request pathisStatic: 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 finishstatusCode: 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 contexttracer: The Tracer instancespan: 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
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
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
| Option | Description | Default |
|---|
WithServiceName(name) | Set service name | "rivaas-service" |
WithServiceVersion(version) | Set service version | "1.0.0" |
WithNoop() | Noop provider | Yes (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 propagator | W3C Trace Context |
WithGlobalTracerProvider() | Register globally | No |
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
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..."
The following headers are automatically filtered and will never be recorded, even if explicitly included:
AuthorizationCookieSet-CookieX-API-KeyX-Auth-TokenProxy-AuthorizationWWW-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:
WithoutParams() - If set, no parameters are recordedWithExcludeParams() - Blacklist takes precedence over whitelistWithRecordParams() - Only whitelisted parameters are recorded- 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 instanceopts: 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)
| Method | Complexity | Performance |
|---|
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
| Option | Description | Default Behavior |
|---|
WithExcludePaths(paths...) | Exclude exact paths | All paths traced |
WithExcludePrefixes(prefixes...) | Exclude by prefix | All paths traced |
WithExcludePatterns(patterns...) | Exclude by regex | All paths traced |
WithHeaders(headers...) | Record headers | No headers recorded |
WithRecordParams(params...) | Whitelist params | All params recorded |
WithExcludeParams(params...) | Blacklist params | No params excluded |
WithoutParams() | Disable params | All 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",
)
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
)
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)
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
)
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
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:
Verify collector is running:
docker ps | grep otel-collector
Check endpoint is correct:
// Correct format: "host:port"
tracing.WithOTLP("localhost:4317")
// Not: "http://localhost:4317" (no protocol for gRPC)
Check network connectivity:
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
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.
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:#1F2A27Quick Navigation
Core types, functions, and validation methods.
View →
Configuration options and validator settings.
View →
Custom validation interfaces and providers.
View →
Validation strategy selection and priority.
View →
Common validation issues and solutions.
View →
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:
- Interface methods (
Validate() / ValidateContext()) - Struct tags (
validate:"...") - 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)
- 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
External Links
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 instanceerror - 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 validationval - The value to validateopts - 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 validationval - The value to validatepm - Map of present fieldsopts - 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 errorsTruncated - 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 messageMeta - 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 validationStrategyJSONSchema - Use JSON Schema validationStrategyInterface - 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 sizemaxRecursionDepth - 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)
- 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:
- Validator Creation: Pass to
New() or MustNew(). Applies to all validations. - 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):
| Option | Purpose |
|---|
WithMaxErrors | Limit total errors returned |
WithMaxFields | Limit fields in partial validation |
WithMaxCachedSchemas | Schema cache size |
WithRedactor | Redact sensitive fields |
WithCustomTag | Register custom validation tag |
WithMessages | Custom error messages |
WithMessageFunc | Dynamic error messages |
WithFieldNameMapper | Transform field names |
Per-Call Options
Options commonly used per-call (override validator config):
| Option | Purpose |
|---|
WithStrategy | Choose validation strategy |
WithRunAll | Run all strategies |
WithRequireAny | OR logic with WithRunAll |
WithPartial | Enable partial validation |
WithPresence | Set presence map |
WithMaxErrors | Override error limit |
WithCustomValidator | Add custom validator |
WithCustomSchema | Override JSON Schema |
WithDisallowUnknownFields | Reject unknown fields |
WithContext | Override context |
Usage Patterns
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.
Supported formats:
email - Email addressuri / url - URLhostname - DNS hostnameipv4 / ipv6 - IP addressesdate - Date (YYYY-MM-DD)date-time - RFC3339 date-timeuuid - 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:
ValidatorWithContext / ValidatorInterface (highest)- Struct tags (
validate:"...") 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:
- Interface Methods -
Validate() / ValidateContext() - Struct Tags -
validate:"..." tags - JSON Schema -
JSONSchemaProvider interface
Strategy Types
StrategyAuto
const StrategyAuto Strategy = iota
Automatically selects the best strategy based on the type (default behavior).
Priority Order:
- Interface methods (highest priority)
- Struct tags
- JSON Schema (lowest priority)
Example:
// Uses automatic strategy selection
err := validation.Validate(ctx, &user)
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:#1F2A27Applicability 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
| Strategy | Advantages | Disadvantages | Best For |
|---|
| Interface | Most flexible, full programmatic control | More code, not declarative | Complex business logic, database checks |
| Tags | Concise, declarative, well-documented | Limited to supported tags | Standard validation, simple rules |
| JSON Schema | Portable, language-independent | Verbose, learning curve | Shared validation with frontend |
Strategy Patterns
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)
Fastest to Slowest:
- Tags - Cached reflection, zero allocation after first use
- Interface - Direct method call, user code performance
- 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 tagsschema.* - 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:
- 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),
)
- Wrong tag name
// Wrong
Email string `validation:"required"` // Should be "validate"
// Correct
Email string `validate:"required"`
- 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:
- 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
- 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:
- 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 { ... }
- 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")
}),
)
Issue: Validation is slow
Possible Causes:
- 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
}
- 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
}
- 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:
- 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),
)
- 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
)
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:
- Check documentation: User Guide
- Review examples: Examples
- Check pkg.go.dev: API Documentation
- GitHub Issues: Report a bug
- 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
- 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
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 instanceerror - 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:
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.
func WithErrorFormatter(formatter errors.Formatter) Option
Configures a single error formatter for all error responses.
func WithErrorFormatters(formatters map[string]errors.Formatter) Option
Configures multiple error formatters with content negotiation based on Accept header.
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
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
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 indexGET /debug/pprof/cmdline - Command lineGET /debug/pprof/profile - CPU profileGET /debug/pprof/symbol - Symbol lookupGET /debug/pprof/trace - Execution traceGET /debug/pprof/allocs - Memory allocationsGET /debug/pprof/block - Block profileGET /debug/pprof/goroutine - Goroutine profileGET /debug/pprof/heap - Heap profileGET /debug/pprof/mutex - Mutex profileGET /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 useerr: 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 requestc.Response - HTTP response writerc.Param(name) - Path parameterc.Query(name) - Query parameterc.JSON(status, data) - Send JSON responsec.String(status, text) - Send text responsec.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
| Hook | Order | Error Handling | Timeout | Async |
|---|
| OnStart | Sequential | Stop on first error | No | No |
| OnReady | - | Panic caught and logged | No | Yes |
| OnReload | Sequential | Stop on first error, logged | No | No |
| OnShutdown | LIFO | Errors ignored | Yes (shutdown timeout) | No |
| OnStop | - | Panic caught and logged | No | No |
| OnRoute | Sequential | - | No | No |
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
- 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
External Links
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
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.
Security Warning
Only use in development or behind a trusted load balancer. DO NOT enable on public-facing servers without TLS.
r := router.MustNew(router.WithH2C(true))
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
))
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
Note
For tracing, metrics, and logging configuration, use the app package which provides WithObservability(), WithTracing(), WithMetrics(), and WithLogging() options. These options configure the full observability stack and integrate with the router automatically.
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.
Memory Safety
Context objects are pooled and reused. Never store references to Context beyond the request handler. Check Context Guide for details.
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"}
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")
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
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)
c.Header(key, value string)
Sets a response header with automatic security sanitization (newlines stripped).
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
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:
- Static route:
GET / - One parameter:
GET /users/:id - 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.
| Framework | ns/op | B/op | allocs/op | Notes |
|---|
| Gin | 61.6 | 0 | 0 | Zero alloc |
| Rivaas | 65.3 | 0 | 0 | Zero alloc |
| Echo | 80.1 | 8 | 1 | |
| StdMux | 82.8 | 0 | 0 | Zero alloc |
| Chi | 284.0 | 368 | 2 | |
| Beego | 640.2 | 360 | 4 | |
| Hertz | 1738.0 | 3448 | 24 | via ut.PerformRequest |
| Fiber | 2203.0 | 1976 | 20 | via http adaptor |
| FiberV3 | 7493.0 | 33096 | 15 | via 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.
| Framework | ns/op | B/op | allocs/op | Notes |
|---|
| Gin | 96.5 | 0 | 0 | Zero alloc |
| Echo | 140.9 | 16 | 2 | |
| Rivaas | 141.0 | 0 | 0 | Zero alloc |
| StdMux | 240.0 | 16 | 1 | |
| Chi | 368.1 | 368 | 2 | |
| Beego | 1038.0 | 400 | 6 | |
| Hertz | 2070.0 | 3544 | 27 | via ut.PerformRequest |
| Fiber | 2294.0 | 2061 | 20 | via http adaptor |
| FiberV3 | 7826.0 | 33112 | 16 | via 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.
| Framework | ns/op | B/op | allocs/op | Notes |
|---|
| Gin | 166.3 | 0 | 0 | Zero alloc |
| Rivaas | 216.3 | 0 | 0 | Zero alloc |
| Echo | 239.6 | 32 | 4 | |
| StdMux | 431.4 | 48 | 2 | |
| Chi | 465.7 | 368 | 2 | |
| Beego | 1368.0 | 448 | 8 | |
| Hertz | 2313.0 | 3664 | 29 | via ut.PerformRequest |
| Fiber | 2370.0 | 2076 | 20 | via http adaptor |
| FiberV3 | 8136.0 | 33128 | 18 | via 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:
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
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
))
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 valuecount (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"])
}
})
Header injection attempt blocked and sanitized.
Fields:
header (string) - Header namevalue (string) - Original valuesanitized (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 methodpath (string) - Route pathparam_count (int) - Number of parameters
DiagH2CEnabled
H2C enabled (development warning).
Fields:
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
- Log diagnostic events for security monitoring
- Track metrics for diagnostic event frequency
- Alert on suspicious patterns (e.g., repeated XFF warnings)
- 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
| Issue | Solution | Example |
|---|
| 404 Route Not Found | Check route syntax and order. | r.GET("/users/:id", handler) |
| Middleware Not Running | Register before routes. | r.Use(middleware); r.GET("/path", handler) |
| Parameters Not Working | Use :param syntax. | r.GET("/users/:id", handler) |
| CORS Issues | Add CORS middleware. | r.Use(cors.New()) |
| Memory Leaks | Don’t store context references. | Extract data immediately. |
| Slow Performance | Use 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"),
))
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