From a1afd3d546f6003de7cae88cc66fe357d242311f Mon Sep 17 00:00:00 2001 From: raf555 Date: Wed, 6 May 2026 15:03:15 +0700 Subject: [PATCH 1/5] feat: implement prom metrics --- go.mod | 7 +++ go.sum | 14 +++++ melt/prommetric/label.go | 13 ++++ melt/prommetric/metrics.go | 121 ++++++++++++++++++++++++++++++++++++ melt/prommetric/options.go | 22 +++++++ melt/prommetric/recorder.go | 28 +++++++++ 6 files changed, 205 insertions(+) create mode 100644 melt/prommetric/label.go create mode 100644 melt/prommetric/metrics.go create mode 100644 melt/prommetric/options.go create mode 100644 melt/prommetric/recorder.go diff --git a/go.mod b/go.mod index 4847f06..18ba364 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-playground/validator/v10 v10.30.1 github.com/infisical/go-sdk v0.6.5 github.com/joho/godotenv v1.5.1 + github.com/prometheus/client_golang v1.23.2 github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/remychantenay/slog-otel v1.3.4 github.com/samber/slog-formatter v1.2.2 @@ -42,6 +43,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/smithy-go v1.24.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -65,9 +67,13 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oracle/oci-go-sdk/v65 v65.95.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rs/zerolog v1.26.1 // indirect github.com/samber/lo v1.52.0 // indirect github.com/samber/slog-common v0.19.0 // indirect @@ -84,6 +90,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.49.0 // indirect diff --git a/go.sum b/go.sum index 006f85e..c09d632 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -135,6 +137,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oracle/oci-go-sdk/v65 v65.95.2 h1:0HJ0AgpLydp/DtvYrF2d4str2BjXOVAeNbuW7E07g94= github.com/oracle/oci-go-sdk/v65 v65.95.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -145,7 +149,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/remychantenay/slog-otel v1.3.4 h1:xoM41ayLff2U8zlK5PH31XwD7Lk3W9wKfl4+RcmKom4= @@ -223,6 +235,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/melt/prommetric/label.go b/melt/prommetric/label.go new file mode 100644 index 0000000..aa293ae --- /dev/null +++ b/melt/prommetric/label.go @@ -0,0 +1,13 @@ +package prommetric + +// Label is a convenient wrapper of anything. +// Zero Label can provide label names with Labels(). +type Label interface { + // Labels returns label names of this Label. + // It should be a static slice with fixed length. + Labels() []string + + // Values returns values of this Label. + // The length is the same as returned by Labels. + Values() []string +} diff --git a/melt/prommetric/metrics.go b/melt/prommetric/metrics.go new file mode 100644 index 0000000..7e7a763 --- /dev/null +++ b/melt/prommetric/metrics.go @@ -0,0 +1,121 @@ +package prommetric + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type Metrics[T Label] struct { + counter *prometheus.CounterVec + gauge *prometheus.GaugeVec + duration *prometheus.HistogramVec +} + +func New[T Label](prefix, name string, opts ...Option) Recorder[T] { + o := &options{ + buckets: prometheus.DefBuckets, + registerer: prometheus.DefaultRegisterer, + } + for _, opt := range opts { + opt(o) + } + + var zeroLabel T + factory := promauto.With(o.registerer) + + counter := factory.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: prefix, + Name: name + "_total", + Help: name + " counter", + }, + zeroLabel.Labels(), + ) + + gauge := factory.NewGaugeVec( + prometheus.GaugeOpts{ + Subsystem: prefix, + Name: name, + Help: name + " gauge", + }, + zeroLabel.Labels(), + ) + + duration := factory.NewHistogramVec( + prometheus.HistogramOpts{ + Subsystem: prefix, + Name: name + "_duration_seconds", + Help: name + " duration in seconds", + Buckets: o.buckets, + }, + zeroLabel.Labels(), + ) + + return &Metrics[T]{ + duration: duration, + gauge: gauge, + counter: counter, + } +} + +// Count implements [Recorder]. +func (m *Metrics[T]) Count(label T) Counter { + c := m.counter.WithLabelValues(label.Values()...) + return &counter{c} +} + +type counter struct { + c prometheus.Counter +} + +// Add implements [Counter]. +func (c *counter) Add(val float64) { + c.c.Add(val) +} + +// Inc implements [Counter]. +func (c *counter) Inc() { + c.c.Inc() +} + +// Duration implements [Recorder]. +func (m *Metrics[T]) Duration(label T) DurationObserver { + h := m.duration.WithLabelValues(label.Values()...) + return &durationObserver{h} +} + +type durationObserver struct { + h prometheus.Observer +} + +// Observe implements [DurationObserver]. +func (d *durationObserver) Observe(dur time.Duration) { + d.h.Observe(dur.Seconds()) +} + +// Gauge implements [Recorder]. +func (m *Metrics[T]) Gauge(label T) Gauge { + g := m.gauge.WithLabelValues(label.Values()...) + return &gauge{g} +} + +type gauge struct { + g prometheus.Gauge +} + +// Set implements [Gauge]. +func (g *gauge) Set(val float64) { g.g.Set(val) } + +// Inc implements [Gauge]. +func (g *gauge) Inc() { g.g.Inc() } + +// Dec implements [Gauge]. +func (g *gauge) Dec() { g.g.Dec() } + +// Add implements [Gauge]. +func (g *gauge) Add(val float64) { g.g.Add(val) } + +// Sub implements [Gauge]. +func (g *gauge) Sub(val float64) { g.g.Sub(val) } diff --git a/melt/prommetric/options.go b/melt/prommetric/options.go new file mode 100644 index 0000000..1271ea1 --- /dev/null +++ b/melt/prommetric/options.go @@ -0,0 +1,22 @@ +package prommetric + +import "github.com/prometheus/client_golang/prometheus" + +type options struct { + buckets []float64 + registerer prometheus.Registerer +} + +type Option func(*options) + +func WithBuckets(buckets []float64) Option { + return func(o *options) { + o.buckets = buckets + } +} + +func WithRegisterer(r prometheus.Registerer) Option { + return func(o *options) { + o.registerer = r + } +} diff --git a/melt/prommetric/recorder.go b/melt/prommetric/recorder.go new file mode 100644 index 0000000..38da3c7 --- /dev/null +++ b/melt/prommetric/recorder.go @@ -0,0 +1,28 @@ +package prommetric + +import ( + "time" +) + +type Recorder[T Label] interface { + Count(label T) Counter + Duration(label T) DurationObserver + Gauge(label T) Gauge +} + +type Counter interface { + Inc() + Add(val float64) +} + +type DurationObserver interface { + Observe(duration time.Duration) +} + +type Gauge interface { + Set(val float64) + Inc() + Dec() + Add(val float64) + Sub(val float64) +} From fe0e565335c47790292f7eecdbf39d77acff81e2 Mon Sep 17 00:00:00 2001 From: raf555 Date: Wed, 6 May 2026 15:06:06 +0700 Subject: [PATCH 2/5] add nolabel --- melt/prommetric/label.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/melt/prommetric/label.go b/melt/prommetric/label.go index aa293ae..e2ce7ea 100644 --- a/melt/prommetric/label.go +++ b/melt/prommetric/label.go @@ -11,3 +11,17 @@ type Label interface { // The length is the same as returned by Labels. Values() []string } + +type NoLabel struct{} + +// Labels implements [Label]. +func (n NoLabel) Labels() []string { + return nil +} + +// Values implements [Label]. +func (n NoLabel) Values() []string { + return nil +} + +var _ Label = NoLabel{} From c6852c502e2f23bbd487690e0b6eb5a9734bdf40 Mon Sep 17 00:00:00 2001 From: raf555 Date: Wed, 6 May 2026 15:11:41 +0700 Subject: [PATCH 3/5] make nolabel pointer --- melt/prommetric/label.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/melt/prommetric/label.go b/melt/prommetric/label.go index e2ce7ea..00f1bf4 100644 --- a/melt/prommetric/label.go +++ b/melt/prommetric/label.go @@ -15,13 +15,13 @@ type Label interface { type NoLabel struct{} // Labels implements [Label]. -func (n NoLabel) Labels() []string { +func (n *NoLabel) Labels() []string { return nil } // Values implements [Label]. -func (n NoLabel) Values() []string { +func (n *NoLabel) Values() []string { return nil } -var _ Label = NoLabel{} +var _ Label = (*NoLabel)(nil) From ffe94ab70869f57f8b07d7d37118e0def36135f3 Mon Sep 17 00:00:00 2001 From: raf555 Date: Wed, 6 May 2026 18:22:16 +0700 Subject: [PATCH 4/5] chore: add nolabel --- melt/prommetric/metrics.go | 29 +++++++++++++++++++++++++++++ melt/prommetric/recorder.go | 6 ++++++ 2 files changed, 35 insertions(+) diff --git a/melt/prommetric/metrics.go b/melt/prommetric/metrics.go index 7e7a763..5dd3313 100644 --- a/melt/prommetric/metrics.go +++ b/melt/prommetric/metrics.go @@ -13,6 +13,8 @@ type Metrics[T Label] struct { duration *prometheus.HistogramVec } +var _ Recorder[*NoLabel] = (*Metrics[*NoLabel])(nil) + func New[T Label](prefix, name string, opts ...Option) Recorder[T] { o := &options{ buckets: prometheus.DefBuckets, @@ -119,3 +121,30 @@ func (g *gauge) Add(val float64) { g.g.Add(val) } // Sub implements [Gauge]. func (g *gauge) Sub(val float64) { g.g.Sub(val) } + +type MetricsNoLabel struct { + m Recorder[*NoLabel] +} + +var _ RecorderNoLabel = (*MetricsNoLabel)(nil) + +func NewNoLabel(prefix, name string, opts ...Option) RecorderNoLabel { + return &MetricsNoLabel{ + m: New[*NoLabel](prefix, name, opts...), + } +} + +// Count implements [RecorderNoLabel]. +func (m *MetricsNoLabel) Count() Counter { + return m.m.Count(nil) +} + +// Duration implements [RecorderNoLabel]. +func (m *MetricsNoLabel) Duration() DurationObserver { + return m.m.Duration(nil) +} + +// Gauge implements [RecorderNoLabel]. +func (m *MetricsNoLabel) Gauge() Gauge { + return m.m.Gauge(nil) +} diff --git a/melt/prommetric/recorder.go b/melt/prommetric/recorder.go index 38da3c7..d054199 100644 --- a/melt/prommetric/recorder.go +++ b/melt/prommetric/recorder.go @@ -10,6 +10,12 @@ type Recorder[T Label] interface { Gauge(label T) Gauge } +type RecorderNoLabel interface { + Count() Counter + Duration() DurationObserver + Gauge() Gauge +} + type Counter interface { Inc() Add(val float64) From 8ea15a74a3c6255b83c27fa9ff0957d498b3112b Mon Sep 17 00:00:00 2001 From: raf555 Date: Wed, 6 May 2026 19:01:13 +0700 Subject: [PATCH 5/5] rename --- melt/prommetric/metrics.go | 64 ++++++++++++++++++------------------- melt/prommetric/recorder.go | 13 ++++---- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/melt/prommetric/metrics.go b/melt/prommetric/metrics.go index 5dd3313..3d7ff94 100644 --- a/melt/prommetric/metrics.go +++ b/melt/prommetric/metrics.go @@ -7,15 +7,42 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" ) +type MetricsNoLabel struct { + m RecorderWithLabel[*NoLabel] +} + +var _ Recorder = (*MetricsNoLabel)(nil) + +func New(prefix, name string, opts ...Option) Recorder { + return &MetricsNoLabel{ + m: NewWithLabel[*NoLabel](prefix, name, opts...), + } +} + +// Count implements [Recorder]. +func (m *MetricsNoLabel) Count() Counter { + return m.m.Count(nil) +} + +// Duration implements [Recorder]. +func (m *MetricsNoLabel) Duration() DurationObserver { + return m.m.Duration(nil) +} + +// Gauge implements [Recorder]. +func (m *MetricsNoLabel) Gauge() Gauge { + return m.m.Gauge(nil) +} + type Metrics[T Label] struct { counter *prometheus.CounterVec gauge *prometheus.GaugeVec duration *prometheus.HistogramVec } -var _ Recorder[*NoLabel] = (*Metrics[*NoLabel])(nil) +var _ RecorderWithLabel[*NoLabel] = (*Metrics[*NoLabel])(nil) -func New[T Label](prefix, name string, opts ...Option) Recorder[T] { +func NewWithLabel[T Label](prefix, name string, opts ...Option) RecorderWithLabel[T] { o := &options{ buckets: prometheus.DefBuckets, registerer: prometheus.DefaultRegisterer, @@ -62,7 +89,7 @@ func New[T Label](prefix, name string, opts ...Option) Recorder[T] { } } -// Count implements [Recorder]. +// Count implements [RecorderWithLabel]. func (m *Metrics[T]) Count(label T) Counter { c := m.counter.WithLabelValues(label.Values()...) return &counter{c} @@ -82,7 +109,7 @@ func (c *counter) Inc() { c.c.Inc() } -// Duration implements [Recorder]. +// Duration implements [RecorderWithLabel]. func (m *Metrics[T]) Duration(label T) DurationObserver { h := m.duration.WithLabelValues(label.Values()...) return &durationObserver{h} @@ -97,7 +124,7 @@ func (d *durationObserver) Observe(dur time.Duration) { d.h.Observe(dur.Seconds()) } -// Gauge implements [Recorder]. +// Gauge implements [RecorderWithLabel]. func (m *Metrics[T]) Gauge(label T) Gauge { g := m.gauge.WithLabelValues(label.Values()...) return &gauge{g} @@ -121,30 +148,3 @@ func (g *gauge) Add(val float64) { g.g.Add(val) } // Sub implements [Gauge]. func (g *gauge) Sub(val float64) { g.g.Sub(val) } - -type MetricsNoLabel struct { - m Recorder[*NoLabel] -} - -var _ RecorderNoLabel = (*MetricsNoLabel)(nil) - -func NewNoLabel(prefix, name string, opts ...Option) RecorderNoLabel { - return &MetricsNoLabel{ - m: New[*NoLabel](prefix, name, opts...), - } -} - -// Count implements [RecorderNoLabel]. -func (m *MetricsNoLabel) Count() Counter { - return m.m.Count(nil) -} - -// Duration implements [RecorderNoLabel]. -func (m *MetricsNoLabel) Duration() DurationObserver { - return m.m.Duration(nil) -} - -// Gauge implements [RecorderNoLabel]. -func (m *MetricsNoLabel) Gauge() Gauge { - return m.m.Gauge(nil) -} diff --git a/melt/prommetric/recorder.go b/melt/prommetric/recorder.go index d054199..7b6e790 100644 --- a/melt/prommetric/recorder.go +++ b/melt/prommetric/recorder.go @@ -4,17 +4,16 @@ import ( "time" ) -type Recorder[T Label] interface { - Count(label T) Counter - Duration(label T) DurationObserver - Gauge(label T) Gauge -} - -type RecorderNoLabel interface { +type Recorder interface { Count() Counter Duration() DurationObserver Gauge() Gauge } +type RecorderWithLabel[T Label] interface { + Count(label T) Counter + Duration(label T) DurationObserver + Gauge(label T) Gauge +} type Counter interface { Inc()