Skip to content

Commit 02ee6ab

Browse files
committed
refactor: improve Redis TimeSeries metrics recording
- Extract key generation to getTimeSeriesKey() function - Add recordProjectMetrics() method for reusable metrics recording - Update key format to ts:project-{metricType}:{projectId}:{granularity} - Support multiple metric types (events-accepted, events-rate-limited, etc.) - Move test helpers to separate test_helpers.go file - Use TSIncrBy instead of TSAdd for correct TS.INCRBY behavior - Add detailed comments explaining hash-based pseudo-random generation - Translate all comments to English Addresses PR review comments: - Key composition moved to separate method (neSpecc) - Metrics recording extracted to recordProjectMetrics (neSpecc) - Test functions moved to test_helpers.go (n0str) - Hash algorithm explained (neSpecc) - All comments in English (Copilot)"
1 parent 8cfc7fc commit 02ee6ab

File tree

3 files changed

+108
-138
lines changed

3 files changed

+108
-138
lines changed

pkg/redis/client.go

Lines changed: 6 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -253,18 +253,16 @@ func (r *RedisClient) TSCreateIfNotExists(
253253
return nil // already exists
254254
}
255255

256+
labelArgs := []interface{}{"LABELS"}
257+
for k, v := range labels {
258+
labelArgs = append(labelArgs, k, v)
259+
}
260+
256261
args := []interface{}{key}
257262
if retention > 0 {
258263
args = append(args, "RETENTION", int64(retention/time.Millisecond))
259264
}
260-
// Allow duplicate timestamps by summing their values
261-
args = append(args, "DUPLICATE_POLICY", "SUM")
262-
263-
// Add labels at the end
264-
args = append(args, "LABELS")
265-
for k, v := range labels {
266-
args = append(args, k, v)
267-
}
265+
args = append(args, labelArgs...)
268266

269267
res := r.rdb.Do(r.ctx, append([]interface{}{"TS.CREATE"}, args...)...)
270268
return res.Err()
@@ -316,51 +314,3 @@ func (r *RedisClient) SafeTSIncrBy(
316314
}
317315
return err
318316
}
319-
320-
// TSAdd adds a sample to a RedisTimeSeries key with labels and timestamp.
321-
// Uses RedisTimeSeries command TS.ADD.
322-
func (r *RedisClient) TSAdd(
323-
key string,
324-
value int64,
325-
timestamp int64,
326-
labels map[string]string,
327-
) error {
328-
// Prepare label arguments
329-
labelArgs := []interface{}{"LABELS"}
330-
for k, v := range labels {
331-
labelArgs = append(labelArgs, k, v)
332-
}
333-
334-
if timestamp == 0 {
335-
timestamp = time.Now().UnixNano() / int64(time.Millisecond)
336-
}
337-
338-
args := []interface{}{key, timestamp, value}
339-
args = append(args, "ON_DUPLICATE", "SUM")
340-
args = append(args, labelArgs...)
341-
342-
cmdArgs := append([]interface{}{"TS.ADD"}, args...)
343-
res := r.rdb.Do(r.ctx, cmdArgs...)
344-
return res.Err()
345-
}
346-
347-
// SafeTSAdd ensures that a TS key exists and adds a sample safely.
348-
// Automatically creates the time series if it doesn't exist.
349-
func (r *RedisClient) SafeTSAdd(
350-
key string,
351-
value int64,
352-
labels map[string]string,
353-
retention time.Duration,
354-
) error {
355-
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
356-
357-
err := r.TSAdd(key, value, timestamp, labels)
358-
if err != nil && strings.Contains(err.Error(), "TSDB: key does not exist") {
359-
log.Warnf("TS key %s does not exist, creating it...", key)
360-
if err2 := r.TSCreateIfNotExists(key, labels, retention); err2 != nil {
361-
return fmt.Errorf("failed to create TS: %w", err2)
362-
}
363-
return r.TSAdd(key, value, timestamp, labels)
364-
}
365-
return err
366-
}

pkg/server/errorshandler/handler.go

Lines changed: 28 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -107,31 +107,8 @@ func (handler *Handler) process(body []byte) ResponseMessage {
107107
// increment processed errors counter
108108
handler.ErrorsProcessed.Inc()
109109

110-
// add event to time series (minutely, hourly, and daily)
111-
minutelyKey := fmt.Sprintf("ts:events:%s:minutely", projectId)
112-
hourlyKey := fmt.Sprintf("ts:events:%s:hourly", projectId)
113-
dailyKey := fmt.Sprintf("ts:events:%s:daily", projectId)
114-
115-
labels := map[string]string{
116-
"type": "error",
117-
"status": "accepted",
118-
"project": projectId,
119-
}
120-
121-
// minutely: stored for 24 hours
122-
if err := handler.RedisClient.SafeTSAdd(minutelyKey, 1, labels, 24*time.Hour); err != nil {
123-
log.Errorf("failed to add to minutely TS: %v", err)
124-
}
125-
126-
// hourly: stored for 7 days
127-
if err := handler.RedisClient.SafeTSAdd(hourlyKey, 1, labels, 7*24*time.Hour); err != nil {
128-
log.Errorf("failed to add to hourly TS: %v", err)
129-
}
130-
131-
// daily: stored for 90 days
132-
if err := handler.RedisClient.SafeTSAdd(dailyKey, 1, labels, 90*24*time.Hour); err != nil {
133-
log.Errorf("failed to add to daily TS: %v", err)
134-
}
110+
// record project metrics
111+
handler.recordProjectMetrics(projectId, "events-accepted")
135112

136113
return ResponseMessage{200, false, "OK"}
137114
}
@@ -153,67 +130,36 @@ func GetQueueCache(nonDefaultQueues []string) map[string]bool {
153130
return cache
154131
}
155132

156-
// GenerateTestTimeSeriesData - generates test data for minutely, hourly, and daily time series
157-
// This should be called after manually deleting the Redis keys
158-
// Usage: handler.GenerateTestTimeSeriesData(projectId)
159-
func (handler *Handler) GenerateTestTimeSeriesData(projectId string) error {
160-
minutelyKey := fmt.Sprintf("ts:events:%s:minutely", projectId)
161-
hourlyKey := fmt.Sprintf("ts:events:%s:hourly", projectId)
162-
dailyKey := fmt.Sprintf("ts:events:%s:daily", projectId)
133+
// getTimeSeriesKey generates a Redis TimeSeries key for project metrics
134+
func getTimeSeriesKey(projectId, metricType, granularity string) string {
135+
return fmt.Sprintf("ts:project-%s:%s:%s", metricType, projectId, granularity)
136+
}
137+
138+
// recordProjectMetrics records project metrics to Redis TimeSeries
139+
// metricType can be: "events-accepted", "events-rate-limited", etc.
140+
func (handler *Handler) recordProjectMetrics(projectId, metricType string) {
141+
minutelyKey := getTimeSeriesKey(projectId, metricType, "minutely")
142+
hourlyKey := getTimeSeriesKey(projectId, metricType, "hourly")
143+
dailyKey := getTimeSeriesKey(projectId, metricType, "daily")
163144

164145
labels := map[string]string{
165146
"type": "error",
166-
"status": "test",
147+
"status": metricType,
167148
"project": projectId,
168149
}
169150

170-
now := time.Now()
171-
172-
// Minutely data: last 24 hours (1440 minutes)
173-
log.Infof("Generating minutely test data for project %s...", projectId)
174-
minuteStart := now.Add(-24 * time.Hour)
175-
for t := minuteStart; t.Before(now); t = t.Add(1 * time.Minute) {
176-
// Hash-based pseudo-random: 0-10 events per minute with realistic peaks/valleys
177-
hash := (t.Unix() * 2654435761) ^ 0xdeadbeef
178-
eventsCount := int64((hash % 11))
179-
for i := int64(0); i < eventsCount; i++ {
180-
timestamp := t.UnixNano()/int64(time.Millisecond) + i*100
181-
if err := handler.RedisClient.TSAdd(minutelyKey, 1, timestamp, labels); err != nil {
182-
return fmt.Errorf("failed to add minutely test data: %w", err)
183-
}
184-
}
185-
}
186-
187-
// Hourly data: last 7 days (168 hours)
188-
log.Infof("Generating hourly test data for project %s...", projectId)
189-
hourStart := now.Add(-7 * 24 * time.Hour)
190-
for t := hourStart; t.Before(now); t = t.Add(1 * time.Hour) {
191-
// Hash-based pseudo-random: 5-95 events per hour
192-
hash := (t.Unix() * 2654435761) ^ 0xcafebabe
193-
eventsCount := int64(5 + (hash % 90))
194-
for i := int64(0); i < eventsCount; i++ {
195-
timestamp := t.UnixNano()/int64(time.Millisecond) + i*1000
196-
if err := handler.RedisClient.TSAdd(hourlyKey, 1, timestamp, labels); err != nil {
197-
return fmt.Errorf("failed to add hourly test data: %w", err)
198-
}
199-
}
200-
}
201-
202-
// Daily data: last 90 days
203-
log.Infof("Generating daily test data for project %s...", projectId)
204-
dayStart := now.Add(-90 * 24 * time.Hour)
205-
for t := dayStart; t.Before(now); t = t.Add(24 * time.Hour) {
206-
// Hash-based pseudo-random: 100-1900 events per day
207-
hash := (t.Unix() * 2654435761) ^ 0xbaadf00d
208-
eventsCount := int64(100 + (hash % 1800))
209-
for i := int64(0); i < eventsCount; i++ {
210-
timestamp := t.UnixNano()/int64(time.Millisecond) + i*10000
211-
if err := handler.RedisClient.TSAdd(dailyKey, 1, timestamp, labels); err != nil {
212-
return fmt.Errorf("failed to add daily test data: %w", err)
213-
}
214-
}
215-
}
216-
217-
log.Infof("Test data generation completed for project %s", projectId)
218-
return nil
151+
// minutely: store for 24 hours
152+
if err := handler.RedisClient.SafeTSIncrBy(minutelyKey, 1, labels, 24*time.Hour); err != nil {
153+
log.Errorf("failed to increment minutely TS for %s: %v", metricType, err)
154+
}
155+
156+
// hourly: store for 7 days
157+
if err := handler.RedisClient.SafeTSIncrBy(hourlyKey, 1, labels, 7*24*time.Hour); err != nil {
158+
log.Errorf("failed to increment hourly TS for %s: %v", metricType, err)
159+
}
160+
161+
// daily: store for 90 days
162+
if err := handler.RedisClient.SafeTSIncrBy(dailyKey, 1, labels, 90*24*time.Hour); err != nil {
163+
log.Errorf("failed to increment daily TS for %s: %v", metricType, err)
164+
}
219165
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package errorshandler
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
log "github.com/sirupsen/logrus"
8+
)
9+
10+
// GenerateTestTimeSeriesData - generates test data for minutely, hourly, and daily time series
11+
// This should be called after manually deleting the Redis keys
12+
// Usage: handler.GenerateTestTimeSeriesData(projectId)
13+
func (handler *Handler) GenerateTestTimeSeriesData(projectId string) error {
14+
metricType := "events-accepted"
15+
minutelyKey := getTimeSeriesKey(projectId, metricType, "minutely")
16+
hourlyKey := getTimeSeriesKey(projectId, metricType, "hourly")
17+
dailyKey := getTimeSeriesKey(projectId, metricType, "daily")
18+
19+
labels := map[string]string{
20+
"type": "error",
21+
"status": "test",
22+
"project": projectId,
23+
}
24+
25+
now := time.Now()
26+
27+
// Minutely data: last 24 hours (1440 minutes)
28+
log.Infof("Generating minutely test data for project %s...", projectId)
29+
minuteStart := now.Add(-24 * time.Hour)
30+
for t := minuteStart; t.Before(now); t = t.Add(1 * time.Minute) {
31+
// Hash-based pseudo-random: 0-10 events per minute with realistic peaks/valleys
32+
// Using multiplicative hash (2654435761 is a prime close to 2^32/phi) XOR with constant
33+
// to generate deterministic but pseudo-random event counts based on timestamp
34+
hash := (t.Unix() * 2654435761) ^ 0xdeadbeef
35+
eventsCount := int64((hash % 11))
36+
37+
// Use TSIncrBy to increment counter for this minute
38+
timestamp := t.UnixNano() / int64(time.Millisecond)
39+
if err := handler.RedisClient.TSIncrBy(minutelyKey, eventsCount, timestamp, labels); err != nil {
40+
return fmt.Errorf("failed to add minutely test data: %w", err)
41+
}
42+
}
43+
44+
// Hourly data: last 7 days (168 hours)
45+
log.Infof("Generating hourly test data for project %s...", projectId)
46+
hourStart := now.Add(-7 * 24 * time.Hour)
47+
for t := hourStart; t.Before(now); t = t.Add(1 * time.Hour) {
48+
// Hash-based pseudo-random: 5-95 events per hour
49+
hash := (t.Unix() * 2654435761) ^ 0xcafebabe
50+
eventsCount := int64(5 + (hash % 90))
51+
52+
timestamp := t.UnixNano() / int64(time.Millisecond)
53+
if err := handler.RedisClient.TSIncrBy(hourlyKey, eventsCount, timestamp, labels); err != nil {
54+
return fmt.Errorf("failed to add hourly test data: %w", err)
55+
}
56+
}
57+
58+
// Daily data: last 90 days
59+
log.Infof("Generating daily test data for project %s...", projectId)
60+
dayStart := now.Add(-90 * 24 * time.Hour)
61+
for t := dayStart; t.Before(now); t = t.Add(24 * time.Hour) {
62+
// Hash-based pseudo-random: 100-1900 events per day
63+
hash := (t.Unix() * 2654435761) ^ 0xbaadf00d
64+
eventsCount := int64(100 + (hash % 1800))
65+
66+
timestamp := t.UnixNano() / int64(time.Millisecond)
67+
if err := handler.RedisClient.TSIncrBy(dailyKey, eventsCount, timestamp, labels); err != nil {
68+
return fmt.Errorf("failed to add daily test data: %w", err)
69+
}
70+
}
71+
72+
log.Infof("Test data generation completed for project %s", projectId)
73+
return nil
74+
}

0 commit comments

Comments
 (0)