diff --git a/pkg/cache/metrics_middleware.go b/pkg/cache/metrics_middleware.go index 90966127655faffca96dae0e5703528dcade9ff6..42d7f3d8d37356b784d93f175c2bbc00f9b65b13 100644 --- a/pkg/cache/metrics_middleware.go +++ b/pkg/cache/metrics_middleware.go @@ -4,35 +4,50 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +const badKey = "BADKEY" + type metricsMiddleware struct { - cache Cache - hitsTotal prometheus.Counter - missesTotal prometheus.Counter + cache Cache + hitsTotal prometheus.Counter + missesTotal prometheus.Counter + invalidatesTotal prometheus.Counter } // MetricsMiddleware возвращает обертку над кэшем, которая используется для отслеживания количества хитов и промахов в кэше. // +// subsystem указывает подсистему, к которой принадлежат метрики. +// Значение должно быть уникальным, совпадение разрешено только при совпадении ключей labels. Пустое значение допустимо. +// +// labels - список меток, где каждый элемент метки соответствует парам ключ-значение. Отсутствие допустимо. +// Значения меток должны быть уникальными в рамках одной subsystem. +// // Метрики записываются в prometheus.DefaultRegisterer -func MetricsMiddleware(cache Cache, serviceName string) Cache { +func MetricsMiddleware(cache Cache, subsystem string, labels ...string) Cache { if cache == nil { panic("cannot wrap metrics in cache, cache is nil") } middleware := &metricsMiddleware{ cache: cache, hitsTotal: prometheus.NewCounter(prometheus.CounterOpts{ - Name: "cache_hits_total", - Help: "Количество попаданий в кэш.", - ConstLabels: prometheus.Labels{"service_name": serviceName}, + Subsystem: subsystem, + Name: "cache_hits_total", + Help: "Количество попаданий в кэш.", }), missesTotal: prometheus.NewCounter(prometheus.CounterOpts{ - Name: "cache_misses_total", - Help: "Количество пропусков в кэш.", - ConstLabels: prometheus.Labels{"service_name": serviceName}, + Subsystem: subsystem, + Name: "cache_misses_total", + Help: "Количество пропусков в кэш.", + }), + invalidatesTotal: prometheus.NewCounter(prometheus.CounterOpts{ + Subsystem: subsystem, + Name: "cache_invalidates_total", + Help: "Количество инвалидаций кэша.", }), } - prometheus.MustRegister( + prometheus.WrapRegistererWith(argsToLabels(labels), prometheus.DefaultRegisterer).MustRegister( middleware.hitsTotal, middleware.missesTotal, + middleware.invalidatesTotal, ) return middleware } @@ -52,5 +67,25 @@ func (c *metricsMiddleware) Get(key any) (any, error) { } func (c *metricsMiddleware) Remove(key any) error { + c.invalidatesTotal.Inc() return c.cache.Remove(key) } + +// argsToLabels преобразует массив строк args в метки типа prometheus.Labels. +// +// Функция ожидает, что каждое значение будет следовать за соответствующим ключом в массиве args, +// и возвращает метки, соответствующие парам ключ-значение. +func argsToLabels(args []string) prometheus.Labels { + labels := make(prometheus.Labels, len(args)/2) + for len(args) > 0 { + // если в массиве args остался только один элемент, он будет рассмотрен как значение с недопустимым ключом. + if len(args) == 1 { + labels[badKey] = args[0] + break + } + + labels[args[0]] = args[1] + args = args[2:] + } + return labels +} diff --git a/pkg/cache/metrics_middleware_test.go b/pkg/cache/metrics_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3d2abf739477c04a4c0a80149748324ea73e713d --- /dev/null +++ b/pkg/cache/metrics_middleware_test.go @@ -0,0 +1,55 @@ +package cache + +import ( + "reflect" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +func TestMetricsMiddleware_argsToLabels(t *testing.T) { + testcases := []struct { + name string + input []string + want prometheus.Labels + }{ + { + name: "input is empty", + input: []string{}, + want: prometheus.Labels{}, + }, + { + name: "input is nil", + input: nil, + want: prometheus.Labels{}, + }, + { + name: "valid", + input: []string{"key", "value"}, + want: prometheus.Labels{"key": "value"}, + }, + { + name: "multi valid", + input: []string{"key", "value", "key1", "value1"}, + want: prometheus.Labels{"key": "value", "key1": "value1"}, + }, + { + name: "bad key", + input: []string{"value"}, + want: prometheus.Labels{badKey: "value"}, + }, + { + name: "multi bad key", + input: []string{"key", "value", "value1"}, + want: prometheus.Labels{"key": "value", badKey: "value1"}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got := argsToLabels(tc.input) + require.True(t, reflect.DeepEqual(tc.want, got)) + }) + } +}