package redis import ( "context" "strconv" "strings" "time" "github.com/redis/go-redis/v9" "bugs.watch/agent/internal/aggregate" ) // Collector queries Redis INFO each sample interval. // All queries run under a 3-second timeout; any failure is silently skipped. // Metrics mapping: // db_conn_pct → used_memory / maxmemory * 100 (skipped when maxmemory=0) // db_threads_running → connected_clients // db_size_bytes → used_memory type Collector struct { client *redis.Client } func New(addr string) (*Collector, error) { opts, err := redis.ParseURL(addr) if err != nil { return nil, err } opts.DialTimeout = 3 * time.Second opts.ReadTimeout = 3 * time.Second opts.WriteTimeout = 3 * time.Second return &Collector{client: redis.NewClient(opts)}, nil } func (c *Collector) Name() string { return "redis" } func (c *Collector) Close() { c.client.Close() } func (c *Collector) Collect(window *aggregate.Window) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() info, err := c.client.Info(ctx, "memory", "clients").Result() if err != nil { return } fields := parseInfo(info) usedMem, _ := strconv.ParseUint(fields["used_memory"], 10, 64) maxMem, _ := strconv.ParseUint(fields["maxmemory"], 10, 64) clients, _ := strconv.ParseUint(fields["connected_clients"], 10, 64) if usedMem > 0 { window.SetDBSizeBytes(usedMem) } if clients > 0 { window.AddDBThreadsRunning(float64(clients)) } if maxMem > 0 && usedMem > 0 { window.AddDBConnPct(float64(usedMem) / float64(maxMem) * 100) } } func parseInfo(s string) map[string]string { out := make(map[string]string) for _, line := range strings.Split(s, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } k, v, ok := strings.Cut(line, ":") if ok { out[strings.TrimSpace(k)] = strings.TrimSpace(v) } } return out }