InVisionApp/go-health

View on GitHub
checkers/redis/redis.go

Summary

Maintainability
A
35 mins
Test Coverage
package redischk

import (
    "crypto/tls"
    "fmt"
    "time"

    "github.com/go-redis/redis"
)

const (
    // RedisDefaultSetValue will be used if the "Set" check method is enabled
    // and "RedisSetOptions.Value" is _not_ set.
    RedisDefaultSetValue = "go-health/redis-check"
)

// RedisConfig is used for configuring the go-redis check.
//
// "Auth" is _required_; redis connection/auth config.
//
// "Ping" is optional; the most basic check method, performs a `.Ping()` on the client.
//
// "Get" is optional; perform a "GET" on a key; refer to the "RedisGetOptions" docs for details.
//
// "Set" is optional; perform a "SET" on a key; refer to the "RedisSetOptions" docs for details.
//
// Note: At least _one_ check method must be set/enabled; you can also enable
// _all_ of the check methods (ie. perform a ping, set this key and now try to
// retrieve that key).
type RedisConfig struct {
    Auth *RedisAuthConfig
    Ping bool
    Set  *RedisSetOptions
    Get  *RedisGetOptions
}

// RedisAuthConfig defines how to connect to redis.
type RedisAuthConfig struct {
    Addr     string // `host:port` format
    Password string // leave blank if no password
    DB       int    // leave unset if no specific db

    TLS *tls.Config // TLS config in case we are using in-transit encryption
}

// RedisSetOptions contains attributes that can alter the behavior of the redis
// "SET" check.
//
// "Key" is _required_; the name of the key we are attempting to "SET".
//
// "Value" is optional; what the value should hold; if not set, it will be set
// to "RedisDefaultSetValue".
//
// "Expiration" is optional; if set, a TTL will be attached to the key.
type RedisSetOptions struct {
    Key        string
    Value      string
    Expiration time.Duration
}

// RedisGetOptions contains attributes that can alter the behavior of the redis
// "GET" check.
//
// "Key" is _required_; the name of the key that we are attempting to "GET".
//
// "Expect" is optional; optionally verify that the value for the key matches
// the Expect value.
//
// "NoErrorMissingKey" is optional; by default, the "GET" check will error if
// the key we are fetching does not exist; flip this bool if that is normal/expected/ok.
type RedisGetOptions struct {
    Key               string
    Expect            string
    NoErrorMissingKey bool
}

// Redis implements the ICheckable interface
type Redis struct {
    Config *RedisConfig
    client *redis.Client
}

// NewRedis creates a new "go-redis/redis" checker that can be used w/ "AddChecks()".
func NewRedis(cfg *RedisConfig) (*Redis, error) {
    // validate settings
    if err := validateRedisConfig(cfg); err != nil {
        return nil, fmt.Errorf("Unable to validate redis config: %v", err)
    }

    // try to connect
    c := redis.NewClient(&redis.Options{
        Addr:     cfg.Auth.Addr,
        Password: cfg.Auth.Password, // no password set
        DB:       cfg.Auth.DB,       // use default DB

        TLSConfig: cfg.Auth.TLS,
    })

    if _, err := c.Ping().Result(); err != nil {
        return nil, fmt.Errorf("Unable to establish initial connection to redis: %v", err)
    }

    return &Redis{
        Config: cfg,
        client: c,
    }, nil
}

// Status is used for performing a redis check against a dependency; it satisfies
// the "ICheckable" interface.
func (r *Redis) Status() (interface{}, error) {
    if r.Config.Ping {
        if _, err := r.client.Ping().Result(); err != nil {
            return nil, fmt.Errorf("Ping failed: %v", err)
        }
    }

    if r.Config.Set != nil {
        err := r.client.Set(r.Config.Set.Key, r.Config.Set.Value, r.Config.Set.Expiration).Err()
        if err != nil {
            return nil, fmt.Errorf("Unable to complete set: %v", err)
        }
    }

    if r.Config.Get != nil {
        val, err := r.client.Get(r.Config.Get.Key).Result()
        if err != nil {
            if err == redis.Nil {
                if !r.Config.Get.NoErrorMissingKey {
                    return nil, fmt.Errorf("Unable to complete get: '%v' not found", r.Config.Get.Key)
                }
            } else {
                return nil, fmt.Errorf("Unable to complete get: %v", err)
            }
        }

        if r.Config.Get.Expect != "" {
            if r.Config.Get.Expect != val {
                return nil, fmt.Errorf("Unable to complete get: returned value '%v' does not match expected value '%v'",
                    val, r.Config.Get.Expect)
            }
        }
    }

    return nil, nil
}

func validateRedisConfig(cfg *RedisConfig) error {
    if cfg == nil {
        return fmt.Errorf("Main config cannot be nil")
    }

    if cfg.Auth == nil {
        return fmt.Errorf("Auth config cannot be nil")
    }

    if cfg.Auth.Addr == "" {
        return fmt.Errorf("Addr string must be set in auth config")
    }

    // At least one check method must be set
    if !cfg.Ping && cfg.Set == nil && cfg.Get == nil {
        return fmt.Errorf("At minimum, either cfg.Ping, cfg.Set or cfg.Get must be set")
    }

    // If .Set is set, verify that at minimum .Key is set
    if cfg.Set != nil {
        if cfg.Set.Key == "" {
            return fmt.Errorf("If cfg.Set is used, cfg.Set.Key must be set")
        }

        if cfg.Set.Value == "" {
            cfg.Set.Value = RedisDefaultSetValue
        }
    }

    // If .Get is set, verify that at minimum .Key is set
    if cfg.Get != nil {
        if cfg.Get.Key == "" {
            return fmt.Errorf("If cfg.Get is used, cfg.Get.Key must be set")
        }
    }

    return nil
}