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  }