fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+478
@@ -0,0 +1,478 @@
|
||||
# Metrics Package
|
||||
|
||||
A pluggable metrics collection system with Prometheus implementation.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
import "github.com/bitechdev/ResolveSpec/pkg/metrics"
|
||||
|
||||
// Initialize Prometheus provider with default config
|
||||
provider := metrics.NewPrometheusProvider(nil)
|
||||
metrics.SetProvider(provider)
|
||||
|
||||
// Apply middleware to your router
|
||||
router.Use(provider.Middleware)
|
||||
|
||||
// Expose metrics endpoint
|
||||
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]`
|
||||
|
||||
### Pushgateway Configuration (Optional)
|
||||
|
||||
For batch jobs, cron tasks, or short-lived processes, you can push metrics to Prometheus Pushgateway:
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `PushgatewayURL` | `string` | `""` | URL of Pushgateway (e.g., "http://pushgateway:9091") |
|
||||
| `PushgatewayJobName` | `string` | `"resolvespec"` | Job name for pushed metrics |
|
||||
| `PushgatewayInterval` | `int` | `0` | Auto-push interval in seconds (0 = disabled) |
|
||||
|
||||
```go
|
||||
config := &metrics.Config{
|
||||
PushgatewayURL: "http://pushgateway:9091",
|
||||
PushgatewayJobName: "batch-job",
|
||||
PushgatewayInterval: 30, // Push every 30 seconds
|
||||
}
|
||||
```
|
||||
|
||||
## Provider Interface
|
||||
|
||||
The package uses a provider interface, allowing you to plug in different metric systems:
|
||||
|
||||
```go
|
||||
type Provider interface {
|
||||
RecordHTTPRequest(method, path, status string, duration time.Duration)
|
||||
IncRequestsInFlight()
|
||||
DecRequestsInFlight()
|
||||
RecordDBQuery(operation, table string, duration time.Duration, err error)
|
||||
RecordCacheHit(provider string)
|
||||
RecordCacheMiss(provider string)
|
||||
UpdateCacheSize(provider string, size int64)
|
||||
Handler() http.Handler
|
||||
}
|
||||
```
|
||||
|
||||
## Recording Metrics
|
||||
|
||||
### HTTP Metrics (Automatic)
|
||||
|
||||
When using the middleware, HTTP metrics are recorded automatically:
|
||||
|
||||
```go
|
||||
router.Use(provider.Middleware)
|
||||
```
|
||||
|
||||
**Collected:**
|
||||
- Request duration (histogram)
|
||||
- Request count by method, path, and status
|
||||
- Requests in flight (gauge)
|
||||
|
||||
### Database Metrics
|
||||
|
||||
```go
|
||||
start := time.Now()
|
||||
rows, err := db.Query("SELECT * FROM users WHERE id = ?", userID)
|
||||
duration := time.Since(start)
|
||||
|
||||
metrics.GetProvider().RecordDBQuery("SELECT", "users", duration, err)
|
||||
```
|
||||
|
||||
### Cache Metrics
|
||||
|
||||
```go
|
||||
// Record cache hit
|
||||
metrics.GetProvider().RecordCacheHit("memory")
|
||||
|
||||
// Record cache miss
|
||||
metrics.GetProvider().RecordCacheMiss("memory")
|
||||
|
||||
// Update cache size
|
||||
metrics.GetProvider().UpdateCacheSize("memory", 1024)
|
||||
```
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
When using `PrometheusProvider`, the following metrics are available:
|
||||
|
||||
| Metric Name | Type | Labels | Description |
|
||||
|-------------|------|--------|-------------|
|
||||
| `http_request_duration_seconds` | Histogram | method, path, status | HTTP request duration |
|
||||
| `http_requests_total` | Counter | method, path, status | Total HTTP requests |
|
||||
| `http_requests_in_flight` | Gauge | - | Current in-flight requests |
|
||||
| `db_query_duration_seconds` | Histogram | operation, table | Database query duration |
|
||||
| `db_queries_total` | Counter | operation, table, status | Total database queries |
|
||||
| `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
|
||||
|
||||
### HTTP Request Rate
|
||||
|
||||
```promql
|
||||
rate(http_requests_total[5m])
|
||||
```
|
||||
|
||||
### HTTP Request Duration (95th percentile)
|
||||
|
||||
```promql
|
||||
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
|
||||
```
|
||||
|
||||
### Database Query Error Rate
|
||||
|
||||
```promql
|
||||
rate(db_queries_total{status="error"}[5m])
|
||||
```
|
||||
|
||||
### Cache Hit Rate
|
||||
|
||||
```promql
|
||||
rate(cache_hits_total[5m]) / (rate(cache_hits_total[5m]) + rate(cache_misses_total[5m]))
|
||||
```
|
||||
|
||||
## No-Op Provider
|
||||
|
||||
If metrics are disabled:
|
||||
|
||||
```go
|
||||
// No provider set - uses no-op provider automatically
|
||||
metrics.GetProvider().RecordHTTPRequest(...) // Does nothing
|
||||
```
|
||||
|
||||
## Custom Provider
|
||||
|
||||
Implement your own metrics provider:
|
||||
|
||||
```go
|
||||
type CustomProvider struct{}
|
||||
|
||||
func (c *CustomProvider) RecordHTTPRequest(method, path, status string, duration time.Duration) {
|
||||
// Send to your metrics system
|
||||
}
|
||||
|
||||
// Implement other Provider interface methods...
|
||||
|
||||
func (c *CustomProvider) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Return your metrics format
|
||||
})
|
||||
}
|
||||
|
||||
// Use it
|
||||
metrics.SetProvider(&CustomProvider{})
|
||||
```
|
||||
|
||||
## Pushgateway Usage
|
||||
|
||||
### Automatic Push (Batch Jobs)
|
||||
|
||||
For jobs that run periodically, use automatic pushing:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/metrics"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure with automatic pushing every 30 seconds
|
||||
config := &metrics.Config{
|
||||
Enabled: true,
|
||||
Provider: "prometheus",
|
||||
Namespace: "batch_job",
|
||||
PushgatewayURL: "http://pushgateway:9091",
|
||||
PushgatewayJobName: "data-processor",
|
||||
PushgatewayInterval: 30, // Push every 30 seconds
|
||||
}
|
||||
|
||||
provider := metrics.NewPrometheusProvider(config)
|
||||
metrics.SetProvider(provider)
|
||||
|
||||
// Ensure cleanup on exit
|
||||
defer provider.StopAutoPush()
|
||||
|
||||
// Your batch job logic here
|
||||
processBatchData()
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Push (Short-lived Processes)
|
||||
|
||||
For one-time jobs or when you want manual control:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/metrics"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure without automatic pushing
|
||||
config := &metrics.Config{
|
||||
Enabled: true,
|
||||
Provider: "prometheus",
|
||||
PushgatewayURL: "http://pushgateway:9091",
|
||||
PushgatewayJobName: "migration-job",
|
||||
// PushgatewayInterval: 0 (default - no auto-push)
|
||||
}
|
||||
|
||||
provider := metrics.NewPrometheusProvider(config)
|
||||
metrics.SetProvider(provider)
|
||||
|
||||
// Run your job
|
||||
err := runMigration()
|
||||
|
||||
// Push metrics at the end
|
||||
if pushErr := provider.Push(); pushErr != nil {
|
||||
log.Printf("Failed to push metrics: %v", pushErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker Compose with Pushgateway
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
batch-job:
|
||||
build: .
|
||||
environment:
|
||||
PUSHGATEWAY_URL: "http://pushgateway:9091"
|
||||
|
||||
pushgateway:
|
||||
image: prom/pushgateway
|
||||
ports:
|
||||
- "9091:9091"
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
```
|
||||
|
||||
**prometheus.yml for Pushgateway:**
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
# Scrape the pushgateway
|
||||
- job_name: 'pushgateway'
|
||||
honor_labels: true # Important: preserve job labels from pushed metrics
|
||||
static_configs:
|
||||
- targets: ['pushgateway:9091']
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/metrics"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize metrics with default config
|
||||
provider := metrics.NewPrometheusProvider(nil)
|
||||
metrics.SetProvider(provider)
|
||||
|
||||
// Create router
|
||||
router := mux.NewRouter()
|
||||
|
||||
// Apply metrics middleware
|
||||
router.Use(provider.Middleware)
|
||||
|
||||
// Expose metrics endpoint
|
||||
router.Handle("/metrics", provider.Handler())
|
||||
|
||||
// Your API routes
|
||||
router.HandleFunc("/api/users", getUsersHandler)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", router))
|
||||
}
|
||||
|
||||
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Record database query
|
||||
start := time.Now()
|
||||
users, err := fetchUsers()
|
||||
duration := time.Since(start)
|
||||
|
||||
metrics.GetProvider().RecordDBQuery("SELECT", "users", duration, err)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
// Return users...
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- prometheus
|
||||
```
|
||||
|
||||
**prometheus.yml:**
|
||||
|
||||
```yaml
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'resolvespec'
|
||||
static_configs:
|
||||
- targets: ['app:8080']
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Label Cardinality**: Keep labels low-cardinality
|
||||
- ✅ Good: `method`, `status_code`
|
||||
- ❌ Bad: `user_id`, `timestamp`
|
||||
|
||||
2. **Path Normalization**: Normalize dynamic paths
|
||||
```go
|
||||
// Instead of /api/users/123
|
||||
// Use /api/users/:id
|
||||
```
|
||||
|
||||
3. **Metric Naming**: Follow Prometheus conventions
|
||||
- Use `_total` suffix for counters
|
||||
- Use `_seconds` suffix for durations
|
||||
- Use base units (seconds, not milliseconds)
|
||||
|
||||
4. **Performance**: Metrics collection is lock-free and highly performant
|
||||
- Safe for high-throughput applications
|
||||
- Minimal overhead (<1% in most cases)
|
||||
|
||||
5. **Pull vs Push**:
|
||||
- **Use Pull (default)**: Long-running services, web servers, microservices
|
||||
- **Use Push (Pushgateway)**: Batch jobs, cron tasks, short-lived processes, serverless functions
|
||||
- Pull is preferred for most applications as it allows Prometheus to detect if your service is down
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
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"`
|
||||
|
||||
// PushgatewayURL is the URL of the Prometheus Pushgateway (optional)
|
||||
// If set, metrics will be pushed to this gateway instead of only being scraped
|
||||
// Example: "http://pushgateway:9091"
|
||||
PushgatewayURL string `mapstructure:"pushgateway_url"`
|
||||
|
||||
// PushgatewayJobName is the job name to use when pushing metrics to Pushgateway
|
||||
// Default: "resolvespec"
|
||||
PushgatewayJobName string `mapstructure:"pushgateway_job_name"`
|
||||
|
||||
// PushgatewayInterval is the interval at which to push metrics to Pushgateway
|
||||
// Only used if PushgatewayURL is set. If 0, automatic pushing is disabled.
|
||||
// Default: 0 (no automatic pushing)
|
||||
PushgatewayInterval int `mapstructure:"pushgateway_interval"`
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
// Set default job name if pushgateway is configured but job name is empty
|
||||
if c.PushgatewayURL != "" && c.PushgatewayJobName == "" {
|
||||
c.PushgatewayJobName = "resolvespec"
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
// Provider defines the interface for metric collection
|
||||
type Provider interface {
|
||||
// RecordHTTPRequest records metrics for an HTTP request
|
||||
RecordHTTPRequest(method, path, status string, duration time.Duration)
|
||||
|
||||
// IncRequestsInFlight increments the in-flight requests counter
|
||||
IncRequestsInFlight()
|
||||
|
||||
// DecRequestsInFlight decrements the in-flight requests counter
|
||||
DecRequestsInFlight()
|
||||
|
||||
// RecordDBQuery records metrics for a database query
|
||||
RecordDBQuery(operation, schema, entity, table string, duration time.Duration, err error)
|
||||
|
||||
// RecordCacheHit records a cache hit
|
||||
RecordCacheHit(provider string)
|
||||
|
||||
// RecordCacheMiss records a cache miss
|
||||
RecordCacheMiss(provider string)
|
||||
|
||||
// UpdateCacheSize updates the cache size metric
|
||||
UpdateCacheSize(provider string, size int64)
|
||||
|
||||
// RecordEventPublished records an event publication
|
||||
RecordEventPublished(source, eventType string)
|
||||
|
||||
// RecordEventProcessed records an event processing with its status
|
||||
RecordEventProcessed(source, eventType, status string, duration time.Duration)
|
||||
|
||||
// UpdateEventQueueSize updates the event queue size metric
|
||||
UpdateEventQueueSize(size int64)
|
||||
|
||||
// RecordPanic records a panic event
|
||||
RecordPanic(methodName string)
|
||||
|
||||
// Handler returns an HTTP handler for exposing metrics (e.g., /metrics endpoint)
|
||||
Handler() http.Handler
|
||||
}
|
||||
|
||||
// globalProvider is the global metrics provider, protected by globalProviderMu.
|
||||
var (
|
||||
globalProviderMu sync.RWMutex
|
||||
globalProvider Provider
|
||||
)
|
||||
|
||||
// SetProvider sets the global metrics provider.
|
||||
func SetProvider(p Provider) {
|
||||
globalProviderMu.Lock()
|
||||
globalProvider = p
|
||||
globalProviderMu.Unlock()
|
||||
}
|
||||
|
||||
// GetProvider returns the current metrics provider.
|
||||
func GetProvider() Provider {
|
||||
globalProviderMu.RLock()
|
||||
p := globalProvider
|
||||
globalProviderMu.RUnlock()
|
||||
if p == nil {
|
||||
return &NoOpProvider{}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// NoOpProvider is a no-op implementation of Provider
|
||||
type NoOpProvider struct{}
|
||||
|
||||
func (n *NoOpProvider) RecordHTTPRequest(method, path, status string, duration time.Duration) {}
|
||||
func (n *NoOpProvider) IncRequestsInFlight() {}
|
||||
func (n *NoOpProvider) DecRequestsInFlight() {}
|
||||
func (n *NoOpProvider) RecordDBQuery(operation, schema, entity, table string, duration time.Duration, err error) {
|
||||
}
|
||||
func (n *NoOpProvider) RecordCacheHit(provider string) {}
|
||||
func (n *NoOpProvider) RecordCacheMiss(provider string) {}
|
||||
func (n *NoOpProvider) UpdateCacheSize(provider string, size int64) {}
|
||||
func (n *NoOpProvider) RecordEventPublished(source, eventType string) {}
|
||||
func (n *NoOpProvider) RecordEventProcessed(source, eventType, status string, duration time.Duration) {
|
||||
}
|
||||
func (n *NoOpProvider) UpdateEventQueueSize(size int64) {}
|
||||
func (n *NoOpProvider) RecordPanic(methodName string) {}
|
||||
func (n *NoOpProvider) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err := w.Write([]byte("Metrics provider not configured"))
|
||||
if err != nil {
|
||||
logger.Warn("Failed to write. %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/client_golang/prometheus/push"
|
||||
)
|
||||
|
||||
// PrometheusProvider implements the Provider interface using Prometheus
|
||||
type PrometheusProvider struct {
|
||||
requestDuration *prometheus.HistogramVec
|
||||
requestTotal *prometheus.CounterVec
|
||||
requestsInFlight prometheus.Gauge
|
||||
dbQueryDuration *prometheus.HistogramVec
|
||||
dbQueryTotal *prometheus.CounterVec
|
||||
cacheHits *prometheus.CounterVec
|
||||
cacheMisses *prometheus.CounterVec
|
||||
cacheSize *prometheus.GaugeVec
|
||||
eventPublished *prometheus.CounterVec
|
||||
eventProcessed *prometheus.CounterVec
|
||||
eventDuration *prometheus.HistogramVec
|
||||
eventQueueSize prometheus.Gauge
|
||||
panicsTotal *prometheus.CounterVec
|
||||
|
||||
// Pushgateway fields (optional)
|
||||
pushgatewayURL string
|
||||
pushgatewayJobName string
|
||||
pusher *push.Pusher
|
||||
pushTicker *time.Ticker
|
||||
pushStop chan bool
|
||||
}
|
||||
|
||||
// NewPrometheusProvider creates a new Prometheus metrics provider
|
||||
// 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
|
||||
}
|
||||
|
||||
p := &PrometheusProvider{
|
||||
requestDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: metricName("http_request_duration_seconds"),
|
||||
Help: "HTTP request duration in seconds",
|
||||
Buckets: cfg.HTTPRequestBuckets,
|
||||
},
|
||||
[]string{"method", "path", "status"},
|
||||
),
|
||||
requestTotal: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: metricName("http_requests_total"),
|
||||
Help: "Total number of HTTP requests",
|
||||
},
|
||||
[]string{"method", "path", "status"},
|
||||
),
|
||||
|
||||
requestsInFlight: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: metricName("http_requests_in_flight"),
|
||||
Help: "Current number of HTTP requests being processed",
|
||||
},
|
||||
),
|
||||
dbQueryDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: metricName("db_query_duration_seconds"),
|
||||
Help: "Database query duration in seconds",
|
||||
Buckets: cfg.DBQueryBuckets,
|
||||
},
|
||||
[]string{"operation", "schema", "entity", "table"},
|
||||
),
|
||||
dbQueryTotal: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: metricName("db_queries_total"),
|
||||
Help: "Total number of database queries",
|
||||
},
|
||||
[]string{"operation", "schema", "entity", "table", "status"},
|
||||
),
|
||||
cacheHits: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: metricName("cache_hits_total"),
|
||||
Help: "Total number of cache hits",
|
||||
},
|
||||
[]string{"provider"},
|
||||
),
|
||||
cacheMisses: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: metricName("cache_misses_total"),
|
||||
Help: "Total number of cache misses",
|
||||
},
|
||||
[]string{"provider"},
|
||||
),
|
||||
cacheSize: promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
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: metricName("panics_total"),
|
||||
Help: "Total number of panics",
|
||||
},
|
||||
[]string{"method"},
|
||||
),
|
||||
|
||||
pushgatewayURL: cfg.PushgatewayURL,
|
||||
pushgatewayJobName: cfg.PushgatewayJobName,
|
||||
}
|
||||
|
||||
// Initialize pushgateway if configured
|
||||
if cfg.PushgatewayURL != "" {
|
||||
p.pusher = push.New(cfg.PushgatewayURL, cfg.PushgatewayJobName).
|
||||
Gatherer(prometheus.DefaultGatherer)
|
||||
|
||||
// Start automatic pushing if interval is configured
|
||||
if cfg.PushgatewayInterval > 0 {
|
||||
p.pushStop = make(chan bool)
|
||||
p.pushTicker = time.NewTicker(time.Duration(cfg.PushgatewayInterval) * time.Second)
|
||||
go p.startAutoPush()
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// ResponseWriter wraps http.ResponseWriter to capture status code
|
||||
type ResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
|
||||
return &ResponseWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *ResponseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// RecordHTTPRequest implements Provider interface
|
||||
func (p *PrometheusProvider) RecordHTTPRequest(method, path, status string, duration time.Duration) {
|
||||
p.requestDuration.WithLabelValues(method, path, status).Observe(duration.Seconds())
|
||||
p.requestTotal.WithLabelValues(method, path, status).Inc()
|
||||
}
|
||||
|
||||
// IncRequestsInFlight implements Provider interface
|
||||
func (p *PrometheusProvider) IncRequestsInFlight() {
|
||||
p.requestsInFlight.Inc()
|
||||
}
|
||||
|
||||
// DecRequestsInFlight implements Provider interface
|
||||
func (p *PrometheusProvider) DecRequestsInFlight() {
|
||||
p.requestsInFlight.Dec()
|
||||
}
|
||||
|
||||
// RecordDBQuery implements Provider interface
|
||||
func (p *PrometheusProvider) RecordDBQuery(operation, schema, entity, table string, duration time.Duration, err error) {
|
||||
status := "success"
|
||||
if err != nil {
|
||||
status = "error"
|
||||
}
|
||||
p.dbQueryDuration.WithLabelValues(operation, schema, entity, table).Observe(duration.Seconds())
|
||||
p.dbQueryTotal.WithLabelValues(operation, schema, entity, table, status).Inc()
|
||||
}
|
||||
|
||||
// RecordCacheHit implements Provider interface
|
||||
func (p *PrometheusProvider) RecordCacheHit(provider string) {
|
||||
p.cacheHits.WithLabelValues(provider).Inc()
|
||||
}
|
||||
|
||||
// RecordCacheMiss implements Provider interface
|
||||
func (p *PrometheusProvider) RecordCacheMiss(provider string) {
|
||||
p.cacheMisses.WithLabelValues(provider).Inc()
|
||||
}
|
||||
|
||||
// UpdateCacheSize implements Provider interface
|
||||
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()
|
||||
}
|
||||
|
||||
// Handler implements Provider interface
|
||||
func (p *PrometheusProvider) Handler() http.Handler {
|
||||
return promhttp.Handler()
|
||||
}
|
||||
|
||||
// Middleware returns an HTTP middleware that collects metrics
|
||||
func (p *PrometheusProvider) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Increment in-flight requests
|
||||
p.IncRequestsInFlight()
|
||||
defer p.DecRequestsInFlight()
|
||||
|
||||
// Wrap response writer to capture status code
|
||||
rw := NewResponseWriter(w)
|
||||
|
||||
// Call next handler
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
// Record metrics
|
||||
duration := time.Since(start)
|
||||
status := strconv.Itoa(rw.statusCode)
|
||||
|
||||
p.RecordHTTPRequest(r.Method, r.URL.Path, status, duration)
|
||||
})
|
||||
}
|
||||
|
||||
// Push manually pushes metrics to the configured Pushgateway
|
||||
// Returns an error if pushing fails or if Pushgateway is not configured
|
||||
func (p *PrometheusProvider) Push() error {
|
||||
if p.pusher == nil {
|
||||
return nil // Pushgateway not configured, silently skip
|
||||
}
|
||||
return p.pusher.Push()
|
||||
}
|
||||
|
||||
// startAutoPush runs in a goroutine and periodically pushes metrics to Pushgateway
|
||||
func (p *PrometheusProvider) startAutoPush() {
|
||||
for {
|
||||
select {
|
||||
case <-p.pushTicker.C:
|
||||
if err := p.Push(); err != nil {
|
||||
// Log error but continue pushing
|
||||
// Note: In production, you might want to use a proper logger
|
||||
_ = err
|
||||
}
|
||||
case <-p.pushStop:
|
||||
p.pushTicker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StopAutoPush stops the automatic push goroutine
|
||||
// This should be called when shutting down the application
|
||||
func (p *PrometheusProvider) StopAutoPush() {
|
||||
if p.pushStop != nil {
|
||||
close(p.pushStop)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user