...

Source file src/github.com/prometheus/alertmanager/template/template.go

Documentation: github.com/prometheus/alertmanager/template

     1  // Copyright 2015 Prometheus Team
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package template
    15  
    16  import (
    17  	"bytes"
    18  	tmplhtml "html/template"
    19  	"io"
    20  	"net/url"
    21  	"path"
    22  	"path/filepath"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  	tmpltext "text/template"
    27  	"time"
    28  
    29  	"github.com/prometheus/common/model"
    30  	"golang.org/x/text/cases"
    31  	"golang.org/x/text/language"
    32  
    33  	"github.com/prometheus/alertmanager/asset"
    34  	"github.com/prometheus/alertmanager/types"
    35  )
    36  
    37  // Template bundles a text and a html template instance.
    38  type Template struct {
    39  	text *tmpltext.Template
    40  	html *tmplhtml.Template
    41  
    42  	ExternalURL *url.URL
    43  }
    44  
    45  // FromGlobs calls ParseGlob on all path globs provided and returns the
    46  // resulting Template.
    47  func FromGlobs(paths ...string) (*Template, error) {
    48  	t := &Template{
    49  		text: tmpltext.New("").Option("missingkey=zero"),
    50  		html: tmplhtml.New("").Option("missingkey=zero"),
    51  	}
    52  
    53  	t.text = t.text.Funcs(tmpltext.FuncMap(DefaultFuncs))
    54  	t.html = t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs))
    55  
    56  	defaultTemplates := []string{"default.tmpl", "email.tmpl"}
    57  
    58  	for _, file := range defaultTemplates {
    59  		f, err := asset.Assets.Open(path.Join("/templates", file))
    60  		if err != nil {
    61  			return nil, err
    62  		}
    63  		defer f.Close()
    64  		b, err := io.ReadAll(f)
    65  		if err != nil {
    66  			return nil, err
    67  		}
    68  		if t.text, err = t.text.Parse(string(b)); err != nil {
    69  			return nil, err
    70  		}
    71  		if t.html, err = t.html.Parse(string(b)); err != nil {
    72  			return nil, err
    73  		}
    74  
    75  	}
    76  
    77  	for _, tp := range paths {
    78  		// ParseGlob in the template packages errors if not at least one file is
    79  		// matched. We want to allow empty matches that may be populated later on.
    80  		p, err := filepath.Glob(tp)
    81  		if err != nil {
    82  			return nil, err
    83  		}
    84  		if len(p) > 0 {
    85  			if t.text, err = t.text.ParseGlob(tp); err != nil {
    86  				return nil, err
    87  			}
    88  			if t.html, err = t.html.ParseGlob(tp); err != nil {
    89  				return nil, err
    90  			}
    91  		}
    92  	}
    93  	return t, nil
    94  }
    95  
    96  // ExecuteTextString needs a meaningful doc comment (TODO(fabxc)).
    97  func (t *Template) ExecuteTextString(text string, data interface{}) (string, error) {
    98  	if text == "" {
    99  		return "", nil
   100  	}
   101  	tmpl, err := t.text.Clone()
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  	tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text)
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  	var buf bytes.Buffer
   110  	err = tmpl.Execute(&buf, data)
   111  	return buf.String(), err
   112  }
   113  
   114  // ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)).
   115  func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, error) {
   116  	if html == "" {
   117  		return "", nil
   118  	}
   119  	tmpl, err := t.html.Clone()
   120  	if err != nil {
   121  		return "", err
   122  	}
   123  	tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(html)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  	var buf bytes.Buffer
   128  	err = tmpl.Execute(&buf, data)
   129  	return buf.String(), err
   130  }
   131  
   132  type FuncMap map[string]interface{}
   133  
   134  var DefaultFuncs = FuncMap{
   135  	"toUpper": strings.ToUpper,
   136  	"toLower": strings.ToLower,
   137  	"title":   cases.Title(language.AmericanEnglish).String,
   138  	// join is equal to strings.Join but inverts the argument order
   139  	// for easier pipelining in templates.
   140  	"join": func(sep string, s []string) string {
   141  		return strings.Join(s, sep)
   142  	},
   143  	"match": regexp.MatchString,
   144  	"safeHtml": func(text string) tmplhtml.HTML {
   145  		return tmplhtml.HTML(text)
   146  	},
   147  	"reReplaceAll": func(pattern, repl, text string) string {
   148  		re := regexp.MustCompile(pattern)
   149  		return re.ReplaceAllString(text, repl)
   150  	},
   151  	"stringSlice": func(s ...string) []string {
   152  		return s
   153  	},
   154  }
   155  
   156  // Pair is a key/value string pair.
   157  type Pair struct {
   158  	Name, Value string
   159  }
   160  
   161  // Pairs is a list of key/value string pairs.
   162  type Pairs []Pair
   163  
   164  // Names returns a list of names of the pairs.
   165  func (ps Pairs) Names() []string {
   166  	ns := make([]string, 0, len(ps))
   167  	for _, p := range ps {
   168  		ns = append(ns, p.Name)
   169  	}
   170  	return ns
   171  }
   172  
   173  // Values returns a list of values of the pairs.
   174  func (ps Pairs) Values() []string {
   175  	vs := make([]string, 0, len(ps))
   176  	for _, p := range ps {
   177  		vs = append(vs, p.Value)
   178  	}
   179  	return vs
   180  }
   181  
   182  // KV is a set of key/value string pairs.
   183  type KV map[string]string
   184  
   185  // SortedPairs returns a sorted list of key/value pairs.
   186  func (kv KV) SortedPairs() Pairs {
   187  	var (
   188  		pairs     = make([]Pair, 0, len(kv))
   189  		keys      = make([]string, 0, len(kv))
   190  		sortStart = 0
   191  	)
   192  	for k := range kv {
   193  		if k == string(model.AlertNameLabel) {
   194  			keys = append([]string{k}, keys...)
   195  			sortStart = 1
   196  		} else {
   197  			keys = append(keys, k)
   198  		}
   199  	}
   200  	sort.Strings(keys[sortStart:])
   201  
   202  	for _, k := range keys {
   203  		pairs = append(pairs, Pair{k, kv[k]})
   204  	}
   205  	return pairs
   206  }
   207  
   208  // Remove returns a copy of the key/value set without the given keys.
   209  func (kv KV) Remove(keys []string) KV {
   210  	keySet := make(map[string]struct{}, len(keys))
   211  	for _, k := range keys {
   212  		keySet[k] = struct{}{}
   213  	}
   214  
   215  	res := KV{}
   216  	for k, v := range kv {
   217  		if _, ok := keySet[k]; !ok {
   218  			res[k] = v
   219  		}
   220  	}
   221  	return res
   222  }
   223  
   224  // Names returns the names of the label names in the LabelSet.
   225  func (kv KV) Names() []string {
   226  	return kv.SortedPairs().Names()
   227  }
   228  
   229  // Values returns a list of the values in the LabelSet.
   230  func (kv KV) Values() []string {
   231  	return kv.SortedPairs().Values()
   232  }
   233  
   234  // Data is the data passed to notification templates and webhook pushes.
   235  //
   236  // End-users should not be exposed to Go's type system, as this will confuse them and prevent
   237  // simple things like simple equality checks to fail. Map everything to float64/string.
   238  type Data struct {
   239  	Receiver string `json:"receiver"`
   240  	Status   string `json:"status"`
   241  	Alerts   Alerts `json:"alerts"`
   242  
   243  	GroupLabels       KV `json:"groupLabels"`
   244  	CommonLabels      KV `json:"commonLabels"`
   245  	CommonAnnotations KV `json:"commonAnnotations"`
   246  
   247  	ExternalURL string `json:"externalURL"`
   248  }
   249  
   250  // Alert holds one alert for notification templates.
   251  type Alert struct {
   252  	Status       string    `json:"status"`
   253  	Labels       KV        `json:"labels"`
   254  	Annotations  KV        `json:"annotations"`
   255  	StartsAt     time.Time `json:"startsAt"`
   256  	EndsAt       time.Time `json:"endsAt"`
   257  	GeneratorURL string    `json:"generatorURL"`
   258  	Fingerprint  string    `json:"fingerprint"`
   259  }
   260  
   261  // Alerts is a list of Alert objects.
   262  type Alerts []Alert
   263  
   264  // Firing returns the subset of alerts that are firing.
   265  func (as Alerts) Firing() []Alert {
   266  	res := []Alert{}
   267  	for _, a := range as {
   268  		if a.Status == string(model.AlertFiring) {
   269  			res = append(res, a)
   270  		}
   271  	}
   272  	return res
   273  }
   274  
   275  // Resolved returns the subset of alerts that are resolved.
   276  func (as Alerts) Resolved() []Alert {
   277  	res := []Alert{}
   278  	for _, a := range as {
   279  		if a.Status == string(model.AlertResolved) {
   280  			res = append(res, a)
   281  		}
   282  	}
   283  	return res
   284  }
   285  
   286  // Data assembles data for template expansion.
   287  func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data {
   288  	data := &Data{
   289  		Receiver:          regexp.QuoteMeta(recv),
   290  		Status:            string(types.Alerts(alerts...).Status()),
   291  		Alerts:            make(Alerts, 0, len(alerts)),
   292  		GroupLabels:       KV{},
   293  		CommonLabels:      KV{},
   294  		CommonAnnotations: KV{},
   295  		ExternalURL:       t.ExternalURL.String(),
   296  	}
   297  
   298  	// The call to types.Alert is necessary to correctly resolve the internal
   299  	// representation to the user representation.
   300  	for _, a := range types.Alerts(alerts...) {
   301  		alert := Alert{
   302  			Status:       string(a.Status()),
   303  			Labels:       make(KV, len(a.Labels)),
   304  			Annotations:  make(KV, len(a.Annotations)),
   305  			StartsAt:     a.StartsAt,
   306  			EndsAt:       a.EndsAt,
   307  			GeneratorURL: a.GeneratorURL,
   308  			Fingerprint:  a.Fingerprint().String(),
   309  		}
   310  		for k, v := range a.Labels {
   311  			alert.Labels[string(k)] = string(v)
   312  		}
   313  		for k, v := range a.Annotations {
   314  			alert.Annotations[string(k)] = string(v)
   315  		}
   316  		data.Alerts = append(data.Alerts, alert)
   317  	}
   318  
   319  	for k, v := range groupLabels {
   320  		data.GroupLabels[string(k)] = string(v)
   321  	}
   322  
   323  	if len(alerts) >= 1 {
   324  		var (
   325  			commonLabels      = alerts[0].Labels.Clone()
   326  			commonAnnotations = alerts[0].Annotations.Clone()
   327  		)
   328  		for _, a := range alerts[1:] {
   329  			if len(commonLabels) == 0 && len(commonAnnotations) == 0 {
   330  				break
   331  			}
   332  			for ln, lv := range commonLabels {
   333  				if a.Labels[ln] != lv {
   334  					delete(commonLabels, ln)
   335  				}
   336  			}
   337  			for an, av := range commonAnnotations {
   338  				if a.Annotations[an] != av {
   339  					delete(commonAnnotations, an)
   340  				}
   341  			}
   342  		}
   343  		for k, v := range commonLabels {
   344  			data.CommonLabels[string(k)] = string(v)
   345  		}
   346  		for k, v := range commonAnnotations {
   347  			data.CommonAnnotations[string(k)] = string(v)
   348  		}
   349  	}
   350  
   351  	return data
   352  }
   353  

View as plain text