Skip to content

Commit aa26547

Browse files
feat(observability): migrate column extract default configs to database
- Move SelectBest/Extract/configScore logic from clip_processor to entity layer - Use workspace_id=0 for global default configs with wildcard '*' matching - Add 4-bit scoring strategy (workspace > agent > spanListType > platformType) - Simplify ClipProcessor.Transform to delegate to entity layer - Add DB error logging consistent with other processors - Seed 3 default extract configs via init SQL with fixed IDs for idempotency - Add comprehensive tests for SelectBest, configScore, Extract
1 parent f4f75ea commit aa26547

11 files changed

Lines changed: 668 additions & 279 deletions

File tree

backend/modules/observability/domain/trace/entity/column_extract_config.go

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33

44
package entity
55

6-
import "time"
6+
import (
7+
"strings"
8+
"time"
9+
10+
"github.com/coze-dev/coze-loop/backend/pkg/json"
11+
)
712

813
type ColumnExtractRule struct {
914
Column string
@@ -22,3 +27,128 @@ type ColumnExtractConfig struct {
2227
UpdatedAt time.Time
2328
UpdatedBy string
2429
}
30+
31+
// Extract extracts the value from content using the JSONPath rule for the given column.
32+
// Returns the extracted string, or empty string if extraction fails.
33+
func (c *ColumnExtractConfig) Extract(content string, column string) string {
34+
if c == nil || len(c.Columns) == 0 {
35+
return ""
36+
}
37+
var rule *ColumnExtractRule
38+
for i := range c.Columns {
39+
if c.Columns[i].Column == column {
40+
rule = &c.Columns[i]
41+
break
42+
}
43+
}
44+
if rule == nil {
45+
return ""
46+
}
47+
return extractByJSONPath(content, rule.JSONPath)
48+
}
49+
50+
// ColumnExtractConfigs is a list of ColumnExtractConfig with selection logic.
51+
type ColumnExtractConfigs []*ColumnExtractConfig
52+
53+
// SelectBest selects the best matching config using a scoring strategy.
54+
//
55+
// Priority dimensions (highest to lowest weight):
56+
// 1. Workspace: target workspace > default (wsID=0). Cross-workspace configs are excluded.
57+
// 2. Agent: exact agent match > empty agent (wildcard).
58+
// 3. SpanListType: exact match > wildcard '*'.
59+
// 4. PlatformType: exact match > wildcard '*'.
60+
//
61+
// Returns nil if no config matches.
62+
func (configs ColumnExtractConfigs) SelectBest(workspaceId int64, agentName, platformType, spanListType string) *ColumnExtractConfig {
63+
var (
64+
best *ColumnExtractConfig
65+
bestScore int
66+
)
67+
68+
for _, cfg := range configs {
69+
score := configScore(cfg, workspaceId, agentName, platformType, spanListType)
70+
if score < 0 {
71+
continue // not a valid match
72+
}
73+
if best == nil || score > bestScore {
74+
best = cfg
75+
bestScore = score
76+
}
77+
}
78+
79+
return best
80+
}
81+
82+
// configScore computes a match score for a config. Returns -1 if the config doesn't match.
83+
// Higher score = better match. Score layout (4 bits):
84+
//
85+
// bit 3 (8): workspace match (target ws=1, default ws=0)
86+
// bit 2 (4): agent match (exact=1, empty=0)
87+
// bit 1 (2): spanListType match (exact=1, wildcard=0)
88+
// bit 0 (1): platformType match (exact=1, wildcard=0)
89+
func configScore(cfg *ColumnExtractConfig, workspaceId int64, agentName, platformType, spanListType string) int {
90+
// workspace: must be target or default(0), reject cross-workspace
91+
isTarget := cfg.WorkspaceID == workspaceId
92+
isDefault := cfg.WorkspaceID == 0
93+
if !isTarget && !isDefault {
94+
return -1
95+
}
96+
97+
// agent: must be exact match or empty wildcard
98+
agentMatch := agentName != "" && cfg.AgentName == agentName
99+
agentEmpty := cfg.AgentName == ""
100+
if !agentMatch && !agentEmpty {
101+
return -1
102+
}
103+
104+
// platformType: must be exact or wildcard '*'
105+
platformExact := cfg.PlatformType == platformType
106+
platformWild := cfg.PlatformType == "*"
107+
if !platformExact && !platformWild {
108+
return -1
109+
}
110+
111+
// spanListType: must be exact or wildcard '*'
112+
spanExact := cfg.SpanListType == spanListType
113+
spanWild := cfg.SpanListType == "*"
114+
if !spanExact && !spanWild {
115+
return -1
116+
}
117+
118+
score := 0
119+
if isTarget {
120+
score |= 8
121+
}
122+
if agentMatch {
123+
score |= 4
124+
}
125+
if platformExact {
126+
score |= 1
127+
}
128+
if spanExact {
129+
score |= 2
130+
}
131+
return score
132+
}
133+
134+
func extractByJSONPath(content, jsonPath string) string {
135+
if content == "" || jsonPath == "" {
136+
return ""
137+
}
138+
if !json.Valid([]byte(content)) {
139+
return ""
140+
}
141+
// For recursive descent queries ($..field), take the last match
142+
if strings.Contains(jsonPath, "..") {
143+
result, err := json.GetLastStringByJSONPath(content, jsonPath)
144+
if err != nil {
145+
return ""
146+
}
147+
return result
148+
}
149+
result, err := json.GetStringByJSONPathRecursively(content, jsonPath)
150+
if err != nil {
151+
return ""
152+
}
153+
return result
154+
}

0 commit comments

Comments
 (0)