diff --git a/pkg/cache/metrics_middleware.go b/pkg/cache/metrics_middleware.go index 9c4ec424dbe1d396cd4d96d1ea325c4fe66016db..6c220498044716ee4794780d61fa6c4876d17e73 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 fdd2689eab3cc59d78d186cec9a2377e4fa987b4..d4979e0f8ea12ea9f00dbacf6691fa73f904db9a 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 3ad04152a4c33dc8920d8f40e43f0c3cb99a3306..98fbc42012d0f7245c2156c3d3c83c51aaa5ba99 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 0000000000000000000000000000000000000000..62cb3963e43999f0d00237ec21d20bc1f7a45de8 --- /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 0000000000000000000000000000000000000000..7d9b6b31962a73b78451cfb608788f34878235cb --- /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 b33d76b98a94b20ca357d6e42d2658ac389ee883..310790098a4c74617a60de9969ae62f35b86916e 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 +}