diff --git a/backend/modules/observability/domain/metric/service/offline_metric.go b/backend/modules/observability/domain/metric/service/offline_metric.go index 19216ba81..b009c4404 100644 --- a/backend/modules/observability/domain/metric/service/offline_metric.go +++ b/backend/modules/observability/domain/metric/service/offline_metric.go @@ -203,8 +203,18 @@ func (m *MetricsService) parseStartDate(startDate string) (int64, int64, error) func (m *MetricsService) shouldTraverseMetric(metricDef entity.IMetricDefinition, reqMetrics []string) bool { if len(reqMetrics) != 0 && !lo.Contains(reqMetrics, metricDef.Name()) { - return false - } else if metricDef.OExpression().MetricName != "" { // rely on other metric, no need to offline calculate + if compound, ok := metricDef.(entity.IMetricCompound); ok { + matched := lo.ContainsBy(compound.GetMetrics(), func(sub entity.IMetricDefinition) bool { + return lo.Contains(reqMetrics, sub.Name()) + }) + if !matched { + return false + } + } else { + return false + } + } + if metricDef.OExpression().MetricName != "" { return false } return true diff --git a/backend/modules/observability/domain/metric/service/offline_metric_test.go b/backend/modules/observability/domain/metric/service/offline_metric_test.go index baf9649a4..6ed18fab1 100644 --- a/backend/modules/observability/domain/metric/service/offline_metric_test.go +++ b/backend/modules/observability/domain/metric/service/offline_metric_test.go @@ -83,6 +83,49 @@ func TestMetricsService_shouldTraverseMetric(t *testing.T) { assert.False(t, result) }) + + t.Run("compound metric matched by sub-metric name - should traverse", func(t *testing.T) { + t.Parallel() + svc := &MetricsService{} + sub1 := &testMetricDefinition{name: "sub_metric_a"} + sub2 := &testMetricDefinition{name: "sub_metric_b"} + compound := &testCompoundMetricDefinition{ + testMetricDefinition: &testMetricDefinition{name: "compound_metric"}, + metrics: []entity.IMetricDefinition{sub1, sub2}, + operator: entity.MetricOperatorDivide, + } + + result := svc.shouldTraverseMetric(compound, []string{"sub_metric_a"}) + assert.True(t, result) + }) + + t.Run("compound metric not matched by any sub-metric name - should not traverse", func(t *testing.T) { + t.Parallel() + svc := &MetricsService{} + sub1 := &testMetricDefinition{name: "sub_metric_a"} + compound := &testCompoundMetricDefinition{ + testMetricDefinition: &testMetricDefinition{name: "compound_metric"}, + metrics: []entity.IMetricDefinition{sub1}, + operator: entity.MetricOperatorDivide, + } + + result := svc.shouldTraverseMetric(compound, []string{"unrelated_metric"}) + assert.False(t, result) + }) + + t.Run("compound metric matched by parent name - should traverse", func(t *testing.T) { + t.Parallel() + svc := &MetricsService{} + sub1 := &testMetricDefinition{name: "sub_metric_a"} + compound := &testCompoundMetricDefinition{ + testMetricDefinition: &testMetricDefinition{name: "compound_metric"}, + metrics: []entity.IMetricDefinition{sub1}, + operator: entity.MetricOperatorDivide, + } + + result := svc.shouldTraverseMetric(compound, []string{"compound_metric"}) + assert.True(t, result) + }) } // TestMetricsService_buildDrillDownFields 测试钻取字段构建 @@ -1082,6 +1125,47 @@ func TestMetricsService_buildTraverseMetrics_GroupBelong(t *testing.T) { assert.Error(t, err) assert.Nil(t, metrics) }) + + t.Run("compound matched by sub-metric name expands all sub-metrics", func(t *testing.T) { + t.Parallel() + metric1 := &testMetricDefinition{name: "metric_numerator", metricType: entity.MetricTypeSummary} + metric2 := &testMetricDefinition{name: "metric_denominator", metricType: entity.MetricTypeSummary} + compound := &testCompoundMetricDefinition{ + testMetricDefinition: &testMetricDefinition{name: "metric_ratio", metricType: entity.MetricTypeSummary}, + metrics: []entity.IMetricDefinition{metric1, metric2}, + operator: entity.MetricOperatorDivide, + } + pMetrics := &entity.PlatformMetrics{ + MetricGroups: map[string]*entity.MetricGroup{ + "group_a": {MetricDefinitions: []entity.IMetricDefinition{compound}}, + "group_b": {MetricDefinitions: []entity.IMetricDefinition{metric1, metric2}}, + }, + DrillDownObjects: map[string]*loop_span.FilterField{}, + PlatformMetricDefs: map[loop_span.PlatformType]*entity.PlatformMetricDef{ + loop_span.PlatformType("test_platform"): {MetricGroups: []string{"group_a", "group_b"}}, + }, + } + svc := &MetricsService{ + metricDefMap: map[string]entity.IMetricDefinition{ + metric1.Name(): metric1, + metric2.Name(): metric2, + compound.Name(): compound, + }, + pMetrics: pMetrics, + } + metrics, err := svc.buildTraverseMetrics(context.Background(), &TraverseMetricsReq{ + PlatformTypes: []loop_span.PlatformType{"test_platform"}, + MetricsNames: []string{"metric_numerator"}, + }) + assert.NoError(t, err) + assert.Len(t, metrics, 2) + names := make([]string, 0, len(metrics)) + for _, m := range metrics { + names = append(names, m.metricDef.Name()) + } + assert.Contains(t, names, "metric_numerator") + assert.Contains(t, names, "metric_denominator") + }) } // TestMetricsService_TraverseMetrics 覆盖 TraverseMetrics 成功路径