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(&not.NotificationTemplateKeys, "alert", true)
   321  			checkTplKeys(&not.UnknownTemplateKeys, "unknown", false)
   322  			checkTplKeys(&not.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  }