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 }