github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/logger/debugger_redis.go (about) 1 package logger 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/redis/go-redis/v9" 11 ) 12 13 const ( 14 debugRedisAddChannel = "add:log-debug" 15 debugRedisRmvChannel = "rmv:log-debug" 16 debugRedisPrefix = "debug:" 17 ) 18 19 var ( 20 ErrInvalidDomainFormat = errors.New("invalid domain format") 21 ) 22 23 // RedisDebugger is a redis based [Debugger] implementation. 24 // 25 // It use redis to synchronize all the instances between them. 26 // This implementation is safe to use in a multi instance setup. 27 // 28 // Technically speaking this is a wrapper around a [MemDebugger] using redis pub/sub 29 // and store for syncing the instances between them and restoring the domain list at 30 // startup. 31 type RedisDebugger struct { 32 client redis.UniversalClient 33 store *MemDebugger 34 sub *redis.PubSub 35 } 36 37 // NewRedisDebugger instantiates a new [RedisDebugger], bootstraps the service 38 // with the state saved in redis and starts the subscription to the change 39 // channel. 40 func NewRedisDebugger(client redis.UniversalClient) (*RedisDebugger, error) { 41 ctx := context.Background() 42 43 dbg := &RedisDebugger{ 44 client: client, 45 store: NewMemDebugger(), 46 sub: client.Subscribe(ctx, debugRedisAddChannel, debugRedisRmvChannel), 47 } 48 49 go dbg.bootstrap(ctx) 50 go dbg.subscribeEvents() 51 52 return dbg, nil 53 } 54 55 // AddDomain adds the specified domain to the debug list. 56 func (r *RedisDebugger) AddDomain(domain string, ttl time.Duration) error { 57 ctx := context.Background() 58 59 if strings.ContainsRune(domain, '/') { 60 return ErrInvalidDomainFormat 61 } 62 63 // Publish the domain to add to the other instances, the memory storage will be updated 64 // when the instance will consume its own event. 65 err := r.client.Publish(ctx, debugRedisAddChannel, domain+"/"+ttl.String()).Err() 66 if err != nil { 67 return err 68 } 69 70 key := debugRedisPrefix + domain 71 err = r.client.Set(ctx, key, 0, ttl).Err() 72 73 return err 74 } 75 76 // RemoveDomain removes the specified domain from the debug list. 77 func (r *RedisDebugger) RemoveDomain(domain string) error { 78 ctx := context.Background() 79 80 // Publish the domain to remove to the other instances, the memory storage will be updated 81 // when the instance will consume its own event. 82 err := r.client.Publish(ctx, debugRedisRmvChannel, domain+"/0").Err() 83 if err != nil { 84 return err 85 } 86 87 key := debugRedisPrefix + domain 88 err = r.client.Del(ctx, key).Err() 89 return err 90 } 91 92 func (r *RedisDebugger) ExpiresAt(domain string) *time.Time { 93 return r.store.ExpiresAt(domain) 94 } 95 96 func (r *RedisDebugger) subscribeEvents() { 97 for msg := range r.sub.Channel() { 98 parts := strings.Split(msg.Payload, "/") 99 domain := parts[0] 100 101 switch msg.Channel { 102 case debugRedisAddChannel: 103 var ttl time.Duration 104 if len(parts) >= 2 { 105 ttl, _ = time.ParseDuration(parts[1]) 106 } 107 108 _ = r.store.AddDomain(domain, ttl) 109 110 case debugRedisRmvChannel: 111 r.store.RemoveDomain(domain) 112 } 113 } 114 } 115 116 // Close the redis client and the subscription channel. 117 func (r *RedisDebugger) Close() error { 118 err := r.sub.Close() 119 if err != nil { 120 return fmt.Errorf("failed to close the subscription: %w", err) 121 } 122 123 err = r.client.Close() 124 if err != nil { 125 return fmt.Errorf("failed to close the client: %w", err) 126 } 127 128 return nil 129 } 130 131 func (r *RedisDebugger) bootstrap(ctx context.Context) { 132 keys, err := r.client.Keys(ctx, debugRedisPrefix+"*").Result() 133 if err != nil { 134 return 135 } 136 137 for _, key := range keys { 138 ttl, err := r.client.TTL(ctx, key).Result() 139 if err != nil { 140 continue 141 } 142 143 domain := strings.TrimPrefix(key, debugRedisPrefix) 144 r.store.AddDomain(domain, ttl) 145 } 146 }