diff --git a/pkg/metrics/README.md b/pkg/metrics/README.md index 19fa6e7..99fd250 100644 --- a/pkg/metrics/README.md +++ b/pkg/metrics/README.md @@ -7,8 +7,8 @@ A pluggable metrics collection system with Prometheus implementation. ```go import "github.com/bitechdev/ResolveSpec/pkg/metrics" -// Initialize Prometheus provider -provider := metrics.NewPrometheusProvider() +// Initialize Prometheus provider with default config +provider := metrics.NewPrometheusProvider(nil) metrics.SetProvider(provider) // Apply middleware to your router @@ -18,6 +18,41 @@ router.Use(provider.Middleware) http.Handle("/metrics", provider.Handler()) ``` +## Configuration + +You can customize the metrics provider using a configuration struct: + +```go +import "github.com/bitechdev/ResolveSpec/pkg/metrics" + +// Create custom configuration +config := &metrics.Config{ + Enabled: true, + Provider: "prometheus", + Namespace: "myapp", // Prefix all metrics with "myapp_" + HTTPRequestBuckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5}, + DBQueryBuckets: []float64{0.001, 0.01, 0.05, 0.1, 0.5, 1}, +} + +// Initialize with custom config +provider := metrics.NewPrometheusProvider(config) +metrics.SetProvider(provider) +``` + +### Configuration Options + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | `bool` | `true` | Enable/disable metrics collection | +| `Provider` | `string` | `"prometheus"` | Metrics provider type | +| `Namespace` | `string` | `""` | Prefix for all metric names | +| `HTTPRequestBuckets` | `[]float64` | See below | Histogram buckets for HTTP duration (seconds) | +| `DBQueryBuckets` | `[]float64` | See below | Histogram buckets for DB query duration (seconds) | + +**Default HTTP Request Buckets:** `[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]` + +**Default DB Query Buckets:** `[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]` + ## Provider Interface The package uses a provider interface, allowing you to plug in different metric systems: @@ -87,6 +122,13 @@ When using `PrometheusProvider`, the following metrics are available: | `cache_hits_total` | Counter | provider | Total cache hits | | `cache_misses_total` | Counter | provider | Total cache misses | | `cache_size_items` | Gauge | provider | Current cache size | +| `events_published_total` | Counter | source, event_type | Total events published | +| `events_processed_total` | Counter | source, event_type, status | Total events processed | +| `event_processing_duration_seconds` | Histogram | source, event_type | Event processing duration | +| `event_queue_size` | Gauge | - | Current event queue size | +| `panics_total` | Counter | method | Total panics recovered | + +**Note:** If a custom `Namespace` is configured, all metric names will be prefixed with `{namespace}_`. ## Prometheus Queries @@ -148,6 +190,8 @@ metrics.SetProvider(&CustomProvider{}) ## Complete Example +### Basic Usage + ```go package main @@ -162,8 +206,8 @@ import ( ) func main() { - // Initialize metrics - provider := metrics.NewPrometheusProvider() + // Initialize metrics with default config + provider := metrics.NewPrometheusProvider(nil) metrics.SetProvider(provider) // Create router @@ -198,6 +242,42 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { } ``` +### With Custom Configuration + +```go +package main + +import ( + "log" + "net/http" + + "github.com/bitechdev/ResolveSpec/pkg/metrics" + "github.com/gorilla/mux" +) + +func main() { + // Custom metrics configuration + metricsConfig := &metrics.Config{ + Enabled: true, + Provider: "prometheus", + Namespace: "myapp", + // Custom buckets optimized for your application + HTTPRequestBuckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10}, + DBQueryBuckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1}, + } + + // Initialize with custom config + provider := metrics.NewPrometheusProvider(metricsConfig) + metrics.SetProvider(provider) + + router := mux.NewRouter() + router.Use(provider.Middleware) + router.Handle("/metrics", provider.Handler()) + + log.Fatal(http.ListenAndServe(":8080", router)) +} +``` + ## Docker Compose Example ```yaml diff --git a/pkg/metrics/config.go b/pkg/metrics/config.go new file mode 100644 index 0000000..b3bd2b3 --- /dev/null +++ b/pkg/metrics/config.go @@ -0,0 +1,46 @@ +package metrics + +// Config holds configuration for the metrics provider +type Config struct { + // Enabled determines whether metrics collection is enabled + Enabled bool `mapstructure:"enabled"` + + // Provider specifies which metrics provider to use (prometheus, noop) + Provider string `mapstructure:"provider"` + + // Namespace is an optional prefix for all metric names + Namespace string `mapstructure:"namespace"` + + // HTTPRequestBuckets defines histogram buckets for HTTP request duration (in seconds) + // Default: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] + HTTPRequestBuckets []float64 `mapstructure:"http_request_buckets"` + + // DBQueryBuckets defines histogram buckets for database query duration (in seconds) + // Default: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5] + DBQueryBuckets []float64 `mapstructure:"db_query_buckets"` +} + +// DefaultConfig returns a Config with sensible defaults +func DefaultConfig() *Config { + return &Config{ + Enabled: true, + Provider: "prometheus", + // HTTP requests typically take longer than DB queries + HTTPRequestBuckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}, + // DB queries are usually faster + DBQueryBuckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5}, + } +} + +// ApplyDefaults fills in any missing values with defaults +func (c *Config) ApplyDefaults() { + if c.Provider == "" { + c.Provider = "prometheus" + } + if len(c.HTTPRequestBuckets) == 0 { + c.HTTPRequestBuckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10} + } + if len(c.DBQueryBuckets) == 0 { + c.DBQueryBuckets = []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5} + } +} diff --git a/pkg/metrics/example_test.go b/pkg/metrics/example_test.go new file mode 100644 index 0000000..35f8788 --- /dev/null +++ b/pkg/metrics/example_test.go @@ -0,0 +1,64 @@ +package metrics_test + +import ( + "fmt" + + "github.com/bitechdev/ResolveSpec/pkg/metrics" +) + +// ExampleNewPrometheusProvider_default demonstrates using default configuration +func ExampleNewPrometheusProvider_default() { + // Initialize with default configuration + provider := metrics.NewPrometheusProvider(nil) + metrics.SetProvider(provider) + + fmt.Println("Provider initialized with defaults") + // Output: Provider initialized with defaults +} + +// ExampleNewPrometheusProvider_custom demonstrates using custom configuration +func ExampleNewPrometheusProvider_custom() { + // Create custom configuration + config := &metrics.Config{ + Enabled: true, + Provider: "prometheus", + Namespace: "myapp", + HTTPRequestBuckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5}, + DBQueryBuckets: []float64{0.001, 0.01, 0.05, 0.1, 0.5, 1}, + } + + // Initialize with custom configuration + provider := metrics.NewPrometheusProvider(config) + metrics.SetProvider(provider) + + fmt.Println("Provider initialized with custom config") + // Output: Provider initialized with custom config +} + +// ExampleDefaultConfig demonstrates getting default configuration +func ExampleDefaultConfig() { + config := metrics.DefaultConfig() + fmt.Printf("Default provider: %s\n", config.Provider) + fmt.Printf("Default enabled: %v\n", config.Enabled) + // Output: + // Default provider: prometheus + // Default enabled: true +} + +// ExampleConfig_ApplyDefaults demonstrates applying defaults to partial config +func ExampleConfig_ApplyDefaults() { + // Create partial configuration + config := &metrics.Config{ + Namespace: "myapp", + // Other fields will be filled with defaults + } + + // Apply defaults + config.ApplyDefaults() + + fmt.Printf("Provider: %s\n", config.Provider) + fmt.Printf("Namespace: %s\n", config.Namespace) + // Output: + // Provider: prometheus + // Namespace: myapp +} diff --git a/pkg/metrics/prometheus.go b/pkg/metrics/prometheus.go index 9761a68..1a6f08a 100644 --- a/pkg/metrics/prometheus.go +++ b/pkg/metrics/prometheus.go @@ -20,23 +20,44 @@ type PrometheusProvider struct { cacheHits *prometheus.CounterVec cacheMisses *prometheus.CounterVec cacheSize *prometheus.GaugeVec + eventPublished *prometheus.CounterVec + eventProcessed *prometheus.CounterVec + eventDuration *prometheus.HistogramVec + eventQueueSize prometheus.Gauge panicsTotal *prometheus.CounterVec } // NewPrometheusProvider creates a new Prometheus metrics provider -func NewPrometheusProvider() *PrometheusProvider { +// If cfg is nil, default configuration will be used +func NewPrometheusProvider(cfg *Config) *PrometheusProvider { + // Use default config if none provided + if cfg == nil { + cfg = DefaultConfig() + } else { + // Apply defaults for any missing values + cfg.ApplyDefaults() + } + + // Helper to add namespace prefix if configured + metricName := func(name string) string { + if cfg.Namespace != "" { + return cfg.Namespace + "_" + name + } + return name + } + return &PrometheusProvider{ requestDuration: promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "http_request_duration_seconds", + Name: metricName("http_request_duration_seconds"), Help: "HTTP request duration in seconds", - Buckets: prometheus.DefBuckets, + Buckets: cfg.HTTPRequestBuckets, }, []string{"method", "path", "status"}, ), requestTotal: promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "http_requests_total", + Name: metricName("http_requests_total"), Help: "Total number of HTTP requests", }, []string{"method", "path", "status"}, @@ -44,49 +65,77 @@ func NewPrometheusProvider() *PrometheusProvider { requestsInFlight: promauto.NewGauge( prometheus.GaugeOpts{ - Name: "http_requests_in_flight", + Name: metricName("http_requests_in_flight"), Help: "Current number of HTTP requests being processed", }, ), dbQueryDuration: promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "db_query_duration_seconds", + Name: metricName("db_query_duration_seconds"), Help: "Database query duration in seconds", - Buckets: prometheus.DefBuckets, + Buckets: cfg.DBQueryBuckets, }, []string{"operation", "table"}, ), dbQueryTotal: promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "db_queries_total", + Name: metricName("db_queries_total"), Help: "Total number of database queries", }, []string{"operation", "table", "status"}, ), cacheHits: promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "cache_hits_total", + Name: metricName("cache_hits_total"), Help: "Total number of cache hits", }, []string{"provider"}, ), cacheMisses: promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "cache_misses_total", + Name: metricName("cache_misses_total"), Help: "Total number of cache misses", }, []string{"provider"}, ), cacheSize: promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "cache_size_items", + Name: metricName("cache_size_items"), Help: "Number of items in cache", }, []string{"provider"}, ), + eventPublished: promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: metricName("events_published_total"), + Help: "Total number of events published", + }, + []string{"source", "event_type"}, + ), + eventProcessed: promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: metricName("events_processed_total"), + Help: "Total number of events processed", + }, + []string{"source", "event_type", "status"}, + ), + eventDuration: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: metricName("event_processing_duration_seconds"), + Help: "Event processing duration in seconds", + Buckets: cfg.DBQueryBuckets, // Events are typically fast like DB queries + }, + []string{"source", "event_type"}, + ), + eventQueueSize: promauto.NewGauge( + prometheus.GaugeOpts{ + Name: metricName("event_queue_size"), + Help: "Current number of events in queue", + }, + ), panicsTotal: promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "panics_total", + Name: metricName("panics_total"), Help: "Total number of panics", }, []string{"method"}, @@ -153,6 +202,22 @@ func (p *PrometheusProvider) UpdateCacheSize(provider string, size int64) { p.cacheSize.WithLabelValues(provider).Set(float64(size)) } +// RecordEventPublished implements Provider interface +func (p *PrometheusProvider) RecordEventPublished(source, eventType string) { + p.eventPublished.WithLabelValues(source, eventType).Inc() +} + +// RecordEventProcessed implements Provider interface +func (p *PrometheusProvider) RecordEventProcessed(source, eventType, status string, duration time.Duration) { + p.eventProcessed.WithLabelValues(source, eventType, status).Inc() + p.eventDuration.WithLabelValues(source, eventType).Observe(duration.Seconds()) +} + +// UpdateEventQueueSize implements Provider interface +func (p *PrometheusProvider) UpdateEventQueueSize(size int64) { + p.eventQueueSize.Set(float64(size)) +} + // RecordPanic implements the Provider interface func (p *PrometheusProvider) RecordPanic(methodName string) { p.panicsTotal.WithLabelValues(methodName).Inc()