From 6c6808f067be4ece256d26df22cd90841934b60b Mon Sep 17 00:00:00 2001
From: ensiouel <ensiouel@gmail.com>
Date: Thu, 21 Dec 2023 14:26:33 +0300
Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=BC=D0=B5=D1=82=D1=80=D0=B8?=
 =?UTF-8?q?=D0=BA=D0=B8=20prometheus=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD?=
 =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=20otel=20metric?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pkg/cache/metrics_middleware.go | 27 ++++++++--------
 pkg/metrics/cache.go            | 48 +++++++++++++---------------
 pkg/metrics/request.go          | 50 ++++++++++++------------------
 pkg/metrics/utils.go            | 22 +++++++++++++
 pkg/metrics/utils_test.go       | 55 +++++++++++++++++++++++++++++++++
 pkg/optional/optional.go        |  7 +++++
 6 files changed, 139 insertions(+), 70 deletions(-)
 create mode 100644 pkg/metrics/utils.go
 create mode 100644 pkg/metrics/utils_test.go

diff --git a/pkg/cache/metrics_middleware.go b/pkg/cache/metrics_middleware.go
index 9c4ec424..6c220498 100644
--- a/pkg/cache/metrics_middleware.go
+++ b/pkg/cache/metrics_middleware.go
@@ -1,41 +1,42 @@
 package cache
 
 import (
+	"context"
+
 	"git.perx.ru/perxis/perxis-go/pkg/metrics"
-	"github.com/prometheus/client_golang/prometheus"
+	metricotel "go.opentelemetry.io/otel/metric"
 )
 
 type metricsMiddleware struct {
-	cache        Cache
+	next         Cache
 	cacheMetrics *metrics.CacheMetrics
-	serviceName  string
+	attributes   metricotel.MeasurementOption
 }
 
 // MetricsMiddleware возвращает обертку над кэшем, которая используется для отслеживания количества хитов и промахов в кэше.
-func MetricsMiddleware(cache Cache, cacheMetrics *metrics.CacheMetrics, serviceName string) Cache {
+func MetricsMiddleware(next Cache, cacheMetrics *metrics.CacheMetrics, attributes ...string) Cache {
 	return &metricsMiddleware{
-		cache:        cache,
+		next:         next,
 		cacheMetrics: cacheMetrics,
-		serviceName:  serviceName,
+		attributes:   metricotel.WithAttributes(metrics.AttributesFromKV(attributes)...),
 	}
 }
 
 func (c *metricsMiddleware) Set(key, value any) error {
-	return c.cache.Set(key, value)
+	return c.next.Set(key, value)
 }
 
 func (c *metricsMiddleware) Get(key any) (any, error) {
-	labels := prometheus.Labels{"service": c.serviceName}
-	value, err := c.cache.Get(key)
+	value, err := c.next.Get(key)
 	if err != nil {
-		c.cacheMetrics.MissesTotal.With(labels).Inc()
+		c.cacheMetrics.MissesTotal.Add(context.TODO(), 1, c.attributes)
 		return nil, err
 	}
-	c.cacheMetrics.HitsTotal.With(labels).Inc()
+	c.cacheMetrics.HitsTotal.Add(context.TODO(), 1, c.attributes)
 	return value, nil
 }
 
 func (c *metricsMiddleware) Remove(key any) error {
-	c.cacheMetrics.InvalidatesTotal.With(prometheus.Labels{"service": c.serviceName}).Inc()
-	return c.cache.Remove(key)
+	c.cacheMetrics.InvalidatesTotal.Add(context.TODO(), 1, c.attributes)
+	return c.next.Remove(key)
 }
diff --git a/pkg/metrics/cache.go b/pkg/metrics/cache.go
index fdd2689e..d4979e0f 100644
--- a/pkg/metrics/cache.go
+++ b/pkg/metrics/cache.go
@@ -1,38 +1,32 @@
 package metrics
 
-import "github.com/prometheus/client_golang/prometheus"
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/optional"
+	"go.opentelemetry.io/otel"
+	metricotel "go.opentelemetry.io/otel/metric"
+)
 
 type CacheMetrics struct {
-	HitsTotal        *prometheus.CounterVec
-	MissesTotal      *prometheus.CounterVec
-	InvalidatesTotal *prometheus.CounterVec
+	HitsTotal        metricotel.Int64Counter
+	MissesTotal      metricotel.Int64Counter
+	InvalidatesTotal metricotel.Int64Counter
 }
 
 func NewCacheMetrics(subsystem string) *CacheMetrics {
-	labelNames := []string{
-		"service",
-	}
+	meter := otel.Meter(subsystem)
 	metrics := &CacheMetrics{
-		HitsTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
-			Subsystem: subsystem,
-			Name:      "cache_hits_total",
-			Help:      "Количество найденных в кэше значений",
-		}, labelNames),
-		MissesTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
-			Subsystem: subsystem,
-			Name:      "cache_misses_total",
-			Help:      "Количество не найденных в кэше значений",
-		}, labelNames),
-		InvalidatesTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
-			Subsystem: subsystem,
-			Name:      "cache_invalidates_total",
-			Help:      "Количество инвалидаций кэша",
-		}, labelNames),
+		HitsTotal: optional.Must(meter.Int64Counter(
+			"cache_hits_total",
+			metricotel.WithDescription("Количество найденных в кэше значений"),
+		)),
+		MissesTotal: optional.Must(meter.Int64Counter(
+			"cache_misses_total",
+			metricotel.WithDescription("Количество не найденных в кэше значений"),
+		)),
+		InvalidatesTotal: optional.Must(meter.Int64Counter(
+			"cache_invalidates_total",
+			metricotel.WithDescription("Количество инвалидаций кэша"),
+		)),
 	}
-	prometheus.MustRegister(
-		metrics.HitsTotal,
-		metrics.MissesTotal,
-		metrics.InvalidatesTotal,
-	)
 	return metrics
 }
diff --git a/pkg/metrics/request.go b/pkg/metrics/request.go
index 3ad04152..98fbc420 100644
--- a/pkg/metrics/request.go
+++ b/pkg/metrics/request.go
@@ -1,44 +1,34 @@
 package metrics
 
 import (
-	"github.com/prometheus/client_golang/prometheus"
+	"git.perx.ru/perxis/perxis-go/pkg/optional"
+	"go.opentelemetry.io/otel"
+	metricotel "go.opentelemetry.io/otel/metric"
 )
 
 type RequestMetrics struct {
-	Total           *prometheus.CounterVec
-	FailedTotal     *prometheus.CounterVec
-	DurationSeconds *prometheus.HistogramVec
+	Total                metricotel.Int64Counter
+	FailedTotal          metricotel.Int64Counter
+	DurationMilliseconds metricotel.Int64Histogram
 }
 
 // NewRequestMetrics возвращает метрики для подсчета количества удачных/неудачных запросов, а так же длительности ответов.
-// Метрики записываются в prometheus.DefaultRegisterer
 func NewRequestMetrics(subsystem string) *RequestMetrics {
-	labelNames := []string{
-		"service",
-		"method",
-	}
+	meter := otel.Meter(subsystem)
 	metrics := &RequestMetrics{
-		Total: prometheus.NewCounterVec(prometheus.CounterOpts{
-			Subsystem: subsystem,
-			Name:      "requests_total",
-			Help:      "Количество запросов.",
-		}, labelNames),
-		FailedTotal: prometheus.NewCounterVec(prometheus.CounterOpts{
-			Subsystem: subsystem,
-			Name:      "requests_failed_total",
-			Help:      "Количество запросов, вернувших ошибку.",
-		}, labelNames),
-		DurationSeconds: prometheus.NewHistogramVec(prometheus.HistogramOpts{
-			Subsystem: subsystem,
-			Name:      "request_duration_seconds",
-			Help:      "Длительность обработки запроса.",
-			Buckets:   prometheus.DefBuckets,
-		}, labelNames),
+		Total: optional.Must(meter.Int64Counter(
+			"requests_total",
+			metricotel.WithDescription("Количество запросов"),
+		)),
+		FailedTotal: optional.Must(meter.Int64Counter(
+			"requests_failed_total",
+			metricotel.WithDescription("Количество запросов, вернувших ошибку"),
+		)),
+		DurationMilliseconds: optional.Must(meter.Int64Histogram(
+			"request_duration",
+			metricotel.WithDescription("Длительность обработки запроса"),
+			metricotel.WithUnit("ms"),
+		)),
 	}
-	prometheus.MustRegister(
-		metrics.Total,
-		metrics.FailedTotal,
-		metrics.DurationSeconds,
-	)
 	return metrics
 }
diff --git a/pkg/metrics/utils.go b/pkg/metrics/utils.go
new file mode 100644
index 00000000..62cb3963
--- /dev/null
+++ b/pkg/metrics/utils.go
@@ -0,0 +1,22 @@
+package metrics
+
+import (
+	"go.opentelemetry.io/otel/attribute"
+)
+
+// AttributesFromKV преобразует массив строк args в []attribute.KeyValue.
+//
+// Функция ожидает, что каждое значение будет следовать за соответствующим ключом в массиве args,
+// и возвращает метки, соответствующие парам ключ-значение.
+func AttributesFromKV(args []string) []attribute.KeyValue {
+	labels := make([]attribute.KeyValue, 0, len(args)/2)
+	for len(args) > 0 {
+		// если в массиве args остался только один элемент, он будет проигнорирован
+		if len(args) == 1 {
+			break
+		}
+		labels = append(labels, attribute.Key(args[0]).String(args[1]))
+		args = args[2:]
+	}
+	return labels
+}
diff --git a/pkg/metrics/utils_test.go b/pkg/metrics/utils_test.go
new file mode 100644
index 00000000..7d9b6b31
--- /dev/null
+++ b/pkg/metrics/utils_test.go
@@ -0,0 +1,55 @@
+package metrics
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/otel/attribute"
+)
+
+func TestAttributesFromKV(t *testing.T) {
+	testcases := []struct {
+		name  string
+		input []string
+		want  []attribute.KeyValue
+	}{
+		{
+			name:  "input is empty",
+			input: []string{},
+			want:  []attribute.KeyValue{},
+		},
+		{
+			name:  "input is nil",
+			input: nil,
+			want:  []attribute.KeyValue{},
+		},
+		{
+			name:  "valid",
+			input: []string{"key", "value"},
+			want:  []attribute.KeyValue{attribute.Key("key").String("value")},
+		},
+		{
+			name:  "multi valid",
+			input: []string{"key", "value", "key1", "value1"},
+			want:  []attribute.KeyValue{attribute.Key("key").String("value"), attribute.Key("key1").String("value1")},
+		},
+		{
+			name:  "bad key",
+			input: []string{"value"},
+			want:  []attribute.KeyValue{},
+		},
+		{
+			name:  "multi bad key",
+			input: []string{"key", "value", "value1"},
+			want:  []attribute.KeyValue{attribute.Key("key").String("value")},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AttributesFromKV(tc.input)
+			require.True(t, reflect.DeepEqual(tc.want, got))
+		})
+	}
+}
diff --git a/pkg/optional/optional.go b/pkg/optional/optional.go
index b33d76b9..31079009 100644
--- a/pkg/optional/optional.go
+++ b/pkg/optional/optional.go
@@ -12,3 +12,10 @@ func Bool(v bool) *bool {
 func Ptr[T any](v T) *T {
 	return &v
 }
+
+func Must[T any](t T, err error) T {
+	if err != nil {
+		panic(err)
+	}
+	return t
+}
-- 
GitLab