bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/database/database.go (about)

     1  //Package database implements all persistent data access for bosun.
     2  //Internally it runs ledisdb locally, but uses a redis client to access all data.
     3  //Thus it should be able to migrate to a remote redis instance with minimal effort.
     4  package database
     5  
     6  import (
     7  	"fmt"
     8  	"log"
     9  	"runtime"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"bosun.org/cmd/bosun/database/sentinel"
    15  	"bosun.org/collect"
    16  	"bosun.org/metadata"
    17  	"bosun.org/opentsdb"
    18  	"bosun.org/slog"
    19  	"github.com/garyburd/redigo/redis"
    20  	"github.com/siddontang/ledisdb/config"
    21  	"github.com/siddontang/ledisdb/server"
    22  
    23  	"github.com/captncraig/easyauth/providers/token/redisStore"
    24  )
    25  
    26  var SchemaVersion = int64(2)
    27  
    28  // Core data access interface for everything sched needs
    29  type DataAccess interface {
    30  	RedisConnector
    31  	Metadata() MetadataDataAccess
    32  	Configs() ConfigDataAccess
    33  	Search() SearchDataAccess
    34  	Errors() ErrorDataAccess
    35  	State() StateDataAccess
    36  	Silence() SilenceDataAccess
    37  	Notifications() NotificationDataAccess
    38  	Migrate() error
    39  }
    40  
    41  type MetadataDataAccess interface {
    42  	// Insert Metric Metadata. Field must be one of "desc", "rate", or "unit".
    43  	PutMetricMetadata(metric string, field string, value string) error
    44  	// Get Metric Metadata for given metric.
    45  	GetMetricMetadata(metric string) (*MetricMetadata, error)
    46  
    47  	PutTagMetadata(tags opentsdb.TagSet, name string, value string, updated time.Time) error
    48  	GetTagMetadata(tags opentsdb.TagSet, name string) ([]*TagMetadata, error)
    49  	DeleteTagMetadata(tags opentsdb.TagSet, name string) error
    50  }
    51  
    52  type SearchDataAccess interface {
    53  	AddMetricForTag(tagK, tagV, metric string, time int64) error
    54  	GetMetricsForTag(tagK, tagV string) (map[string]int64, error)
    55  
    56  	AddTagKeyForMetric(metric, tagK string, time int64) error
    57  	GetTagKeysForMetric(metric string) (map[string]int64, error)
    58  
    59  	AddMetric(metric string, time int64) error
    60  	GetAllMetrics() (map[string]int64, error)
    61  
    62  	AddTagValue(metric, tagK, tagV string, time int64) error
    63  	GetTagValues(metric, tagK string) (map[string]int64, error)
    64  
    65  	AddMetricTagSet(metric, tagSet string, time int64) error
    66  	GetMetricTagSets(metric string, tags opentsdb.TagSet) (map[string]int64, error)
    67  
    68  	BackupLastInfos(map[string]map[string]*LastInfo) error
    69  	LoadLastInfos() (map[string]map[string]*LastInfo, error)
    70  }
    71  
    72  type dataAccess struct {
    73  	pool    *redis.Pool
    74  	isRedis bool
    75  }
    76  
    77  // Create a new data access object pointed at the specified address. isRedis parameter used to distinguish true redis from ledis in-proc.
    78  func NewDataAccess(addr []string, isRedis bool, masterName string, redisDb int, redisPass string) DataAccess {
    79  	return newDataAccess(addr, isRedis, masterName, redisDb, redisPass)
    80  }
    81  
    82  func newDataAccess(addr []string, isRedis bool, masterName string, redisDb int, redisPass string) *dataAccess {
    83  	return &dataAccess{
    84  		pool:    newPool(addr, redisPass, masterName, redisDb, isRedis, 1000, true),
    85  		isRedis: isRedis,
    86  	}
    87  }
    88  
    89  // Start in-process ledis server. Data will go in the specified directory and it will bind to the given port.
    90  // Return value is a function you can call to stop the server.
    91  func StartLedis(dataDir string, bind string) (stop func(), err error) {
    92  	cfg := config.NewConfigDefault()
    93  	cfg.DBName = "goleveldb"
    94  	cfg.Addr = bind
    95  	cfg.DataDir = dataDir
    96  	app, err := server.NewApp(cfg)
    97  	if err != nil {
    98  		log.Fatal(err)
    99  		return func() {}, err
   100  	}
   101  	go app.Run()
   102  	return app.Close, nil
   103  }
   104  
   105  //RedisConnector is a simple interface so things can get a raw connection (mostly tests), but still discourage it.
   106  // makes dataAccess interchangable with redis.Pool
   107  type RedisConnector interface {
   108  	Get() redis.Conn
   109  }
   110  
   111  //simple wrapper around a redis conn. Uses close to also stop and submit a simple timer for bosun stats on operations.
   112  type connWrapper struct {
   113  	redis.Conn
   114  	closer func()
   115  }
   116  
   117  func (c *connWrapper) Close() error {
   118  	err := c.Conn.Close()
   119  	c.closer()
   120  	return err
   121  }
   122  
   123  func (d *dataAccess) Get() redis.Conn {
   124  	closer := collect.StartTimer("redis", opentsdb.TagSet{"op": myCallerName()})
   125  	return &connWrapper{
   126  		Conn:   d.pool.Get(),
   127  		closer: closer,
   128  	}
   129  }
   130  
   131  var _ redisStore.Connector = (*dataAccess)(nil) //just a compile time interface check
   132  
   133  //gets name of function that called the currently executing function.
   134  func myCallerName() string {
   135  	fpcs := make([]uintptr, 1)
   136  	runtime.Callers(3, fpcs)
   137  	fun := runtime.FuncForPC(fpcs[0])
   138  	nameSplit := strings.Split(fun.Name(), ".")
   139  	return nameSplit[len(nameSplit)-1]
   140  }
   141  
   142  func newPool(servers []string, password, masterName string, database int, isRedis bool, maxActive int, wait bool) *redis.Pool {
   143  	var lastMu sync.Mutex
   144  	var lastMaster string
   145  	var sntnl *sentinel.Sentinel
   146  	var serverAddr string
   147  	if masterName != "" {
   148  		// It is the Sentinel
   149  		sntnl = &sentinel.Sentinel{
   150  			Addrs:      servers,
   151  			MasterName: masterName,
   152  			Dial: func(addr string) (redis.Conn, error) {
   153  				timeout := 500 * time.Millisecond
   154  				opts := []redis.DialOption{
   155  					redis.DialConnectTimeout(timeout),
   156  					redis.DialReadTimeout(timeout),
   157  					redis.DialWriteTimeout(timeout),
   158  				}
   159  				c, err := redis.Dial("tcp", addr, opts...)
   160  				if err != nil {
   161  					slog.Errorf("Error while redis connect: %s", err.Error())
   162  					return nil, err
   163  				}
   164  				return c, nil
   165  			},
   166  		}
   167  		go func() {
   168  			if err := sntnl.Discover(); err != nil {
   169  				slog.Errorf("Error while discover redis master from sentinel: %s", err.Error())
   170  			}
   171  			for {
   172  				select {
   173  				case <-time.After(30 * time.Second):
   174  					if err := sntnl.Discover(); err != nil {
   175  						slog.Errorf("Error while discover redis master from sentinel: %s", err.Error())
   176  					}
   177  				}
   178  			}
   179  		}()
   180  	}
   181  	return &redis.Pool{
   182  		MaxIdle:     50,
   183  		MaxActive:   maxActive,
   184  		Wait:        wait,
   185  		IdleTimeout: 240 * time.Second,
   186  		Dial: func() (redis.Conn, error) {
   187  			if masterName != "" {
   188  				var err error
   189  				serverAddr, err = sntnl.MasterAddr()
   190  				if err != nil {
   191  					slog.Errorf("Error while get redis master from sentinel: %s", err.Error())
   192  					return nil, err
   193  				}
   194  				lastMu.Lock()
   195  				if serverAddr != lastMaster {
   196  					lastMaster = serverAddr
   197  				}
   198  				lastMu.Unlock()
   199  			} else {
   200  				if len(servers) == 0 {
   201  					return nil, fmt.Errorf("Server address didn't defined")
   202  				}
   203  				serverAddr = servers[0]
   204  			}
   205  			c, err := redis.Dial("tcp", serverAddr, redis.DialDatabase(database))
   206  			if err != nil {
   207  				return nil, err
   208  			}
   209  			if password != "" {
   210  				if _, err := c.Do("AUTH", password); err != nil {
   211  					c.Close()
   212  					return nil, err
   213  				}
   214  			}
   215  			if isRedis {
   216  				if _, err := c.Do("CLIENT", "SETNAME", "bosun"); err != nil {
   217  					c.Close()
   218  					return nil, err
   219  				}
   220  			}
   221  			return c, err
   222  		},
   223  	}
   224  }
   225  
   226  func init() {
   227  	collect.AggregateMeta("bosun.redis", metadata.MilliSecond, "time in milliseconds per redis call.")
   228  }
   229  
   230  // Ledis can't do DEL in a blanket way like redis can. It has a unique command per type.
   231  // These helpers allow easy switching.
   232  func (d *dataAccess) LCLEAR() string {
   233  	if d.isRedis {
   234  		return "DEL"
   235  	}
   236  	return "LCLEAR"
   237  }
   238  
   239  func (d *dataAccess) SCLEAR() string {
   240  	if d.isRedis {
   241  		return "DEL"
   242  	}
   243  	return "SCLEAR"
   244  }
   245  
   246  func (d *dataAccess) HCLEAR() string {
   247  	if d.isRedis {
   248  		return "DEL"
   249  	}
   250  	return "HCLEAR"
   251  }
   252  
   253  func (d *dataAccess) LMCLEAR(key string, value string) (string, []interface{}) {
   254  	if d.isRedis {
   255  		return "LREM", []interface{}{key, 0, value}
   256  	}
   257  	return "LMCLEAR", []interface{}{key, value}
   258  }
   259  
   260  func (d *dataAccess) HSCAN() string {
   261  	if d.isRedis {
   262  		return "HSCAN"
   263  	}
   264  	return "XHSCAN"
   265  }