feat: 错误消息归一化与记忆键构造
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
reUUID = regexp.MustCompile(`[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)
|
||||
reISOTime = regexp.MustCompile(`\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?`)
|
||||
reUnixMs = regexp.MustCompile(`\b1[4-9]\d{12}\b`) // unix 毫秒级时间戳
|
||||
reRequestID = regexp.MustCompile(`\b(req[-_]?|request[-_]?|rid[-_:]?)[-_:]?[0-9a-zA-Z-]{4,}\b`)
|
||||
reLongNum = regexp.MustCompile(`\b\d{4,}\b`) // 连续≥4位数字
|
||||
)
|
||||
|
||||
// normalizeErrorMsg 归一化错误消息:剔除易变片段(UUID/时间戳/请求ID/连续数字),
|
||||
// 使同因不同实例的错误命中同一记忆键。
|
||||
func normalizeErrorMsg(msg string) string {
|
||||
m := msg
|
||||
m = reUUID.ReplaceAllString(m, "{uuid}")
|
||||
m = reISOTime.ReplaceAllString(m, "{time}")
|
||||
m = reUnixMs.ReplaceAllString(m, "{ts}")
|
||||
m = reRequestID.ReplaceAllString(m, "{reqid}")
|
||||
m = reLongNum.ReplaceAllString(m, "{num}")
|
||||
return strings.TrimSpace(m)
|
||||
}
|
||||
|
||||
// buildMemoryKey 构造记忆键 = SHA-256(upstream|error_code|归一化消息)。
|
||||
// 含失败上游维度:不同上游的同类错误互不串扰。
|
||||
func buildMemoryKey(upstream, code, msg string) string {
|
||||
raw := strings.Join([]string{upstream, code, normalizeErrorMsg(msg)}, "|")
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// msgFingerprint 归一化消息的 md5(观测/展示用)。
|
||||
func msgFingerprint(msg string) string {
|
||||
sum := md5.Sum([]byte(normalizeErrorMsg(msg)))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeErrorMsg(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"rate limit exceeded for req-abc123", "rate limit exceeded for {reqid}"},
|
||||
{"timeout at 2026-09-01T09:00:00Z req_88f1a2", "timeout at {time} {reqid}"},
|
||||
{"uuid 0f8fad5b-d9cb-469f-a165-70867728950e remains", "uuid {uuid} remains"},
|
||||
{"err code 12345678 quota exceeded", "err code {num} quota exceeded"},
|
||||
{"clean message unchanged", "clean message unchanged"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := normalizeErrorMsg(c.in); got != c.want {
|
||||
t.Fatalf("normalizeErrorMsg(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMemoryKey(t *testing.T) {
|
||||
k1 := buildMemoryKey("https://a.com", "429", "rate limit for req-a1b2c3")
|
||||
k2 := buildMemoryKey("https://a.com", "429", "rate limit for req-d4e5f6") // 同因不同请求ID
|
||||
if k1 != k2 {
|
||||
t.Fatalf("同因不同请求ID应同键: %s != %s", k1, k2)
|
||||
}
|
||||
k3 := buildMemoryKey("https://b.com", "429", "rate limit for req-a1b2c3") // 不同上游
|
||||
if k1 == k3 {
|
||||
t.Fatalf("不同上游应不同键")
|
||||
}
|
||||
k4 := buildMemoryKey("https://a.com", "500", "rate limit for req-a1b2c3") // 不同错误码
|
||||
if k1 == k4 {
|
||||
t.Fatalf("不同错误码应不同键")
|
||||
}
|
||||
if len(k1) != 64 {
|
||||
t.Fatalf("memory_key 应为 SHA-256 十六进制64位, got %d", len(k1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgFingerprint(t *testing.T) {
|
||||
if msgFingerprint("a req-a1b2c3") != msgFingerprint("a req-d4e5f6") {
|
||||
t.Fatalf("同因指纹应一致")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user