bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/conf/conf.go (about) 1 package conf // import "bosun.org/cmd/bosun/conf" 2 3 import ( 4 "bytes" 5 "fmt" 6 "hash/fnv" 7 8 "net/mail" 9 "net/url" 10 "os/exec" 11 "regexp" 12 "strings" 13 "time" 14 15 "bosun.org/cloudwatch" 16 "bosun.org/cmd/bosun/conf/template" 17 "bosun.org/cmd/bosun/expr" 18 "bosun.org/cmd/bosun/expr/parse" 19 "bosun.org/graphite" 20 "bosun.org/models" 21 "bosun.org/opentsdb" 22 "bosun.org/slog" 23 "github.com/influxdata/influxdb/client/v2" 24 ) 25 26 // SystemConfProvider providers all the information about the system configuration. 27 // the interface exists to ensure that no changes are made to the system configuration 28 // outside of the package without a setter 29 type SystemConfProvider interface { 30 GetHTTPListen() string 31 GetHTTPSListen() string 32 GetTLSCertFile() string 33 GetTLSKeyFile() string 34 35 GetRuleVars() map[string]string 36 37 GetSMTPHost() string 38 GetSMTPUsername() string // SMTP username 39 GetSMTPPassword() string // SMTP password 40 GetPing() bool 41 GetPingDuration() time.Duration 42 GetEmailFrom() string 43 GetLedisDir() string 44 GetLedisBindAddr() string 45 GetRedisHost() []string 46 GetRedisMasterName() string 47 GetRedisDb() int 48 GetRedisPassword() string 49 IsRedisClientSetName() bool 50 GetTimeAndDate() []int 51 GetSearchSince() time.Duration 52 53 GetCheckFrequency() time.Duration 54 GetDefaultRunEvery() int 55 GetAlertCheckDistribution() string 56 GetUnknownThreshold() int 57 GetMinGroupSize() int 58 59 GetShortURLKey() string 60 GetInternetProxy() string 61 62 GetRuleFilePath() string 63 SaveEnabled() bool 64 ReloadEnabled() bool 65 GetCommandHookPath() string 66 67 SetTSDBHost(tsdbHost string) 68 GetTSDBHost() string 69 70 GetAnnotateElasticHosts() expr.ElasticConfig 71 GetAnnotateIndex() string 72 73 GetAuthConf() *AuthConf 74 75 GetMaxRenderedTemplateAge() int 76 77 GetExampleExpression() string 78 79 // Contexts 80 GetTSDBContext() opentsdb.Context 81 GetGraphiteContext() graphite.Context 82 GetInfluxContext() client.HTTPConfig 83 GetElasticContext() expr.ElasticHosts 84 GetAzureMonitorContext() expr.AzureMonitorClients 85 GetCloudWatchContext() cloudwatch.Context 86 GetPromContext() expr.PromClients 87 AnnotateEnabled() bool 88 89 MakeLink(string, *url.Values) string 90 EnabledBackends() EnabledBackends 91 } 92 93 // ValidateSystemConf runs sanity checks on the system configuration 94 func ValidateSystemConf(sc SystemConfProvider) error { 95 hasSMTPHost := sc.GetSMTPHost() != "" 96 hasEmailFrom := sc.GetEmailFrom() != "" 97 if hasSMTPHost != hasEmailFrom { 98 return fmt.Errorf("email notififications require that both SMTP Host and EmailFrom be set") 99 } 100 if sc.GetDefaultRunEvery() <= 0 { 101 return fmt.Errorf("default run every must be greater than 0, is %v", sc.GetDefaultRunEvery()) 102 } 103 if sc.GetHTTPSListen() != "" && (sc.GetTLSCertFile() == "" || sc.GetTLSKeyFile() == "") { 104 return fmt.Errorf("must specify TLSCertFile and TLSKeyFile if HTTPSListen is specified") 105 } 106 return nil 107 } 108 109 // RuleConfProvider is an interface for accessing information that bosun needs to know about 110 // rule configuration. Rule configuration includes Macros, Alerts, Notifications, Lookup 111 // tables, squelching, and variable expansion. Currently there is only one implementation of 112 // this inside bosun in the rule package. The interface exists to ensure that the rest of 113 // Bosun does not manipulate the rule configuration in unexpected ways. Also so the possibility 114 // of an alternative store for rules can exist the future. However, when this is added it is expected 115 // that the interface will change significantly. 116 type RuleConfProvider interface { 117 RuleConfWriter 118 GetTemplate(string) *Template 119 120 GetAlerts() map[string]*Alert 121 GetAlert(string) *Alert 122 123 GetNotifications() map[string]*Notification 124 GetNotification(string) *Notification 125 126 GetLookup(string) *Lookup 127 128 AlertSquelched(*Alert) func(opentsdb.TagSet) bool 129 Squelched(*Alert, opentsdb.TagSet) bool 130 Expand(string, map[string]string, bool) string 131 GetFuncs(EnabledBackends) map[string]parse.Func 132 } 133 134 // RuleConfWriter is a collection of the methods that are used to manipulate the configuration 135 // Save methods will trigger the reload that has been passed to the rule configuration 136 type RuleConfWriter interface { 137 BulkEdit(BulkEditRequest) error 138 GetRawText() string 139 GetHash() string 140 SaveRawText(rawConf, diff, user, message string, args ...string) error 141 RawDiff(rawConf string) (string, error) 142 SetReload(reload func() error) 143 SetSaveHook(SaveHook) 144 } 145 146 // Squelch is a map of tag keys to regexes that are applied to tag values. Squelches 147 // are used to filter results from query responses 148 type Squelch map[string]*regexp.Regexp 149 150 // Squelches is a collection of Squelch 151 type Squelches []Squelch 152 153 // Add adds a sqluech baed on the tags in the first argument. The value of the tag 154 // is a regular expression. Tags are passed as a string in the format of 155 func (s *Squelches) Add(v string) error { 156 tags, err := opentsdb.ParseTags(v) 157 if tags == nil && err != nil { 158 return err 159 } 160 sq := make(Squelch) 161 for k, v := range tags { 162 re, err := regexp.Compile(v) 163 if err != nil { 164 return err 165 } 166 sq[k] = re 167 } 168 *s = append(*s, sq) 169 return nil 170 } 171 172 // Squelched takes a tag set and returns true if the given 173 // tagset should be squelched based on the Squelches 174 func (s *Squelches) Squelched(tags opentsdb.TagSet) bool { 175 for _, squelch := range *s { 176 if squelch.Squelched(tags) { 177 return true 178 } 179 } 180 return false 181 } 182 183 // Squelched takes a tag set and returns true if the given 184 // tagset should be squelched based on the Squelche 185 func (s Squelch) Squelched(tags opentsdb.TagSet) bool { 186 if len(s) == 0 { 187 return false 188 } 189 for k, v := range s { 190 tagv, ok := tags[k] 191 if !ok || !v.MatchString(tagv) { 192 return false 193 } 194 } 195 return true 196 } 197 198 // Template stores information about a notification template. Templates 199 // are based on Go's text and html/template. 200 type Template struct { 201 Text string 202 Vars 203 Name string 204 Body *template.Template `json:"-"` 205 Subject *template.Template `json:"-"` 206 CustomTemplates map[string]*template.Template `json:"-"` 207 208 RawBody, RawSubject string 209 RawCustoms map[string]string 210 211 Locator `json:"-"` 212 } 213 214 func (t *Template) Get(name string) *template.Template { 215 if name == "body" { 216 return t.Body 217 } 218 if name == "subject" { 219 return t.Subject 220 } 221 return t.CustomTemplates[name] 222 } 223 224 // NotificationTemplateKeys is the set of fields that may be templated out per notification. Each field points to the name of a field on a template object. 225 type NotificationTemplateKeys struct { 226 PostTemplate, GetTemplate string // templates to use for post/get urls 227 BodyTemplate string // template to use for post body or email body. defaults to "body" for post and "emailBody" (if it exists) for email 228 EmailSubjectTemplate string // template to use for email subject. Default to "subject" 229 } 230 231 // Combine merges keys from another set, copying only those values that do not exist on the first set of template keys. 232 // It returns a new object every time, and accepts nils on either side. 233 func (n *NotificationTemplateKeys) Combine(defaults *NotificationTemplateKeys) *NotificationTemplateKeys { 234 n2 := &NotificationTemplateKeys{} 235 if n != nil { 236 n2.PostTemplate = n.PostTemplate 237 n2.GetTemplate = n.GetTemplate 238 n2.BodyTemplate = n.BodyTemplate 239 n2.EmailSubjectTemplate = n.EmailSubjectTemplate 240 } 241 if defaults == nil { 242 return n2 243 } 244 if n2.PostTemplate == "" { 245 n2.PostTemplate = defaults.PostTemplate 246 } 247 if n2.GetTemplate == "" { 248 n2.GetTemplate = defaults.GetTemplate 249 } 250 if n2.BodyTemplate == "" { 251 n2.BodyTemplate = defaults.BodyTemplate 252 } 253 if n2.EmailSubjectTemplate == "" { 254 n2.EmailSubjectTemplate = defaults.EmailSubjectTemplate 255 } 256 return n2 257 } 258 259 // Notification stores information about a notification. A notification 260 // is the definition of an action that should be performed when an 261 // alert is triggered 262 type Notification struct { 263 Text string 264 Vars 265 Name string 266 Email []*mail.Address 267 268 Post, Get *url.URL 269 270 // template keys to use for plain notifications 271 NotificationTemplateKeys 272 273 // template keys to use for action notifications. ActionNone contains catch-all fields if present. More specific will override. 274 ActionTemplateKeys map[models.ActionType]*NotificationTemplateKeys 275 276 UnknownTemplateKeys NotificationTemplateKeys 277 UnknownMultiTemplateKeys NotificationTemplateKeys 278 279 Print bool 280 Next *Notification 281 Timeout time.Duration 282 ContentType string 283 RunOnActions string 284 GroupActions bool 285 286 UnknownMinGroupSize *int // nil means use global defaults. 0 means no-grouping at all. 287 UnknownThreshold *int // nil means use global defaults. 0 means no limit 288 289 NextName string `json:"-"` 290 RawEmail string `json:"-"` 291 RawPost, RawGet string `json:"-"` 292 293 Locator `json:"-"` 294 } 295 296 // Vars holds a map of variable names to the variable's value 297 type Vars map[string]string 298 299 // Notifications contains a mapping of notification names to 300 // all notifications in the configuration. The Lookups Property 301 // enables notification lookups - the ability to trigger different 302 // notifications based an alerts resulting tags 303 type Notifications struct { 304 Notifications map[string]*Notification `json:"-"` 305 // Table key -> table 306 Lookups map[string]*Lookup 307 } 308 309 // Get returns the set of notifications based on given tags and applys any notification 310 // lookup tables 311 func (ns *Notifications) Get(c RuleConfProvider, tags opentsdb.TagSet) map[string]*Notification { 312 nots := make(map[string]*Notification) 313 for name, n := range ns.Notifications { 314 nots[name] = n 315 } 316 for key, lookup := range ns.Lookups { 317 l := lookup.ToExpr() 318 val, ok := l.Get(key, tags) 319 if !ok { 320 continue 321 } 322 ns := make(map[string]*Notification) 323 for _, s := range strings.Split(val, ",") { 324 s = strings.TrimSpace(s) 325 n := c.GetNotification(s) 326 if n == nil { 327 continue // TODO error here? 328 } 329 ns[s] = n 330 } 331 for name, n := range ns { 332 nots[name] = n 333 } 334 } 335 return nots 336 } 337 338 // GetAllChained returns all unique notifications, including chains 339 func (ns *Notifications) GetAllChained() map[string]*Notification { 340 m := map[string]*Notification{} 341 var walk func(not *Notification) 342 walk = func(not *Notification) { 343 if m[not.Name] != nil { 344 return 345 } 346 m[not.Name] = not 347 if not.Next != nil { 348 walk(not.Next) 349 } 350 } 351 for _, not := range ns.Notifications { 352 walk(not) 353 } 354 return m 355 } 356 357 // GetNotificationChains returns the warn or crit notification chains for a configured 358 // alert. Each chain is a list of notification names. If a notification name 359 // as already been seen in the chain it ends the list with the notification 360 // name with a of "..." which indicates that the chain will loop. 361 func GetNotificationChains(n map[string]*Notification) [][]string { 362 chains := [][]string{} 363 for _, root := range n { 364 chain := []string{} 365 seen := make(map[string]bool) 366 var walkChain func(next *Notification) 367 walkChain = func(next *Notification) { 368 if next == nil { 369 chains = append(chains, chain) 370 return 371 } 372 if seen[next.Name] { 373 chain = append(chain, fmt.Sprintf("...%v", next.Name)) 374 chains = append(chains, chain) 375 return 376 } 377 chain = append(chain, next.Name) 378 seen[next.Name] = true 379 walkChain(next.Next) 380 } 381 walkChain(root) 382 } 383 return chains 384 } 385 386 // A Lookup is used to return values based on the tags of a response. It 387 // provides switch/case functionality 388 type Lookup struct { 389 Text string 390 Name string 391 Tags []string 392 Entries []*Entry 393 Locator `json:"-"` 394 } 395 396 func (lookup *Lookup) ToExpr() *ExprLookup { 397 l := ExprLookup{ 398 Tags: lookup.Tags, 399 } 400 for _, entry := range lookup.Entries { 401 l.Entries = append(l.Entries, entry.ExprEntry) 402 } 403 return &l 404 } 405 406 // Entry is an entry in a Lookup. 407 type Entry struct { 408 *ExprEntry 409 Def string 410 Name string 411 } 412 413 // Macro provides the ability to reuse partial sections of 414 // alert definition text. Macros can contain other macros 415 type Macro struct { 416 Text string 417 Pairs interface{} // this is BAD TODO 418 Name string 419 Locator `json:"-"` 420 } 421 422 // Alert stores all information about alerts. All other major 423 // sections of rule configuration are referenced by alerts including 424 // Templates, Macros, and Notifications. Alerts hold the expressions 425 // that determine the Severity of the Alert. There are also flags the 426 // alter the behavior of the alert and how the expression is evaluated. 427 // This structure is available to users from templates. Consult documentation 428 // before making changes 429 type Alert struct { 430 Text string 431 Vars 432 *Template `json:"-"` 433 Name string 434 Crit *expr.Expr `json:",omitempty"` 435 Warn *expr.Expr `json:",omitempty"` 436 Depends *expr.Expr `json:",omitempty"` 437 Squelch Squelches `json:"-"` 438 CritNotification *Notifications 439 WarnNotification *Notifications 440 Unknown time.Duration 441 MaxLogFrequency time.Duration 442 IgnoreUnknown bool 443 UnknownsNormal bool 444 UnjoinedOK bool `json:",omitempty"` 445 Log bool 446 RunEvery int 447 ReturnType models.FuncType 448 449 TemplateName string `json:"-"` 450 RawSquelch []string `json:"-"` 451 452 Locator `json:"-"` 453 AlertTemplateKeys map[string]*template.Template `json:"-"` 454 } 455 456 // A Locator stores the information about the location of the rule in the underlying 457 // rule store 458 type Locator interface{} 459 460 // BulkEditRequest is a collection of BulkEditRequest to be applied sequentially 461 type BulkEditRequest []EditRequest 462 463 // EditRequest is a proposed edit to the config file for sections. The Name is the name of section, 464 // Type can be "alert", "template", "notification", "lookup", or "macro". The Text should be the full 465 // text of the definition, including the declaration and brackets (i.e. "alert foo { .. }"). If Delete 466 // is true then the section will be deleted. In order to rename something, specify the old name in the 467 // Name field but have the Text definition contain the new name. 468 type EditRequest struct { 469 Name string 470 Type string 471 Text string 472 Delete bool 473 } 474 475 // SaveHook is a function that is passed files as a string (currently the only implementation 476 // has a single file, so there is no convention for the format of multiple files yet), a user 477 // a message and vargs. A SaveHook is called when using bosun to save the config. A save is reverted 478 // when the SaveHook returns an error. 479 type SaveHook func(files, user, message string, args ...string) error 480 481 // MakeSaveCommandHook takes a function based on the command name and will run it on save passing files, user, 482 // message, args... as arguments to the command. For the SaveHook function that is returned, If the command fails 483 // to execute or returns a non normal output then an error is returned. 484 func MakeSaveCommandHook(cmdName string) (f SaveHook, err error) { 485 _, err = exec.LookPath(cmdName) 486 if err != nil { 487 return f, fmt.Errorf("command %v not found, failed to create save hook: %v", cmdName, err) 488 } 489 f = func(files, user, message string, args ...string) error { 490 cArgs := []string{files, user, message} 491 cArgs = append(cArgs, args...) 492 slog.Infof("executing save hook %v\n", cmdName) 493 c := exec.Command(cmdName, cArgs...) 494 var cOut bytes.Buffer 495 var cErr bytes.Buffer 496 c.Stdout = &cOut 497 c.Stderr = &cErr 498 err := c.Start() 499 if err != nil { 500 return err 501 } 502 err = c.Wait() 503 if err != nil { 504 slog.Warning(cErr.String()) 505 return fmt.Errorf("%v: %v", err, cErr.String()) 506 } 507 slog.Infof("save hook output: %v\n", cOut.String()) 508 return nil 509 } 510 return 511 } 512 513 // GenHash generates a unique hash of a string. It is used so we can compare 514 // edited text configuration to running text configuration and see if it has 515 // changed 516 func GenHash(s string) string { 517 h := fnv.New32a() 518 h.Write([]byte(s)) 519 return fmt.Sprintf("%v", h.Sum32()) 520 }