bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/conf/rule/loaders.go (about) 1 package rule 2 3 import ( 4 "fmt" 5 "net/mail" 6 "net/url" 7 "regexp" 8 "strconv" 9 "strings" 10 "time" 11 12 "bosun.org/cmd/bosun/conf" 13 "bosun.org/cmd/bosun/conf/rule/parse" 14 "bosun.org/cmd/bosun/conf/template" 15 eparse "bosun.org/cmd/bosun/expr/parse" 16 "bosun.org/models" 17 "bosun.org/opentsdb" 18 ) 19 20 func (c *Conf) loadTemplate(s *parse.SectionNode) { 21 name := s.Name.Text 22 if _, ok := c.Templates[name]; ok { 23 c.errorf("duplicate template name: %s", name) 24 } 25 t := conf.Template{ 26 Vars: make(map[string]string), 27 Name: name, 28 CustomTemplates: map[string]*template.Template{}, 29 RawCustoms: map[string]string{}, 30 } 31 t.Text = s.RawText 32 t.Locator = newSectionLocator(s) 33 funcs := template.FuncMap{ 34 "V": func(v string) string { 35 return c.Expand(v, t.Vars, false) 36 }, 37 } 38 saw := make(map[string]bool) 39 inherits := []string{} 40 var kvps = map[string]string{} 41 for _, p := range s.Nodes.Nodes { 42 c.at(p) 43 switch p := p.(type) { 44 case *parse.PairNode: 45 c.seen(p.Key.Text, saw) 46 if p.Key.Text == "inherit" { 47 inherits = append(inherits, p.Val.Text) 48 } else { 49 kvps[p.Key.Text] = p.Val.Text 50 } 51 default: 52 c.errorf("unexpected node") 53 } 54 } 55 // expand all inherits first, add to kvps if not present 56 for _, i := range inherits { 57 other, ok := c.Templates[i] 58 if !ok { 59 c.errorf("cannot inherit unknown template %s", i) 60 } 61 if other.RawBody != "" && kvps["body"] == "" { 62 kvps["body"] = other.RawBody 63 } 64 if other.RawSubject != "" && kvps["subject"] == "" { 65 kvps["subject"] = other.RawSubject 66 } 67 for k, v := range other.RawCustoms { 68 if kvps[k] == "" { 69 kvps[k] = v 70 } 71 } 72 } 73 74 // now process like normal 75 for k, v := range kvps { 76 switch k { 77 case "body": 78 t.RawBody = v 79 tmpl, err := c.bodies.New(name).Funcs(funcs).Parse(t.RawBody) 80 if err != nil { 81 c.error(err) 82 } 83 t.Body = tmpl 84 case "subject": 85 t.RawSubject = v 86 tmpl, err := c.subjects.New(name).Funcs(funcs).Parse(t.RawSubject) 87 if err != nil { 88 c.error(err) 89 } 90 t.Subject = tmpl 91 case "inherit": 92 c.errorf("inherit should have been pruned in first pass") 93 default: 94 if strings.HasPrefix(k, "$") { 95 t.Vars[k] = v 96 t.Vars[k[1:]] = t.Vars[k] 97 continue 98 } 99 t.RawCustoms[k] = v 100 ct, ok := c.customTemplates[k] 101 if !ok { 102 ct = template.New(k).Funcs(defaultFuncs) 103 c.customTemplates[k] = ct 104 } 105 tmpl, err := ct.New(name).Funcs(funcs).Parse(v) 106 if err != nil { 107 c.error(err) 108 } 109 t.CustomTemplates[k] = tmpl 110 } 111 } 112 c.at(s) 113 c.Templates[name] = &t 114 } 115 116 func isHTMLTemplate(name string) bool { 117 name = strings.ToLower(name) 118 if name == "emailbody" || strings.HasSuffix(name, "html") { 119 return true 120 } 121 return false 122 } 123 124 var lookupNotificationRE = regexp.MustCompile(`^lookup\("(.*)", "(.*)"\)$`) 125 126 func (c *Conf) loadAlert(s *parse.SectionNode) { 127 name := s.Name.Text 128 if _, ok := c.Alerts[name]; ok { 129 c.errorf("duplicate alert name: %s", name) 130 } 131 a := conf.Alert{ 132 Vars: make(map[string]string), 133 Name: name, 134 CritNotification: new(conf.Notifications), 135 WarnNotification: new(conf.Notifications), 136 AlertTemplateKeys: map[string]*template.Template{}, 137 } 138 a.Text = s.RawText 139 a.Locator = newSectionLocator(s) 140 procNotification := func(v string, ns *conf.Notifications) { 141 if lookup := lookupNotificationRE.FindStringSubmatch(v); lookup != nil { 142 if ns.Lookups == nil { 143 ns.Lookups = make(map[string]*conf.Lookup) 144 } 145 l := c.Lookups[lookup[1]] 146 if l == nil { 147 c.errorf("unknown lookup table %s", lookup[1]) 148 } 149 for _, e := range l.Entries { 150 for k, v := range e.Values { 151 if k != lookup[2] { 152 continue 153 } 154 if _, err := c.parseNotifications(v); err != nil { 155 c.errorf("lookup %s: %v", v, err) 156 } 157 } 158 } 159 ns.Lookups[lookup[2]] = l 160 return 161 } 162 n, err := c.parseNotifications(v) 163 if err != nil { 164 c.error(err) 165 } 166 if ns.Notifications == nil { 167 ns.Notifications = make(map[string]*conf.Notification) 168 } 169 for k, v := range n { 170 ns.Notifications[k] = v 171 } 172 } 173 pairs := c.getPairs(s, a.Vars, sNormal) 174 for _, p := range pairs { 175 c.at(p.node) 176 v := p.val 177 switch p.key { 178 case "template": 179 a.TemplateName = v 180 t, ok := c.Templates[a.TemplateName] 181 if !ok { 182 c.errorf("template not found %s", a.TemplateName) 183 } 184 a.Template = t 185 case "crit": 186 a.Crit = c.NewExpr(v) 187 case "warn": 188 a.Warn = c.NewExpr(v) 189 case "depends": 190 a.Depends = c.NewExpr(v) 191 case "squelch": 192 a.RawSquelch = append(a.RawSquelch, v) 193 if err := a.Squelch.Add(v); err != nil { 194 c.error(err) 195 } 196 case "critNotification": 197 procNotification(v, a.CritNotification) 198 case "warnNotification": 199 procNotification(v, a.WarnNotification) 200 case "unknown": 201 od, err := opentsdb.ParseDuration(v) 202 if err != nil { 203 c.error(err) 204 } 205 d := time.Duration(od) 206 if d < time.Second { 207 c.errorf("unknown duration must be at least 1s") 208 } 209 a.Unknown = d 210 case "maxLogFrequency": 211 od, err := opentsdb.ParseDuration(v) 212 if err != nil { 213 c.error(err) 214 } 215 d := time.Duration(od) 216 if d < time.Second { 217 c.errorf("max log frequency must be at least 1s") 218 } 219 a.MaxLogFrequency = d 220 case "unjoinedOk": 221 a.UnjoinedOK = true 222 case "ignoreUnknown": 223 a.IgnoreUnknown = true 224 case "unknownIsNormal": 225 a.UnknownsNormal = true 226 case "log": 227 a.Log = true 228 case "runEvery": 229 var err error 230 a.RunEvery, err = strconv.Atoi(v) 231 if err != nil { 232 c.error(err) 233 } 234 default: 235 c.errorf("unknown key %s", p.key) 236 } 237 } 238 if a.MaxLogFrequency != 0 && !a.Log { 239 c.errorf("maxLogFrequency can only be used on alerts with `log = true`.") 240 } 241 c.at(s) 242 if a.Crit == nil && a.Warn == nil { 243 c.errorf("neither crit or warn specified") 244 } 245 var tags eparse.Tags 246 var ret models.FuncType 247 if a.Crit != nil { 248 ctags, err := a.Crit.Root.Tags() 249 if err != nil { 250 c.error(err) 251 } 252 tags = ctags 253 ret = a.Crit.Root.Return() 254 } 255 if a.Warn != nil { 256 wtags, err := a.Warn.Root.Tags() 257 if err != nil { 258 c.error(err) 259 } 260 wret := a.Warn.Root.Return() 261 if a.Crit == nil { 262 tags = wtags 263 ret = wret 264 } else if ret != wret { 265 c.errorf("crit and warn expressions must return same type (%v != %v)", ret, wret) 266 } else if !tags.Equal(wtags) { 267 c.errorf("crit tags (%v) and warn tags (%v) must be equal", tags, wtags) 268 } 269 } 270 if a.Depends != nil { 271 depTags, err := a.Depends.Root.Tags() 272 if err != nil { 273 c.error(err) 274 } 275 if len(depTags) != 0 && len(depTags.Intersection(tags)) < 1 { 276 c.errorf("Depends and crit/warn must share at least one tag.") 277 } 278 } 279 allNots := c.getAllPossibleNotifications(&a) 280 if a.Log { 281 for _, n := range allNots { 282 if n.Next != nil { 283 c.errorf("cannot use log with a chained notification") 284 } 285 } 286 if len(allNots) == 0 { 287 c.errorf("log specified but no notification") 288 } 289 } 290 if len(allNots) > 0 && a.Template == nil { 291 c.errorf("notifications specified but no template") 292 } 293 if a.Template != nil { 294 if a.Body == nil || a.Subject == nil { 295 // alert checks for body or subject since some templates might not be directly used in alerts 296 c.errorf("alert templates must have body and subject specified") 297 } 298 // make sure each notification has it's needed template keys present in this alert's template 299 // also build lookup of which template keys need to be rendered at alert time, and which do not 300 checkNotification := func(not *conf.Notification) { 301 checkSingleKey := func(templateKey string, msg string, alertTime bool) { 302 if templateKey == "" || templateKey == "body" || templateKey == "subject" { 303 return 304 } 305 if tmpl := a.Template.CustomTemplates[templateKey]; tmpl != nil { 306 if alertTime { 307 a.AlertTemplateKeys[templateKey] = tmpl 308 } 309 return 310 } 311 errmsg := fmt.Sprintf("notification %s uses template key %s in %s, but template %s does not include it", not.Name, "%s", "%s", a.Template.Name) 312 c.errorf(errmsg, templateKey, msg) 313 } 314 checkTplKeys := func(tks *conf.NotificationTemplateKeys, ctx string, alertTime bool) { 315 checkSingleKey(tks.BodyTemplate, ctx+" body template", alertTime) 316 checkSingleKey(tks.EmailSubjectTemplate, ctx+" email subject", alertTime) 317 checkSingleKey(tks.GetTemplate, ctx+" get url", alertTime) 318 checkSingleKey(tks.PostTemplate, ctx+" post url", alertTime) 319 } 320 checkTplKeys(¬.NotificationTemplateKeys, "alert", true) 321 checkTplKeys(¬.UnknownTemplateKeys, "unknown", false) 322 checkTplKeys(¬.UnknownMultiTemplateKeys, "unknownMulti", false) 323 for at, ntk := range not.ActionTemplateKeys { 324 key := at.String() 325 if at == models.ActionNone { 326 key = "default" 327 } 328 checkTplKeys(ntk, key, false) 329 } 330 } 331 for _, not := range allNots { 332 checkNotification(not) 333 } 334 } 335 a.ReturnType = ret 336 c.Alerts[name] = &a 337 } 338 339 func (c *Conf) loadNotification(s *parse.SectionNode) { 340 name := s.Name.Text 341 if _, ok := c.Notifications[name]; ok { 342 c.errorf("duplicate notification name: %s", name) 343 } 344 n := conf.Notification{ 345 Vars: make(map[string]string), 346 ContentType: "application/x-www-form-urlencoded", 347 Name: name, 348 RunOnActions: "all", 349 ActionTemplateKeys: map[models.ActionType]*conf.NotificationTemplateKeys{}, 350 GroupActions: true, 351 } 352 n.Text = s.RawText 353 n.Locator = newSectionLocator(s) 354 c.Notifications[name] = &n 355 pairs := c.getPairs(s, n.Vars, sNormal) 356 for _, p := range pairs { 357 c.at(p.node) 358 v := p.val 359 switch k := p.key; k { 360 case "email": 361 n.RawEmail = v 362 email, err := mail.ParseAddressList(n.RawEmail) 363 if err != nil { 364 c.error(err) 365 } 366 n.Email = email 367 case "post": 368 n.RawPost = v 369 post, err := url.Parse(n.RawPost) 370 if err != nil { 371 c.error(err) 372 } 373 n.Post = post 374 case "get": 375 n.RawGet = v 376 get, err := url.Parse(n.RawGet) 377 if err != nil { 378 c.error(err) 379 } 380 n.Get = get 381 case "print": 382 n.Print = true 383 case "contentType": 384 n.ContentType = v 385 case "next": 386 n.NextName = v 387 next, ok := c.Notifications[n.NextName] 388 if !ok { 389 c.errorf("unknown notification %s", n.NextName) 390 } 391 n.Next = next 392 case "timeout": 393 d, err := opentsdb.ParseDuration(v) 394 if err != nil { 395 c.error(err) 396 } 397 n.Timeout = time.Duration(d) 398 399 case "bodyTemplate": 400 n.BodyTemplate = v 401 case "getTemplate": 402 n.GetTemplate = v 403 case "postTemplate": 404 n.PostTemplate = v 405 case "emailSubjectTemplate": 406 n.EmailSubjectTemplate = v 407 case "runOnActions": 408 // todo: validate all/true, none/false, or comma seperated action shortNames 409 n.RunOnActions = v 410 case "groupActions": 411 if v == "false" { 412 n.GroupActions = false 413 } else if v == "true" { 414 n.GroupActions = true 415 } else { 416 c.errorf("invalid boolean value %s", v) 417 } 418 case "unknownMinGroupSize": 419 i, err := strconv.Atoi(v) 420 if err != nil { 421 c.error(err) 422 } 423 n.UnknownMinGroupSize = &i 424 case "unknownThreshold": 425 i, err := strconv.Atoi(v) 426 if err != nil { 427 c.error(err) 428 } 429 n.UnknownThreshold = &i 430 default: 431 // all special template keys are handled in one loop 432 // the following formats are possible: 433 // action(templateKey)(ActionType})? //action 434 // unknown(TemplateKey) //unknown 435 // unknownMulti(TemplateKey) //unknown 436 var keys *conf.NotificationTemplateKeys 437 keyType := k 438 if strings.HasPrefix(k, "action") { 439 keyType = strings.TrimPrefix(k, "action") 440 at := models.ActionNone 441 // look for and trim suffix if there 442 // trim the templateKey and explicitly match actiontype 443 for s, t := range models.ActionShortNames { 444 for _, e := range []string{"Body", "Get", "EmailSubject"} { 445 if strings.Compare(strings.TrimPrefix(keyType, e), s) == 0 { 446 at = t 447 keyType = keyType[:len(keyType)-len(s)] 448 break 449 } 450 } 451 } 452 if n.ActionTemplateKeys[at] == nil { 453 n.ActionTemplateKeys[at] = &conf.NotificationTemplateKeys{} 454 } 455 keys = n.ActionTemplateKeys[at] 456 } else if strings.HasPrefix(k, "unknownMulti") { 457 keys = &n.UnknownMultiTemplateKeys 458 keyType = strings.TrimPrefix(k, "unknownMulti") 459 } else if strings.HasPrefix(k, "unknown") { 460 keys = &n.UnknownTemplateKeys 461 keyType = strings.TrimPrefix(k, "unknown") 462 } else { 463 c.errorf("unknown key %s", k) 464 } 465 switch keyType { 466 case "Body": 467 keys.BodyTemplate = v 468 case "Get": 469 keys.GetTemplate = v 470 case "Post": 471 keys.PostTemplate = v 472 case "EmailSubject": 473 keys.EmailSubjectTemplate = v 474 default: 475 c.errorf("unknown key %s", k) 476 } 477 break 478 479 } 480 } 481 // TODO: make sure get/getTemplate and post/postTemplate are mutually exclusive 482 c.at(s) 483 if n.Timeout > 0 && n.Next == nil { 484 c.errorf("timeout specified without next") 485 } 486 }