metricz

package module
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2025 License: MIT Imports: 7 Imported by: 1

README

metricz

CI Status codecov Go Report Card CodeQL Go Reference License Go Version Release

High-performance metrics collection library for Go with compile-time safety and zero dependencies.

Quick Start

package main

import (
    "fmt"
    "time"
    "github.com/zoobzio/metricz"
)

// Define metrics as constants
const (
    RequestCount = metricz.Key("http_requests_total")
    ResponseTime = metricz.Key("http_response_duration_ms")
    ActiveConns  = metricz.Key("active_connections")
)

func main() {
    // Create isolated registry
    metrics := metricz.New()
    
    // Use typed metrics
    counter := metrics.Counter(RequestCount)
    timer := metrics.Timer(ResponseTime)
    gauge := metrics.Gauge(ActiveConns)
    
    // Thread-safe operations
    counter.Inc()
    counter.Add(5)
    
    gauge.Set(10)
    gauge.Inc()
    
    stopwatch := timer.Start()
    time.Sleep(100 * time.Millisecond)
    stopwatch.Stop()
    
    fmt.Printf("Requests: %.0f\n", counter.Value())
    fmt.Printf("Connections: %.0f\n", gauge.Value())
    fmt.Printf("Response time samples: %d\n", timer.Count())
}

Core Features

Performance
  • Sub-microsecond operations: Atomic operations for metric updates
  • Zero allocations: No memory allocations in steady-state operation
  • Lock-free updates: Uses atomic operations for value changes
  • Minimal overhead: Direct atomic access without intermediate layers
Thread Safety
  • All metric operations are thread-safe
  • Multiple goroutines can update same metrics concurrently
  • Registry operations use efficient read-write mutexes
  • No external synchronization required
Registry Isolation
  • Each registry maintains completely isolated metrics
  • No global state or singleton patterns
  • Multiple registries can coexist without interference
  • Perfect for multi-tenant or component isolation
Compile-Time Safety
  • All metrics require explicit Key type declaration
  • Typos in metric names become compile errors
  • No raw strings accepted by the API
  • Consistent naming enforced at compile time

API Reference

Registry

Create and manage metric collections:

// Create new registry
registry := metricz.New()

// Get or create metrics
counter := registry.Counter(key)
gauge := registry.Gauge(key)
histogram := registry.Histogram(key, buckets)
timer := registry.Timer(key)

// Reset all metrics (useful for testing)
registry.Reset()

// Export metrics
counters := registry.GetCounters()
gauges := registry.GetGauges()
histograms := registry.GetHistograms()
timers := registry.GetTimers()
Counter

Monotonically increasing values:

const RequestsTotal = metricz.Key("requests_total")

counter := registry.Counter(RequestsTotal)
counter.Inc()        // Increment by 1
counter.Add(5.5)     // Add positive value
value := counter.Value() // Read current value
Gauge

Values that can increase or decrease:

const QueueSize = metricz.Key("queue_size")

gauge := registry.Gauge(QueueSize)
gauge.Set(100)      // Set to specific value
gauge.Inc()         // Increment by 1
gauge.Dec()         // Decrement by 1
gauge.Add(10)       // Add value (can be negative)
value := gauge.Value() // Read current value
Histogram

Track value distributions:

const ResponseSize = metricz.Key("response_size_bytes")

// Define bucket boundaries
buckets := []float64{100, 500, 1000, 5000, 10000}
hist := registry.Histogram(ResponseSize, buckets)

// Record observations
hist.Observe(756.5)
hist.Observe(1234.0)

// Read statistics
buckets, counts := hist.Buckets() // Get distribution
sum := hist.Sum()                  // Total of all observations
count := hist.Count()              // Number of observations
overflow := hist.Overflow()        // Count exceeding largest bucket
Timer

Measure durations with built-in histogram:

const ProcessingTime = metricz.Key("processing_time_ms")

timer := registry.Timer(ProcessingTime)

// Method 1: Stopwatch pattern
stopwatch := timer.Start()
// ... do work ...
stopwatch.Stop()

// Method 2: Direct recording
timer.Record(150 * time.Millisecond)

// Custom buckets (in milliseconds)
buckets := []float64{1, 5, 10, 50, 100, 500, 1000}
timer := registry.TimerWithBuckets(ProcessingTime, buckets)

// Read statistics
count := timer.Count()             // Number of recordings
sum := timer.Sum()                 // Total duration in milliseconds
buckets, counts := timer.Buckets() // Distribution

Export for Monitoring Systems

All registries provide export methods for Prometheus, StatsD, or custom monitoring:

func exportMetrics(registry *metricz.Registry) {
    // Get all metrics as maps with string keys
    counters := registry.GetCounters()
    gauges := registry.GetGauges() 
    histograms := registry.GetHistograms()
    timers := registry.GetTimers()
    
    // Export to your monitoring system
    for name, counter := range counters {
        prometheus.CounterVec.WithLabelValues(name).Set(counter.Value())
    }
}

Error Handling

metricz validates all inputs and ignores invalid values rather than panicking:

counter.Add(-1.0)         // Ignored - counters can't decrease
counter.Add(math.NaN())   // Ignored - invalid value
counter.Add(math.Inf(1))  // Ignored - invalid value

gauge.Set(math.NaN())     // Ignored - invalid value

histogram.Observe(math.NaN())  // Ignored - invalid value
histogram.Observe(math.Inf(1)) // Ignored - invalid value

Registry Patterns

Service-Level Registry
type APIService struct {
    registry *metricz.Registry
    requests metricz.Counter
    latency  metricz.Timer
}

func NewAPIService() *APIService {
    registry := metricz.New()
    return &APIService{
        registry: registry,
        requests: registry.Counter(HTTPRequestsTotal),
        latency:  registry.Timer(HTTPRequestLatency),
    }
}
Multi-Registry Isolation
func multiServiceExample() {
    // Complete isolation between services
    apiRegistry := metricz.New()
    dbRegistry := metricz.New()
    cacheRegistry := metricz.New()
    
    // Same key names, different registries - no conflicts
    apiCounter := apiRegistry.Counter(RequestsTotal)
    dbCounter := dbRegistry.Counter(RequestsTotal)
    cacheCounter := cacheRegistry.Counter(RequestsTotal)
    
    // Each tracks independently
    apiCounter.Add(100)   // API: 100
    dbCounter.Add(50)     // DB: 50  
    cacheCounter.Add(25)  // Cache: 25
}

Installation

go get github.com/zoobzio/metricz

Design Decisions

Key Type Enforcement

The library enforces use of the Key type rather than raw strings to prevent metric naming errors that commonly occur in production systems. This design decision catches typos and naming inconsistencies at compile time rather than runtime.

// Constants force consistent naming
const RequestCount = metricz.Key("requests_total")

// Compiler enforces correct usage
registry.Counter(RequestCount)     // ✓ Compiles
registry.Counter("requests_total") // ✗ Won't compile
Registry Isolation

Each registry maintains completely isolated state to prevent metric collisions in complex applications. This allows different components or teams to maintain their own metrics without coordination.

Atomic Operations

The library uses atomic operations (sync/atomic) for all metric value updates to achieve lock-free performance in hot paths. This provides sub-microsecond update times with zero lock contention.

Zero Dependencies

Metricz depends only on the Go standard library (sync and time packages) to maximize compatibility and minimize security surface area. The optional clockz dependency enables deterministic testing.

Bucket Design

Histograms and timers use pre-defined buckets rather than computing quantiles to maintain predictable memory usage and consistent performance regardless of observation count.

No Global Registry

Global state creates dependency injection problems, testing complications, and prevents service isolation. Explicit registries make dependencies clear and testing straightforward. Each registry maintains complete isolation:

// Clear dependency management
type Service struct {
    metrics *metricz.Registry
}

// Easy testing with fresh registries
func TestServiceMetrics(t *testing.T) {
    registry := metricz.New()
    // Test in isolation
}
No Metric Labels/Tags

Labels multiply cardinality and can cause memory issues in production. For high-cardinality data, use multiple registries or separate keys. This design keeps the library simple, predictable, and prevents unbounded memory growth:

// Instead of labels
// metric{service="api", method="GET", endpoint="/users"}

// Use explicit keys or separate registries
const (
    APIGetUsers  = metricz.Key("api_get_users")
    APIPostUsers = metricz.Key("api_post_users")
)

Compatibility

  • Go version: 1.23.2+
  • Dependencies: None - pure Go standard library
  • Platforms: All platforms supported by Go
  • API stability: Semantic versioning with backwards compatibility guarantee

Documentation

Overview

Package metricz provides a type-safe, zero-dependency metrics collection library designed for high-performance applications requiring compile-time safety guarantees.

Core Philosophy

Metricz enforces type safety through its Key-based API, preventing raw string usage that commonly leads to metric naming inconsistencies and runtime errors. All metric operations require explicit Key types, providing compile-time verification of metric names and eliminating string-based typos.

Key-Enforced API

The foundation of metricz is the Key type, which wraps strings and forces explicit declaration of all metric names:

const (
    RequestCount = metricz.Key("http_requests_total")
    ResponseTime = metricz.Key("http_response_duration_seconds")
)

Raw strings are rejected by the API - all methods accept only Key parameters, ensuring metric names are declared as constants and preventing runtime typos.

Four Metric Types

Metricz implements four core metric types with atomic operations:

Counter: Monotonically increasing values for event counting

counter := registry.Counter(RequestCount)
counter.Inc()
counter.Add(5)

Gauge: Arbitrary values that can increase or decrease

gauge := registry.Gauge(MemoryUsage)
gauge.Set(1024)
gauge.Add(512)
gauge.Sub(256)

Histogram: Distribution tracking with configurable buckets

hist := registry.Histogram(ResponseSize, []float64{100, 1000, 10000})
hist.Observe(756.5)

Histograms are general-purpose primitives requiring explicit bucket configuration. For common use cases, consider predefined buckets (DefaultSizeBuckets, DefaultDurationBuckets) or specialized metrics like Timer.

Timer: Duration tracking with sensible latency defaults

timer := registry.Timer(ProcessingTime)  // uses DefaultLatencyBuckets
stop := timer.Start()
// ... work ...
stop()

Timer is a specialized histogram optimized for latency tracking in milliseconds. For custom buckets, use TimerWithBuckets.

Registry Pattern

The Registry provides complete instance isolation, allowing multiple independent metric collections within the same application. Each registry maintains its own metric instances with no cross-registry interference:

registry := metricz.New()
counter := registry.Counter(RequestCount)

Registry operations are thread-safe and use atomic operations where possible for optimal performance under concurrent access.

Thread-Safety Guarantees

All metric operations are thread-safe and designed for high-concurrency environments:

- Metric value updates use atomic operations (sync/atomic) for zero-lock performance - Registry operations use read-write mutexes for efficient concurrent access - Multiple goroutines can safely access the same metrics simultaneously - No external synchronization required for any operations

Zero-Allocation Performance

Metricz is optimized for zero-allocation metric updates in steady-state operation:

- Metric instances are created once and reused - Value updates use atomic operations without allocation - String conversions are minimized and cached where possible - Memory pools are avoided in favor of atomic operations

Zero Dependencies

Metricz depends only on the Go standard library, specifically: - sync (for mutexes and atomic operations) - time (for timer functionality) - No external dependencies for maximum compatibility and minimal attack surface

Example Usage

// Declare metric keys as constants
const (
    RequestsTotal = metricz.Key("http_requests_total")
    ResponseDuration = metricz.Key("http_response_duration_seconds")
)

// Create registry
metrics := metricz.New()

// Use metrics with type safety
counter := metrics.Counter(RequestsTotal)
timer := metrics.Timer(ResponseDuration)

// Thread-safe operations
counter.Inc()
stop := timer.Start()
defer stop()

This design ensures metric collection adds minimal overhead while providing maximum safety and consistency across application boundaries.

Index

Constants

This section is empty.

Variables

View Source
var (
	// DefaultLatencyBuckets provides reasonable latency buckets in milliseconds.
	DefaultLatencyBuckets = []float64{
		1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000,
	}

	// DefaultSizeBuckets provides reasonable size buckets in bytes.
	DefaultSizeBuckets = []float64{
		64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304,
	}

	// DefaultDurationBuckets provides reasonable duration buckets in seconds.
	DefaultDurationBuckets = []float64{
		0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
	}
)

Standard bucket definitions for different types of histograms.

Functions

This section is empty.

Types

type Counter

type Counter interface {
	Inc()
	Add(float64)
	Set(float64)
	Value() float64
}

Counter interface for metrics that only increase.

type Gauge

type Gauge interface {
	Set(float64)
	Add(float64)
	Inc()
	Dec()
	Value() float64
}

Gauge interface for metrics that can increase and decrease.

type Histogram

type Histogram interface {
	Observe(float64)
	Buckets() ([]float64, []uint64)
	Sum() float64
	Count() uint64
	Overflow() uint64
}

Histogram interface for distributional metrics.

type Key

type Key string

Key is the mandatory key type for all metric operations. No raw strings allowed - compile-time enforcement.

type Registry

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

Registry is the metric collection with complete instance isolation. Only accepts Key type - no raw strings, no generics.

func New

func New() *Registry

New creates a Registry that accepts ONLY Key types. No raw strings allowed - forces explicit Key usage. Uses production clock for all timing operations.

func (*Registry) Counter

func (r *Registry) Counter(key Key) Counter

Counter returns a counter metric, creating it if it doesn't exist.

func (*Registry) Gauge

func (r *Registry) Gauge(key Key) Gauge

Gauge returns a gauge metric, creating it if it doesn't exist.

func (*Registry) GetCounters

func (r *Registry) GetCounters() map[Key]Counter

GetCounters returns a copy of all counters for export tools.

func (*Registry) GetGauges

func (r *Registry) GetGauges() map[Key]Gauge

GetGauges returns a copy of all gauges for export tools.

func (*Registry) GetHistograms

func (r *Registry) GetHistograms() map[Key]Histogram

GetHistograms returns a copy of all histograms for export tools.

func (*Registry) GetTimers

func (r *Registry) GetTimers() map[Key]Timer

GetTimers returns a copy of all timers for export tools.

func (*Registry) Histogram

func (r *Registry) Histogram(key Key, buckets []float64) Histogram

Histogram returns a histogram metric, creating it if it doesn't exist. Requires explicit bucket configuration - this is a general-purpose primitive where bucket design depends entirely on your domain (bytes, counts, percentages, etc).

For common cases, consider using predefined buckets:

  • DefaultSizeBuckets for byte measurements
  • DefaultDurationBuckets for duration in seconds
  • DefaultLatencyBuckets via Timer() for latency in milliseconds

func (*Registry) Reset

func (r *Registry) Reset()

Reset clears all metrics for clean test slate.

func (*Registry) Timer

func (r *Registry) Timer(key Key) Timer

Timer returns a timer metric with DefaultLatencyBuckets (in milliseconds). This covers typical web/API latencies from 1ms to 10s and is suitable for most HTTP request timing, RPC calls, and general operation latency tracking.

Timer is a convenience wrapper for the common latency-tracking use case. For custom buckets, use TimerWithBuckets.

func (*Registry) TimerWithBuckets

func (r *Registry) TimerWithBuckets(key Key, buckets []float64) Timer

TimerWithBuckets returns a timer metric with custom buckets (in milliseconds), creating it if it doesn't exist. Use this when your latency profile differs significantly from typical web requests (e.g., database queries, batch operations).

func (*Registry) WithClock

func (r *Registry) WithClock(clock clockz.Clock) *Registry

WithClock replaces the clock in this Registry and returns the Registry for chaining. Primarily used for testing with fake clocks.

type Stopwatch

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

Stopwatch provides convenient timing functionality.

func (*Stopwatch) Stop

func (s *Stopwatch) Stop()

Stop records the elapsed time since Start(). Uses injected clock for deterministic timing.

type Timer

type Timer interface {
	Record(time.Duration)
	Start() *Stopwatch
	// Access underlying histogram data
	Sum() float64
	Count() uint64
	Buckets() ([]float64, []uint64)
	Overflow() uint64
}

Timer interface for timing metrics.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL