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
+}