Repository: moira-alert/moira
Pull request #1183: fix(filter): Fix metric overrides
Author: login: Tetrergeru
Created: 2025-04-16T10:53:12Z | Merged: 2025-05-13T10:15:51Z

Теперь метрики собираются в пачки не только по имени, но и по ретеншен-таймстампу, чтобы убрать шанс того, что две метрики с разными таймстампами, прилетевшие одновременно, сконфликтуют при сохранении

Изменил сигнатуру метода из публичного интерфейса базы данных (была мапа, стал слайс). Обратите внимания, что ключи этой мапы никогда раньше не использовались в реализации, так что логика работы не изменилась


Diff:
diff --git a/database/redis/metric.go b/database/redis/metric.go
index 0ede7c371..9ee6d7f76 100644
--- a/database/redis/metric.go
+++ b/database/redis/metric.go
@@ -127,7 +127,7 @@ func (connector *DbConnector) getMetricRetention(metric string) (int64, error) {
 }
 
 // SaveMetrics saves new metrics.
-func (connector *DbConnector) SaveMetrics(metrics map[string]*moira.MatchedMetric) error {
+func (connector *DbConnector) SaveMetrics(metrics []*moira.MatchedMetric) error {
 	if len(metrics) == 0 {
 		return nil
 	}
diff --git a/database/redis/metric_test.go b/database/redis/metric_test.go
index 2eab9e940..9ee96f084 100644
--- a/database/redis/metric_test.go
+++ b/database/redis/metric_test.go
@@ -133,7 +133,7 @@ func TestMetricsStoring(t *testing.T) {
 		So(err, ShouldBeNil)
 		So(actualRet, ShouldEqual, 60)
 
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: val1})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{val1})
 		So(err, ShouldBeNil)
 
 		actualRet, err = dataBase.GetMetricRetention(metric1)
@@ -156,7 +156,7 @@ func TestMetricsStoring(t *testing.T) {
 		So(err, ShouldBeNil)
 		So(actualValues, ShouldResemble, map[string][]*moira.MetricValue{metric1: {}})
 
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: val2})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{val2})
 		So(err, ShouldBeNil)
 
 		actualValues, err = dataBase.GetMetricsValues([]string{metric1}, 0, 9)
@@ -184,7 +184,7 @@ func TestMetricsStoring(t *testing.T) {
 		So(actualValues, ShouldResemble, map[string][]*moira.MetricValue{metric1: {}})
 
 		// Save metric with changed retention
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: val3})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{val3})
 		So(err, ShouldBeNil)
 
 		// But retention still old, because cache
@@ -213,7 +213,7 @@ func TestRemoveMetricRetention(t *testing.T) {
 			Timestamp:          tsOlder,
 		}
 
-		err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric1Value})
+		err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1Value})
 		So(err, ShouldBeNil)
 
 		actualValues, err := dataBase.GetMetricsValues([]string{metric}, 0, tsNow)
@@ -287,13 +287,13 @@ func TestRemoveMetricValues(t *testing.T) {
 
 		from := fromInf
 
-		err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: met1})
+		err := dataBase.SaveMetrics([]*moira.MatchedMetric{met1})
 		So(err, ShouldBeNil) // Save metric with changed retention
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: met2})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{met2})
 		So(err, ShouldBeNil) // Save metric with changed retention
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: met3})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{met3})
 		So(err, ShouldBeNil) // Save metric with changed retention
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: met4})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{met4})
 		So(err, ShouldBeNil)
 
 		actualValues, err := dataBase.GetMetricsValues([]string{metric1}, 1, 99)
@@ -462,15 +462,15 @@ func TestRemoveMetricValues(t *testing.T) {
 			from := fromInf
 			to := toInf
 
-			err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric1})
+			err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric2})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric3})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric3})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric4})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric4})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric5})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric5})
 			So(err, ShouldBeNil)
 
 			dataBase.metricsCache.Flush()
@@ -508,15 +508,15 @@ func TestRemoveMetricValues(t *testing.T) {
 			from := fromInf
 			to := "5"
 
-			err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric1})
+			err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric2})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric3})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric3})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric4})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric4})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric5})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric5})
 			So(err, ShouldBeNil)
 
 			dataBase.metricsCache.Flush()
@@ -550,15 +550,15 @@ func TestRemoveMetricValues(t *testing.T) {
 			from := "60"
 			to := toInf
 
-			err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric1})
+			err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric2})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric3})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric3})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric4})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric4})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric5})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric5})
 			So(err, ShouldBeNil)
 
 			dataBase.metricsCache.Flush()
@@ -592,15 +592,15 @@ func TestRemoveMetricValues(t *testing.T) {
 			from := "20"
 			to := "40"
 
-			err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric1})
+			err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric2})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric3})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric3})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric4})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric4})
 			So(err, ShouldBeNil)
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric: metric5})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric5})
 			So(err, ShouldBeNil)
 
 			dataBase.metricsCache.Flush()
@@ -687,9 +687,9 @@ func TestMetricSubscription(t *testing.T) {
 			}
 		})
 
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: met1})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{met1})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: met2})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{met2})
 		So(err, ShouldBeNil)
 		time.Sleep(time.Second * 6)
 		tomb1.Kill(nil)
@@ -715,7 +715,9 @@ func TestMetricsStoringErrorConnection(t *testing.T) {
 		So(actual1, ShouldBeEmpty)
 		So(err, ShouldNotBeNil)
 
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{"metric1": {Value: 1, RetentionTimestamp: 1, Timestamp: 1, Retention: 60, Patterns: []string{"12"}, Metric: "123"}})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+			{Value: 1, RetentionTimestamp: 1, Timestamp: 1, Retention: 60, Patterns: []string{"12"}, Metric: "123"},
+		})
 		So(err, ShouldNotBeNil)
 
 		actual2, err := dataBase.GetMetricRetention("123")
@@ -839,22 +841,22 @@ func TestCleanupOutdatedMetrics(t *testing.T) {
 			Value:              4,
 		}
 
-		err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: metric1Value1})
+		err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1Value1})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: metric1Value2})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric1Value2})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: metric1Value3})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric1Value3})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: metric1Value4})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric1Value4})
 		So(err, ShouldBeNil)
 
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: metric2Value1})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2Value1})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: metric2Value2})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2Value2})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: metric2Value3})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2Value3})
 		So(err, ShouldBeNil)
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: metric2Value4})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2Value4})
 		So(err, ShouldBeNil)
 
 		actualValues, err := dataBase.GetMetricsValues([]string{metric1, metric2}, 0, tsNow)
@@ -975,16 +977,16 @@ func TestCleanupFutureMetrics(t *testing.T) {
 				dataBase.metricsCache.Flush()
 			}()
 
-			err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric1,
-				metric2: matchedMetric3,
-				metric3: matchedMetric5,
+			err := dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric1,
+				matchedMetric3,
+				matchedMetric5,
 			})
 			So(err, ShouldBeNil)
 
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric2,
-				metric2: matchedMetric4,
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric2,
+				matchedMetric4,
 			})
 			So(err, ShouldBeNil)
 
@@ -1035,16 +1037,16 @@ func TestCleanupFutureMetrics(t *testing.T) {
 				dataBase.metricsCache.Flush()
 			}()
 
-			err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric1,
-				metric2: matchedMetric3,
-				metric3: matchedMetric5,
+			err := dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric1,
+				matchedMetric3,
+				matchedMetric5,
 			})
 			So(err, ShouldBeNil)
 
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric2,
-				metric2: matchedMetric4,
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric2,
+				matchedMetric4,
 			})
 			So(err, ShouldBeNil)
 
@@ -1138,12 +1140,12 @@ func TestCleanupOutdatedPatternMetrics(t *testing.T) {
 			err := dataBase.addPatterns(pattern1, pattern2, pattern3)
 			So(err, ShouldBeNil)
 
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric1,
-				metric2: matchedMetric2,
-				metric3: matchedMetric3,
-				metric4: matchedMetric4,
-				metric5: matchedMetric5,
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric1,
+				matchedMetric2,
+				matchedMetric3,
+				matchedMetric4,
+				matchedMetric5,
 			})
 			So(err, ShouldBeNil)
 
@@ -1188,9 +1190,9 @@ func TestCleanupOutdatedPatternMetrics(t *testing.T) {
 			err := dataBase.addPatterns(pattern1, pattern2, pattern3)
 			So(err, ShouldBeNil)
 
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric1,
-				metric3: matchedMetric3,
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric1,
+				matchedMetric3,
 			})
 			So(err, ShouldBeNil)
 
@@ -1278,12 +1280,12 @@ func TestGetNonExistentPatternMetrics(t *testing.T) {
 			err := dataBase.addPatterns(pattern1, pattern2, pattern3)
 			So(err, ShouldBeNil)
 
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric1,
-				metric2: matchedMetric2,
-				metric3: matchedMetric3,
-				metric4: matchedMetric4,
-				metric5: matchedMetric5,
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric1,
+				matchedMetric2,
+				matchedMetric3,
+				matchedMetric4,
+				matchedMetric5,
 			})
 			So(err, ShouldBeNil)
 
@@ -1324,9 +1326,9 @@ func TestGetNonExistentPatternMetrics(t *testing.T) {
 			err := dataBase.addPatterns(pattern1, pattern2, pattern3)
 			So(err, ShouldBeNil)
 
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{
-				metric1: matchedMetric1,
-				metric3: matchedMetric3,
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{
+				matchedMetric1,
+				matchedMetric3,
 			})
 			So(err, ShouldBeNil)
 
@@ -1382,10 +1384,10 @@ func TestCleanupAbandonedRetention(t *testing.T) {
 			Timestamp:          tsOlder,
 		}
 
-		err := dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: metric1Value})
+		err := dataBase.SaveMetrics([]*moira.MatchedMetric{metric1Value})
 		So(err, ShouldBeNil)
 
-		err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: metric2Value})
+		err = dataBase.SaveMetrics([]*moira.MatchedMetric{metric2Value})
 		So(err, ShouldBeNil)
 
 		actualValues, err := dataBase.GetMetricsValues([]string{metric1, metric2}, 0, tsNow)
@@ -1449,8 +1451,8 @@ func TestRemoveMetricsByPrefix(t *testing.T) {
 	Convey("Given metrics with pattern my.test.*", t, func() {
 		for i := 1; i <= 10; i++ {
 			err := dataBase.SaveMetrics(
-				map[string]*moira.MatchedMetric{
-					fmt.Sprintf("my.test.super.metric%d", i): {
+				[]*moira.MatchedMetric{
+					{
 						Patterns:           []string{pattern},
 						Metric:             fmt.Sprintf("my.test.super.metric%d", i),
 						Retention:          10,
@@ -1464,8 +1466,8 @@ func TestRemoveMetricsByPrefix(t *testing.T) {
 
 		for i := 1; i <= 10; i++ {
 			err := dataBase.SaveMetrics(
-				map[string]*moira.MatchedMetric{
-					fmt.Sprintf("my.test.mega.metric%d", i): {
+				[]*moira.MatchedMetric{
+					{
 						Patterns:           []string{pattern},
 						Metric:             fmt.Sprintf("my.test.mega.metric%d", i),
 						Retention:          10,
@@ -1519,8 +1521,8 @@ func TestRemoveAllMetrics(t *testing.T) {
 	Convey("Given metrics with pattern my.test.*", t, func() {
 		for i := 1; i <= 10; i++ {
 			err := dataBase.SaveMetrics(
-				map[string]*moira.MatchedMetric{
-					fmt.Sprintf("my.test.super.metric%d", i): {
+				[]*moira.MatchedMetric{
+					{
 						Patterns:           []string{pattern},
 						Metric:             fmt.Sprintf("my.test.super.metric%d", i),
 						Retention:          10,
@@ -1534,8 +1536,8 @@ func TestRemoveAllMetrics(t *testing.T) {
 
 		for i := 1; i <= 10; i++ {
 			err := dataBase.SaveMetrics(
-				map[string]*moira.MatchedMetric{
-					fmt.Sprintf("my.test.mega.metric%d", i): {
+				[]*moira.MatchedMetric{
+					{
 						Patterns:           []string{pattern},
 						Metric:             fmt.Sprintf("my.test.mega.metric%d", i),
 						Retention:          10,
diff --git a/database/redis/trigger_test.go b/database/redis/trigger_test.go
index 5120ea16c..89c1c8f75 100644
--- a/database/redis/trigger_test.go
+++ b/database/redis/trigger_test.go
@@ -372,7 +372,7 @@ func TestTriggerStoring(t *testing.T) {
 			So(actualTriggers, ShouldResemble, []*moira.Trigger{triggerVer1})
 
 			// Save metrics
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric1: val1})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{val1})
 			So(err, ShouldBeNil)
 
 			// And check it
@@ -420,7 +420,7 @@ func TestTriggerStoring(t *testing.T) {
 			So(actualTriggers, ShouldResemble, []*moira.Trigger{triggerVer2})
 
 			// Save metrics for a new pattern metrics
-			err = dataBase.SaveMetrics(map[string]*moira.MatchedMetric{metric2: val2})
+			err = dataBase.SaveMetrics([]*moira.MatchedMetric{val2})
 			So(err, ShouldBeNil)
 
 			// And check it
diff --git a/datatypes.go b/datatypes.go
index 513a8b461..7fc54f2b0 100644
--- a/datatypes.go
+++ b/datatypes.go
@@ -395,6 +395,11 @@ type MatchedMetric struct {
 	Retention          int
 }
 
+type MetricNameAndTimestamp struct {
+	Name      string
+	Timestamp int64
+}
+
 // MetricValue represents metric data.
 type MetricValue struct {
 	RetentionTimestamp int64   `json:"step,omitempty" format:"int64"`
diff --git a/filter/cache_storage.go b/filter/cache_storage.go
index 5e69932d3..ab342774e 100644
--- a/filter/cache_storage.go
+++ b/filter/cache_storage.go
@@ -56,7 +56,7 @@ func NewCacheStorage(logger moira.Logger, metrics *metrics.FilterMetrics, reader
 }
 
 // EnrichMatchedMetric calculate retention and filter cached values.
-func (storage *Storage) EnrichMatchedMetric(batch map[string]*moira.MatchedMetric, m *moira.MatchedMetric) {
+func (storage *Storage) EnrichMatchedMetric(batch map[moira.MetricNameAndTimestamp]*moira.MatchedMetric, m *moira.MatchedMetric) {
 	m.Retention = storage.getRetention(m)
 	m.RetentionTimestamp = moira.RoundToNearestRetention(m.Timestamp, int64(m.Retention))
 
@@ -65,7 +65,9 @@ func (storage *Storage) EnrichMatchedMetric(batch map[string]*moira.MatchedMetri
 	}
 
 	storage.metricsCache[m.Metric] = m
-	batch[m.Metric] = m
+
+	metricKey := moira.MetricNameAndTimestamp{Name: m.Metric, Timestamp: m.RetentionTimestamp}
+	batch[metricKey] = m
 }
 
 // getRetention returns first matched retention for metric.
diff --git a/filter/cache_storage_test.go b/filter/cache_storage_test.go
index 63ad59924..286a6d9d1 100644
--- a/filter/cache_storage_test.go
+++ b/filter/cache_storage_test.go
@@ -183,7 +183,7 @@ func TestCacheStorage(t *testing.T) {
 	})
 
 	Convey("Test empty buffer and different metrics, should buffer len equal to matchedMetrics len", t, func() {
-		buffer := make(map[string]*moira.MatchedMetric)
+		buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 		for _, matchedMetric := range matchedMetrics {
 			storage.EnrichMatchedMetric(buffer, &matchedMetric)
 		}
@@ -194,7 +194,7 @@ func TestCacheStorage(t *testing.T) {
 	storage, _ = NewCacheStorage(nil, filterMetrics, strings.NewReader(testRetentions))
 
 	Convey("Test add one metric twice, should buffer len is 1", t, func() {
-		buffer := make(map[string]*moira.MatchedMetric)
+		buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 		storage.EnrichMatchedMetric(buffer, &matchedMetrics[0])
 		So(len(buffer), ShouldEqual, 1)
 		storage.EnrichMatchedMetric(buffer, &matchedMetrics[0])
@@ -207,7 +207,7 @@ func TestRetentions(t *testing.T) {
 	storage, _ := NewCacheStorage(nil, filterMetrics, strings.NewReader(testRetentions))
 
 	Convey("Simple metric, should 60sec", t, func() {
-		buffer := make(map[string]*moira.MatchedMetric)
+		buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 		matchedMetric := matchedMetrics[0]
 
 		storage.EnrichMatchedMetric(buffer, &matchedMetric)
@@ -217,7 +217,7 @@ func TestRetentions(t *testing.T) {
 	})
 
 	Convey("Suf metric, should 1200sec", t, func() {
-		buffer := make(map[string]*moira.MatchedMetric)
+		buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 		metr := matchedMetrics[6]
 
 		storage.EnrichMatchedMetric(buffer, &metr)
@@ -227,7 +227,7 @@ func TestRetentions(t *testing.T) {
 	})
 
 	Convey("Default metric, should 120sec", t, func() {
-		buffer := make(map[string]*moira.MatchedMetric)
+		buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 		metr := matchedMetrics[14]
 
 		storage.EnrichMatchedMetric(buffer, &metr)
@@ -245,7 +245,7 @@ func TestRetentions(t *testing.T) {
 				RetentionTimestamp: 0,
 				Retention:          10,
 			}
-			buffer := make(map[string]*moira.MatchedMetric)
+			buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 			storage.EnrichMatchedMetric(buffer, &matchedMetric)
 			So(len(buffer), ShouldEqual, 1)
 			So(matchedMetric.Retention, ShouldEqual, 10)
@@ -260,7 +260,7 @@ func TestRetentions(t *testing.T) {
 				RetentionTimestamp: 0,
 				Retention:          60,
 			}
-			buffer := make(map[string]*moira.MatchedMetric)
+			buffer := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric)
 			storage.EnrichMatchedMetric(buffer, &matchedMetric)
 			So(len(buffer), ShouldEqual, 1)
 			So(matchedMetric.Retention, ShouldEqual, 120)
diff --git a/filter/matched_metrics/metrics.go b/filter/matched_metrics/metrics.go
index 46ec101e5..f3236ffff 100644
--- a/filter/matched_metrics/metrics.go
+++ b/filter/matched_metrics/metrics.go
@@ -63,8 +63,8 @@ func (matcher *MetricsMatcher) Start(matchedMetricsChan chan *moira.MatchedMetri
 		Msg("Moira Filter Metrics Matcher started to save cached metrics with cooldown")
 }
 
-func (matcher *MetricsMatcher) receiveBatch(metrics <-chan *moira.MatchedMetric) <-chan map[string]*moira.MatchedMetric {
-	batchedMetrics := make(chan map[string]*moira.MatchedMetric, 1)
+func (matcher *MetricsMatcher) receiveBatch(metrics <-chan *moira.MatchedMetric) <-chan map[moira.MetricNameAndTimestamp]*moira.MatchedMetric {
+	batchedMetrics := make(chan map[moira.MetricNameAndTimestamp]*moira.MatchedMetric, 1)
 
 	go func() {
 		defer close(batchedMetrics)
@@ -73,7 +73,7 @@ func (matcher *MetricsMatcher) receiveBatch(metrics <-chan *moira.MatchedMetric)
 		defer batchTimer.Stop()
 
 		for {
-			batch := make(map[string]*moira.MatchedMetric, matcher.cacheCapacity)
+			batch := make(map[moira.MetricNameAndTimestamp]*moira.MatchedMetric, matcher.cacheCapacity)
 		retry:
 			select {
 			case <-matcher.closeRequest:
@@ -106,8 +106,13 @@ func (matcher *MetricsMatcher) Wait() {
 	matcher.logger.Info().Msg("Moira Filter Metrics Matcher stopped")
 }
 
-func (matcher *MetricsMatcher) save(buffer map[string]*moira.MatchedMetric) {
-	if err := matcher.database.SaveMetrics(buffer); err != nil {
+func (matcher *MetricsMatcher) save(buffer map[moira.MetricNameAndTimestamp]*moira.MatchedMetric) {
+	bufferValues := make([]*moira.MatchedMetric, 0, len(buffer))
+	for _, v := range buffer {
+		bufferValues = append(bufferValues, v)
+	}
+
+	if err := matcher.database.SaveMetrics(bufferValues); err != nil {
 		matcher.logger.Error().
 			Error(err).
 			Msg("Failed to save matched metrics")
diff --git a/interfaces.go b/interfaces.go
index 96d6222ce..8dccc1246 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -106,7 +106,7 @@ type Database interface {
 	RemovePatternWithMetrics(pattern string) error
 
 	SubscribeMetricEvents(tomb *tomb.Tomb, params *SubscribeMetricEventsParams) (<-chan *MetricEvent, error)
-	SaveMetrics(buffer map[string]*MatchedMetric) error
+	SaveMetrics(buffer []*MatchedMetric) error
 	GetMetricRetention(metric string) (int64, error)
 	GetMetricsValues(metrics []string, from int64, until int64) (map[string][]*MetricValue, error)
 	RemoveMetricRetention(metric string) error
diff --git a/metric_source/local/database_test.go b/metric_source/local/database_test.go
index 0f0046277..13568080a 100644
--- a/metric_source/local/database_test.go
+++ b/metric_source/local/database_test.go
@@ -41,24 +41,24 @@ func saveMetrics(database moira.Database, metrics map[string]metricMock, now, re
 	for i := range maxValues {
 		time := timeStart + int64(i)*retention
 
-		metricsMap := make(map[string]*moira.MatchedMetric, len(metrics))
+		metricsBuffer := make([]*moira.MatchedMetric, 0, len(metrics))
 
 		for name, metric := range metrics {
 			if len(metric.values) <= i {
 				continue
 			}
 
-			metricsMap[name] = &moira.MatchedMetric{
+			metricsBuffer = append(metricsBuffer, &moira.MatchedMetric{
 				Metric:             name,
 				Patterns:           metric.patterns,
 				Value:              metric.values[i],
 				Timestamp:          time,
 				RetentionTimestamp: time,
 				Retention:          int(retention),
-			}
+			})
 		}
 
-		err := database.SaveMetrics(metricsMap)
+		err := database.SaveMetrics(metricsBuffer)
 		if err != nil {
 			return err
 		}
diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go
index b76588d60..61327872a 100644
--- a/mock/moira-alert/database.go
+++ b/mock/moira-alert/database.go
@@ -1479,7 +1479,7 @@ func (mr *MockDatabaseMockRecorder) SaveContact(contact any) *gomock.Call {
 }
 
 // SaveMetrics mocks base method.
-func (m *MockDatabase) SaveMetrics(buffer map[string]*moira.MatchedMetric) error {
+func (m *MockDatabase) SaveMetrics(buffer []*moira.MatchedMetric) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "SaveMetrics", buffer)
 	ret0, _ := ret[0].(error)
diff --git a/support/trigger.go b/support/trigger.go
index 20e440d74..d88ebff29 100644
--- a/support/trigger.go
+++ b/support/trigger.go
@@ -108,14 +108,11 @@ func HandlePushTriggerMetrics(
 ) error {
 	logger.Info().Msg("Save trigger metrics")
 
-	buffer := make(map[string]*moira.MatchedMetric, len(patternsMetrics))
-	i := 0
+	buffer := make([]*moira.MatchedMetric, 0, len(patternsMetrics))
 
 	for _, patternMetrics := range patternsMetrics {
 		for metricName, metricValues := range patternMetrics.Metrics {
 			for _, metricValue := range metricValues {
-				i++
-
 				retention, ok := patternMetrics.Retentions[metricName]
 				if !ok {
 					retention = defaultRetention
@@ -131,7 +128,7 @@ func HandlePushTriggerMetrics(
 					RetentionTimestamp: metricValue.RetentionTimestamp,
 					Retention:          int(retention),
 				}
-				buffer[fmt.Sprintf("%d", i)] = &matchedMetric
+				buffer = append(buffer, &matchedMetric)
 			}
 		}
 	}


Reviews:
user: login: kissken
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-16T11:31:28Z
user: login: kissken
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T09:05:53Z
user: login: HeavyPunk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:00:53Z
user: login: Tetrergeru
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:03:26Z
user: login: HeavyPunk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:03:36Z
user: login: Tetrergeru
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:04:49Z
user: login: HeavyPunk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:08:14Z
user: login: HeavyPunk
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T10:08:51Z

Review comments:
diff_hunk: @@ -106,8 +106,13 @@ func (matcher *MetricsMatcher) Wait() {
 	matcher.logger.Info().Msg("Moira Filter Metrics Matcher stopped")
 }
 
-func (matcher *MetricsMatcher) save(buffer map[string]*moira.MatchedMetric) {
-	if err := matcher.database.SaveMetrics(buffer); err != nil {
+func (matcher *MetricsMatcher) save(buffer map[moira.MetricNameAndTimestamp]*moira.MatchedMetric) {
+	bufferValues := make([]*moira.MatchedMetric, 0, len(buffer))
path: filter/matched_metrics/metrics.go
user: login: HeavyPunk
body: Может тут использовать функцию `Map` из `helpers.go` для большей читаемости?
created_at: 2025-05-13T10:00:11Z
author_association: MEMBER
diff_hunk: @@ -106,8 +106,13 @@ func (matcher *MetricsMatcher) Wait() {
 	matcher.logger.Info().Msg("Moira Filter Metrics Matcher stopped")
 }
 
-func (matcher *MetricsMatcher) save(buffer map[string]*moira.MatchedMetric) {
-	if err := matcher.database.SaveMetrics(buffer); err != nil {
+func (matcher *MetricsMatcher) save(buffer map[moira.MetricNameAndTimestamp]*moira.MatchedMetric) {
+	bufferValues := make([]*moira.MatchedMetric, 0, len(buffer))
path: filter/matched_metrics/metrics.go
user: login: Tetrergeru
body: Кажется, не получится, потому что там на вход подаётся slice, а мне тут надо мапу превратить в слайс
created_at: 2025-05-13T10:03:26Z
author_association: MEMBER
diff_hunk: @@ -106,8 +106,13 @@ func (matcher *MetricsMatcher) Wait() {
 	matcher.logger.Info().Msg("Moira Filter Metrics Matcher stopped")
 }
 
-func (matcher *MetricsMatcher) save(buffer map[string]*moira.MatchedMetric) {
-	if err := matcher.database.SaveMetrics(buffer); err != nil {
+func (matcher *MetricsMatcher) save(buffer map[moira.MetricNameAndTimestamp]*moira.MatchedMetric) {
+	bufferValues := make([]*moira.MatchedMetric, 0, len(buffer))
path: filter/matched_metrics/metrics.go
user: login: HeavyPunk
body: Промазал мимо строки, имел в виду логику превращения мапы в слайс ниже
created_at: 2025-05-13T10:03:36Z
author_association: MEMBER
diff_hunk: @@ -106,8 +106,13 @@ func (matcher *MetricsMatcher) Wait() {
 	matcher.logger.Info().Msg("Moira Filter Metrics Matcher stopped")
 }
 
-func (matcher *MetricsMatcher) save(buffer map[string]*moira.MatchedMetric) {
-	if err := matcher.database.SaveMetrics(buffer); err != nil {
+func (matcher *MetricsMatcher) save(buffer map[moira.MetricNameAndTimestamp]*moira.MatchedMetric) {
+	bufferValues := make([]*moira.MatchedMetric, 0, len(buffer))
path: filter/matched_metrics/metrics.go
user: login: Tetrergeru
body: Так у функции `Map` тип `input` — `[]T`, я не могу туда мапу засунуть...
created_at: 2025-05-13T10:04:49Z
author_association: MEMBER
diff_hunk: @@ -106,8 +106,13 @@ func (matcher *MetricsMatcher) Wait() {
 	matcher.logger.Info().Msg("Moira Filter Metrics Matcher stopped")
 }
 
-func (matcher *MetricsMatcher) save(buffer map[string]*moira.MatchedMetric) {
-	if err := matcher.database.SaveMetrics(buffer); err != nil {
+func (matcher *MetricsMatcher) save(buffer map[moira.MetricNameAndTimestamp]*moira.MatchedMetric) {
+	bufferValues := make([]*moira.MatchedMetric, 0, len(buffer))
path: filter/matched_metrics/metrics.go
user: login: HeavyPunk
body: Ага, заметил, тогда так норм
created_at: 2025-05-13T10:08:14Z
author_association: MEMBER

PR comments:
user: login: Tetrergeru
created_at: 2025-04-17T08:28:32Z
body: /build
author_association: MEMBER
user: login: Tetrergeru
created_at: 2025-04-17T11:04:58Z
body: /build
author_association: MEMBER
user: login: Tetrergeru
created_at: 2025-05-13T09:08:45Z
body: /build
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-13T10:17:21Z
body: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: 2025-05-13.ae3e61b
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T10:17:42Z
body: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: 2025-05-13.ae3e61b
author_association: NONE