Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ var NoopMetrics Metrics = &noopMetrics{}
func NewPrometheusMetrics(namespace string) Metrics
```

NewPrometheusMetrics creates a Metrics implementation backed by Prometheus. The namespace is prepended to all metric names \(e.g., "myapp" → "myapp\_worker\_started\_total"\). Metrics are auto\-registered with the default Prometheus registry.
NewPrometheusMetrics creates a Metrics implementation backed by Prometheus. The namespace is prepended to all metric names \(e.g., "myapp" → "myapp\_worker\_started\_total"\). Metrics are auto\-registered with the default Prometheus registry. Safe to call multiple times with the same namespace — returns the cached instance. The cache is process\-global; use a small number of static namespaces \(not per\-request/tenant values\).

<a name="RunOption"></a>
## type RunOption
Expand Down
27 changes: 25 additions & 2 deletions metrics.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package workers

import (
"sync"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
promMetricsMu sync.RWMutex
promMetricsCache = map[string]*prometheusMetrics{}
)

// Metrics collects worker lifecycle metrics.
// Implement this interface to provide custom metrics (e.g., Datadog, StatsD).
// Use NoopMetrics to disable metrics, or NewPrometheusMetrics for the built-in
Expand Down Expand Up @@ -50,9 +56,24 @@ type prometheusMetrics struct {
// NewPrometheusMetrics creates a Metrics implementation backed by Prometheus.
// The namespace is prepended to all metric names (e.g., "myapp" →
// "myapp_worker_started_total"). Metrics are auto-registered with the
// default Prometheus registry.
// default Prometheus registry. Safe to call multiple times with the same
// namespace — returns the cached instance. The cache is process-global;
// use a small number of static namespaces (not per-request/tenant values).
func NewPrometheusMetrics(namespace string) Metrics {
return &prometheusMetrics{
promMetricsMu.RLock()
if m, ok := promMetricsCache[namespace]; ok {
promMetricsMu.RUnlock()
return m
}
promMetricsMu.RUnlock()

promMetricsMu.Lock()
defer promMetricsMu.Unlock()
// Double-check after acquiring write lock.
if m, ok := promMetricsCache[namespace]; ok {
return m
}
m := &prometheusMetrics{
started: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Name: "worker_started_total",
Expand Down Expand Up @@ -90,6 +111,8 @@ func NewPrometheusMetrics(namespace string) Metrics {
Help: "Number of currently active workers.",
}),
}
promMetricsCache[namespace] = m
return m
}

func (p *prometheusMetrics) WorkerStarted(name string) {
Expand Down
28 changes: 28 additions & 0 deletions metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,31 @@ func TestMetrics_NoopDefault(t *testing.T) {

Run(ctx, []*Worker{w}) // no WithMetrics — uses NoopMetrics
}

func TestNewPrometheusMetrics_CachesSameNamespace(t *testing.T) {
m1 := NewPrometheusMetrics("test_cache")
m2 := NewPrometheusMetrics("test_cache")
assert.Same(t, m1, m2, "same namespace should return same instance")
}

func TestNewPrometheusMetrics_DifferentNamespace(t *testing.T) {
m1 := NewPrometheusMetrics("ns_a")
m2 := NewPrometheusMetrics("ns_b")
assert.NotSame(t, m1, m2, "different namespaces should return different instances")
}

func TestNewPrometheusMetrics_ConcurrentSafe(t *testing.T) {
var wg sync.WaitGroup
results := make([]Metrics, 100)
for i := range results {
wg.Add(1)
go func(i int) {
defer wg.Done()
results[i] = NewPrometheusMetrics("concurrent_test")
}(i)
}
wg.Wait()
for _, m := range results {
assert.Same(t, results[0], m, "all concurrent calls should return same instance")
}
}
Loading