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  }