Skip to content

Commit 521e64f

Browse files
kidkidkidHearyShen
authored andcommitted
[feat][backend]: refactor offline metric & support space_id skip (#404)
* feat(backend): refactor * feat(backend): add ut
1 parent d8d824e commit 521e64f

6 files changed

Lines changed: 241 additions & 32 deletions

File tree

backend/modules/observability/application/convertor/metric/metric.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
package metric
55

66
import (
7+
"sort"
8+
"strings"
9+
710
"github.com/coze-dev/coze-loop/backend/kitex_gen/coze/loop/observability/domain/metric"
811
"github.com/coze-dev/coze-loop/backend/modules/observability/domain/metric/entity"
912
"github.com/coze-dev/coze-loop/backend/pkg/lang/ptr"
13+
"github.com/samber/lo"
14+
)
15+
16+
const (
17+
maxPieCount = 10000
18+
retPieCount = 1000
1019
)
1120

1221
func MetricPointDO2DTO(m *entity.MetricPoint) *metric.MetricPoint {
@@ -41,6 +50,9 @@ func MetricDO2DTO(m *entity.Metric) *metric.Metric {
4150
}
4251
ret.TimeSeries[k] = MetricPointListDO2DTO(v)
4352
}
53+
if len(ret.Pie) > retPieCount {
54+
ret.Pie = minimizePie(ret.Pie)
55+
}
4456
return ret
4557
}
4658

@@ -53,3 +65,30 @@ func CompareDTO2DO(c *metric.Compare) *entity.Compare {
5365
Shift: ptr.From(c.ShiftSeconds),
5466
}
5567
}
68+
69+
func minimizePie(pie map[string]string) map[string]string {
70+
if len(pie) > maxPieCount {
71+
for k, v := range pie {
72+
if v == "0" || v == "1" {
73+
delete(pie, k)
74+
}
75+
}
76+
}
77+
if len(pie) > retPieCount {
78+
keys := lo.Keys(pie)
79+
// 假设没有浮点数, pie都是整数
80+
sort.Slice(keys, func(i, j int) bool {
81+
a, b := pie[keys[i]], pie[keys[j]]
82+
if len(a) != len(b) {
83+
return len(a) > len(b)
84+
}
85+
return strings.Compare(a, b) > 0
86+
})
87+
ret := make(map[string]string)
88+
for _, key := range keys[:retPieCount] {
89+
ret[key] = pie[key]
90+
}
91+
pie = ret
92+
}
93+
return pie
94+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) 2026 coze-dev Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package metric
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestMinimizePie(t *testing.T) {
14+
t.Run("size <= 1000 should not change", func(t *testing.T) {
15+
pie := make(map[string]string)
16+
for i := 0; i < 500; i++ {
17+
pie[fmt.Sprintf("key-%d", i)] = "1"
18+
}
19+
result := minimizePie(pie)
20+
assert.Equal(t, 500, len(result))
21+
})
22+
23+
t.Run("size > 1000 should truncate by custom sort", func(t *testing.T) {
24+
pie := make(map[string]string)
25+
// Add 1000 items with value "1" (length 1)
26+
for i := 0; i < 1000; i++ {
27+
pie[fmt.Sprintf("small-%d", i)] = "2"
28+
}
29+
30+
// Add 5 items with value "100" (length 3)
31+
for i := 0; i < 5; i++ {
32+
pie[fmt.Sprintf("large-%d", i)] = "100"
33+
}
34+
35+
// Add 5 items with value "10" (length 2)
36+
for i := 0; i < 5; i++ {
37+
pie[fmt.Sprintf("medium-%d", i)] = "91"
38+
}
39+
40+
// Total 1010 items.
41+
// Sort order desc: "100" (len 3) > "10" (len 2) > "1" (len 1)
42+
// Expected kept:
43+
// 5 "large" items (len 3)
44+
// 5 "medium" items (len 2)
45+
// 990 "small" items (len 1)
46+
// Total kept: 1000.
47+
// Removed: 10 "small" items.
48+
49+
result := minimizePie(pie)
50+
assert.Equal(t, 1000, len(result))
51+
52+
// Verify large and medium are kept
53+
for i := 0; i < 5; i++ {
54+
_, ok := result[fmt.Sprintf("large-%d", i)]
55+
assert.True(t, ok, "large item %d missing", i)
56+
_, ok = result[fmt.Sprintf("medium-%d", i)]
57+
assert.True(t, ok, "medium item %d missing", i)
58+
}
59+
60+
// Verify some small items are removed.
61+
// Since map iteration order is random when building the slice,
62+
// and the sort is stable for equal values (depends on implementation, actually sort.Slice is not guaranteed stable,
63+
// but here values are identical "1").
64+
// Wait, if values are identical "1", their relative order is undefined after sort.
65+
// So we can't deterministically say WHICH "small" items are removed, only that 10 are removed.
66+
67+
removedCount := 0
68+
for i := 0; i < 1000; i++ {
69+
if _, ok := result[fmt.Sprintf("small-%d", i)]; !ok {
70+
removedCount++
71+
}
72+
}
73+
fmt.Println("===", removedCount)
74+
assert.Equal(t, 10, removedCount)
75+
})
76+
77+
t.Run("size > 10000 should filter 0 and 1 first", func(t *testing.T) {
78+
pie := make(map[string]string)
79+
// 5000 items with "0"
80+
for i := 0; i < 5000; i++ {
81+
pie[fmt.Sprintf("zero-%d", i)] = "0"
82+
}
83+
// 5000 items with "1"
84+
for i := 0; i < 5000; i++ {
85+
pie[fmt.Sprintf("one-%d", i)] = "1"
86+
}
87+
// 500 items with "2"
88+
for i := 0; i < 500; i++ {
89+
pie[fmt.Sprintf("two-%d", i)] = "2"
90+
}
91+
92+
// Total 10500.
93+
// Filter "0" and "1" -> removes 10000 items.
94+
// Remaining 500 items with "2".
95+
// 500 <= 1000, so no truncation.
96+
97+
result := minimizePie(pie)
98+
assert.Equal(t, 500, len(result))
99+
100+
for k, v := range result {
101+
assert.Equal(t, "2", v)
102+
assert.Contains(t, k, "two-")
103+
}
104+
})
105+
106+
t.Run("size > 10000 filter 0 and 1 then truncate", func(t *testing.T) {
107+
pie := make(map[string]string)
108+
// 9000 items with "1"
109+
for i := 0; i < 9000; i++ {
110+
pie[fmt.Sprintf("one-%d", i)] = "1"
111+
}
112+
// 1500 items with "2"
113+
for i := 0; i < 1500; i++ {
114+
pie[fmt.Sprintf("two-%d", i)] = "2"
115+
}
116+
117+
// Total 10500.
118+
// Filter "1" -> removes 9000 items.
119+
// Remaining 1500 items with "2".
120+
// 1500 > 1000, truncate to 1000.
121+
122+
result := minimizePie(pie)
123+
assert.Equal(t, 1000, len(result))
124+
125+
for _, v := range result {
126+
assert.Equal(t, "2", v)
127+
}
128+
})
129+
}

backend/modules/observability/domain/component/config/config.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ type ConsumerListening struct {
114114
}
115115

116116
type MetricQueryConfig struct {
117-
SupportOffline bool `mapstructure:"support_offline" json:"support_offline"`
118-
OfflineCriticalPoint int `mapstructure:"offline_critical_point" json:"offline_critical_point"`
117+
SupportOffline bool `mapstructure:"support_offline" json:"support_offline"`
118+
OfflineCriticalPoint int `mapstructure:"offline_critical_point" json:"offline_critical_point"`
119+
SpaceConfigs map[string]*SpaceConfig `mapstructure:"space_configs" json:"space_configs"`
120+
}
121+
122+
type SpaceConfig struct {
123+
DisableQuery bool `mapstructure:"disable_query" json:"disable_query"`
119124
}
120125

121126
//go:generate mockgen -destination=mocks/config.go -package=mocks . ITraceConfig

backend/modules/observability/domain/component/config/mocks/config.go

Lines changed: 30 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/modules/observability/domain/metric/service/metric.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ func (m *MetricsService) QueryMetrics(ctx context.Context, req *QueryMetricsReq)
251251
if len(req.MetricsNames) == 0 {
252252
return &QueryMetricsResp{}, nil
253253
}
254+
qCfg := m.traceConfig.GetMetricQueryConfig(ctx)
255+
val := qCfg.SpaceConfigs[strconv.FormatInt(req.WorkspaceID, 10)]
256+
if val != nil && val.DisableQuery {
257+
return &QueryMetricsResp{}, nil
258+
}
254259
for _, metricName := range req.MetricsNames {
255260
mVal, ok := m.metricDefMap[metricName]
256261
if !ok {

backend/modules/observability/domain/metric/service/metric_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,37 @@ func TestMetricsService_QueryMetrics(t *testing.T) {
227227
assert.Error(t, err)
228228
assert.Nil(t, resp)
229229
})
230+
231+
t.Run("query disabled", func(t *testing.T) {
232+
t.Parallel()
233+
ctrl := gomock.NewController(t)
234+
defer ctrl.Finish()
235+
236+
traceConfigMock := configmocks.NewMockITraceConfig(ctrl)
237+
pMetrics := &entity.PlatformMetrics{
238+
MetricGroups: map[string]*entity.MetricGroup{},
239+
DrillDownObjects: map[string]*loop_span.FilterField{},
240+
PlatformMetricDefs: map[loop_span.PlatformType]*entity.PlatformMetricDef{},
241+
}
242+
243+
traceConfigMock.EXPECT().GetMetricQueryConfig(gomock.Any()).Return(&config.MetricQueryConfig{
244+
SpaceConfigs: map[string]*config.SpaceConfig{
245+
"1": {DisableQuery: true},
246+
},
247+
}).AnyTimes()
248+
249+
svc, err := NewMetricsService(repomocks.NewMockIMetricRepo(ctrl), nil, tenantmocks.NewMockITenantProvider(ctrl), traceServicemocks.NewMockTraceFilterProcessorBuilder(ctrl), traceConfigMock, pMetrics)
250+
assert.NoError(t, err)
251+
252+
resp, err := svc.QueryMetrics(context.Background(), &QueryMetricsReq{
253+
PlatformType: loop_span.PlatformType("loop"),
254+
WorkspaceID: 1,
255+
MetricsNames: []string{"metric_a"},
256+
})
257+
assert.NoError(t, err)
258+
assert.NotNil(t, resp)
259+
assert.Empty(t, resp.Metrics)
260+
})
230261
}
231262

232263
type testMetricDefinition struct {

0 commit comments

Comments
 (0)