@@ -2776,6 +2776,11 @@ func (p *Proxy) recordPatternSnapshotEntry(cacheKey string, payload []byte, now
27762776 if ! p .patternsEnabled {
27772777 return
27782778 }
2779+ canonicalKey := canonicalPatternSnapshotCacheKey (cacheKey )
2780+ if canonicalKey != "" {
2781+ cacheKey = canonicalKey
2782+ }
2783+ payload = normalizePatternSnapshotPayload (payload )
27792784 patternCount := patternCountFromPayload (payload )
27802785 if patternCount > 0 {
27812786 p .metrics .RecordPatternsStored (patternCount )
@@ -2907,29 +2912,34 @@ func (p *Proxy) applyPatternsSnapshot(snapshot patternsSnapshot, source string)
29072912 if strings .TrimSpace (key ) == "" || len (incoming .Value ) == 0 {
29082913 continue
29092914 }
2915+ canonicalKey := canonicalPatternSnapshotCacheKey (key )
2916+ if strings .TrimSpace (canonicalKey ) == "" {
2917+ canonicalKey = key
2918+ }
2919+ incoming .Value = normalizePatternSnapshotPayload (incoming .Value )
29102920 if incoming .UpdatedAtUnixNano <= 0 {
29112921 incoming .UpdatedAtUnixNano = nowUnix
29122922 }
2913- if existing , ok := p .patternsSnapshotEntries [key ]; ok && existing .UpdatedAtUnixNano >= incoming .UpdatedAtUnixNano {
2923+ if existing , ok := p .patternsSnapshotEntries [canonicalKey ]; ok && existing .UpdatedAtUnixNano >= incoming .UpdatedAtUnixNano {
29142924 continue
29152925 }
29162926 incomingPatternCount := incoming .PatternCount
29172927 if incomingPatternCount <= 0 {
29182928 incomingPatternCount = patternCountFromPayload (incoming .Value )
29192929 }
29202930 copied := append ([]byte (nil ), incoming .Value ... )
2921- if existing , ok := p .patternsSnapshotEntries [key ]; ok {
2931+ if existing , ok := p .patternsSnapshotEntries [canonicalKey ]; ok {
29222932 p .patternsSnapshotPatternCount -= int64 (existing .PatternCount )
29232933 p .patternsSnapshotPayloadBytes -= int64 (len (existing .Value ))
29242934 }
2925- p .patternsSnapshotEntries [key ] = patternSnapshotEntry {
2935+ p .patternsSnapshotEntries [canonicalKey ] = patternSnapshotEntry {
29262936 Value : copied ,
29272937 UpdatedAtUnixNano : incoming .UpdatedAtUnixNano ,
29282938 PatternCount : incomingPatternCount ,
29292939 }
29302940 p .patternsSnapshotPatternCount += int64 (incomingPatternCount )
29312941 p .patternsSnapshotPayloadBytes += int64 (len (copied ))
2932- p .cache .SetWithTTL (key , copied , patternsCacheRetention )
2942+ p .cache .SetWithTTL (canonicalKey , copied , patternsCacheRetention )
29332943 appliedEntries ++
29342944 appliedPatterns += incomingPatternCount
29352945 }
@@ -2967,6 +2977,93 @@ func patternSnapshotIdentityFromCacheKey(cacheKey string) string {
29672977 return orgID + "\x00 " + query
29682978}
29692979
2980+ func canonicalPatternSnapshotCacheKey (cacheKey string ) string {
2981+ cacheKey = strings .TrimSpace (cacheKey )
2982+ if cacheKey == "" {
2983+ return ""
2984+ }
2985+ parts := strings .SplitN (cacheKey , ":" , 3 )
2986+ if len (parts ) != 3 || parts [0 ] != "patterns" {
2987+ return cacheKey
2988+ }
2989+ orgID := strings .TrimSpace (parts [1 ])
2990+ params , err := url .ParseQuery (parts [2 ])
2991+ if err != nil {
2992+ return cacheKey
2993+ }
2994+ query := patternScopeQuery (params .Get ("query" ))
2995+ if strings .TrimSpace (query ) == "" {
2996+ return cacheKey
2997+ }
2998+ canonical := url.Values {}
2999+ canonical .Set ("query" , query )
3000+ return "patterns:" + orgID + ":" + canonical .Encode ()
3001+ }
3002+
3003+ func normalizePatternSnapshotPayload (payload []byte ) []byte {
3004+ if len (payload ) == 0 {
3005+ return payload
3006+ }
3007+ var resp patternsResponse
3008+ if err := json .Unmarshal (payload , & resp ); err != nil {
3009+ return payload
3010+ }
3011+ changed := false
3012+ for i := range resp .Data {
3013+ entry := resp .Data [i ]
3014+ if len (entry .Samples ) == 0 {
3015+ continue
3016+ }
3017+ compacted := compactPatternSnapshotSamples (entry .Samples )
3018+ if len (compacted ) != len (entry .Samples ) {
3019+ changed = true
3020+ }
3021+ entry .Samples = compacted
3022+ resp .Data [i ] = entry
3023+ }
3024+ if ! changed {
3025+ return payload
3026+ }
3027+ encoded , err := json .Marshal (resp )
3028+ if err != nil {
3029+ return payload
3030+ }
3031+ return encoded
3032+ }
3033+
3034+ func compactPatternSnapshotSamples (samples [][]interface {}) [][]interface {} {
3035+ if len (samples ) == 0 {
3036+ return samples
3037+ }
3038+ byTimestamp := make (map [int64 ]int , len (samples ))
3039+ order := make ([]int64 , 0 , len (samples ))
3040+ for _ , pair := range samples {
3041+ if len (pair ) < 2 {
3042+ continue
3043+ }
3044+ ts , okTS := numberToInt64 (pair [0 ])
3045+ count , okCount := numberToInt (pair [1 ])
3046+ if ! okTS || ! okCount || count <= 0 {
3047+ continue
3048+ }
3049+ if _ , seen := byTimestamp [ts ]; ! seen {
3050+ order = append (order , ts )
3051+ }
3052+ if count > byTimestamp [ts ] {
3053+ byTimestamp [ts ] = count
3054+ }
3055+ }
3056+ if len (byTimestamp ) == 0 {
3057+ return [][]interface {}{}
3058+ }
3059+ sort .Slice (order , func (i , j int ) bool { return order [i ] < order [j ] })
3060+ compacted := make ([][]interface {}, 0 , len (order ))
3061+ for _ , ts := range order {
3062+ compacted = append (compacted , []interface {}{ts , byTimestamp [ts ]})
3063+ }
3064+ return compacted
3065+ }
3066+
29703067func betterPatternSnapshotEntry (current , incoming patternSnapshotEntry , currentKey , incomingKey string ) bool {
29713068 if incoming .UpdatedAtUnixNano != current .UpdatedAtUnixNano {
29723069 return incoming .UpdatedAtUnixNano > current .UpdatedAtUnixNano
@@ -5328,6 +5425,16 @@ func (p *Proxy) handlePatterns(w http.ResponseWriter, r *http.Request) {
53285425 }
53295426 p .recordPatternFetchDiagnostics (diag )
53305427 p .metrics .RecordPatternsDetected (len (entries ))
5428+ snapshotBody := []byte (nil )
5429+ if len (entries ) > 0 {
5430+ rawSnapshotBody , marshalErr := json .Marshal (patternsResponse {
5431+ Status : "success" ,
5432+ Data : entries ,
5433+ })
5434+ if marshalErr == nil {
5435+ snapshotBody = rawSnapshotBody
5436+ }
5437+ }
53315438 entries = p .prependCustomPatternEntries (entries , startParam , stepParam , patternLimit )
53325439 entries = fillPatternSamplesAcrossRequestedRange (entries , startParam , endParam , stepParam )
53335440 resultBody , err := json .Marshal (patternsResponse {
@@ -5344,11 +5451,15 @@ func (p *Proxy) handlePatterns(w http.ResponseWriter, r *http.Request) {
53445451 // Avoid sticky empty results: first-call empty probes should not poison long-lived pattern cache entries.
53455452 if len (entries ) > 0 {
53465453 now := time .Now ().UTC ()
5454+ snapshotPayload := snapshotBody
5455+ if len (snapshotPayload ) == 0 {
5456+ snapshotPayload = resultBody
5457+ }
53475458 p .cache .SetWithTTL (cacheWriteKey , resultBody , patternsCacheRetention )
5348- p .recordPatternSnapshotEntry (cacheWriteKey , resultBody , now )
5459+ p .recordPatternSnapshotEntry (cacheWriteKey , snapshotPayload , now )
53495460 if derivedStepCacheKey != "" && derivedStepCacheKey != cacheWriteKey {
53505461 p .cache .SetWithTTL (derivedStepCacheKey , resultBody , patternsCacheRetention )
5351- p .recordPatternSnapshotEntry (derivedStepCacheKey , resultBody , now )
5462+ p .recordPatternSnapshotEntry (derivedStepCacheKey , snapshotPayload , now )
53525463 }
53535464 }
53545465 w .Header ().Set ("Content-Type" , "application/json" )
0 commit comments