bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/sched/template.go (about)

     1  package sched
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	htemplate "html/template"
     9  	"io"
    10  	"io/ioutil"
    11  	"math"
    12  	"net/http"
    13  	"net/url"
    14  	"strings"
    15  	"time"
    16  
    17  	"bosun.org/collect"
    18  
    19  	"bosun.org/cmd/bosun/conf"
    20  	"bosun.org/cmd/bosun/conf/template"
    21  	"bosun.org/cmd/bosun/expr"
    22  	"bosun.org/cmd/bosun/sched/slack"
    23  	"bosun.org/models"
    24  	"bosun.org/opentsdb"
    25  	"bosun.org/slog"
    26  
    27  	"github.com/jmoiron/jsonq"
    28  )
    29  
    30  type Context struct {
    31  	*models.IncidentState
    32  	Alert   *conf.Alert
    33  	IsEmail bool
    34  	Errors  []string
    35  
    36  	schedule    *Schedule
    37  	runHistory  *RunHistory
    38  	Attachments []*models.Attachment
    39  	ElasticHost string
    40  
    41  	vars map[string]interface{}
    42  }
    43  
    44  func (s *Schedule) Data(rh *RunHistory, st *models.IncidentState, a *conf.Alert, isEmail bool) *Context {
    45  	c := Context{
    46  		IncidentState: st,
    47  		Alert:         a,
    48  		IsEmail:       isEmail,
    49  		schedule:      s,
    50  		runHistory:    rh,
    51  		ElasticHost:   "default",
    52  		vars:          map[string]interface{}{},
    53  	}
    54  	return &c
    55  }
    56  
    57  func (c *Context) Set(name string, value interface{}) string {
    58  	c.vars[name] = value
    59  	return "" // have to return something
    60  }
    61  
    62  func (c *Context) Get(name string) interface{} {
    63  	return c.vars[name]
    64  }
    65  
    66  // Note: All Context methods that can return nil must return literal nils
    67  // and not typed nils when returning errors to ensure that our global template
    68  // function notNil behaves correctly. Context Functions that return an object
    69  // that users can dereference return nils on errors. Ones that return images or
    70  // string just return the error message.
    71  
    72  // Ack returns the URL to acknowledge an alert.
    73  func (c *Context) Ack() string {
    74  	return c.schedule.SystemConf.MakeLink("/action", &url.Values{
    75  		"type": []string{"ack"},
    76  		"key":  []string{c.Alert.Name + c.AlertKey.Group().String()},
    77  	})
    78  }
    79  
    80  // HostView returns the URL to the host view page.
    81  func (c *Context) HostView(host string) string {
    82  	return c.schedule.SystemConf.MakeLink("/host", &url.Values{
    83  		"time": []string{"1d-ago"},
    84  		"host": []string{host},
    85  	})
    86  }
    87  
    88  // Hack so template can read IncidentId off of event.
    89  func (c *Context) Last() interface{} {
    90  	return struct {
    91  		models.Event
    92  		IncidentId int64
    93  	}{c.IncidentState.Last(), c.Id}
    94  }
    95  
    96  // GetIncidentState returns an IncidentState so users can
    97  // include information about previous or other Incidents in alert notifications
    98  func (c *Context) GetIncidentState(id int64) *models.IncidentState {
    99  	is, err := c.schedule.DataAccess.State().GetIncidentState(id)
   100  	if err != nil {
   101  		c.addError(err)
   102  		return nil
   103  	}
   104  	return is
   105  }
   106  
   107  // Expr takes an expression in the form of a string, changes the tags to
   108  // match the context of the alert, and returns a link to the expression page.
   109  func (c *Context) Expr(v string) string {
   110  	p := url.Values{}
   111  	p.Add("date", c.runHistory.Start.Format(`2006-01-02`))
   112  	p.Add("time", c.runHistory.Start.Format(`15:04:05`))
   113  	p.Add("expr", base64.StdEncoding.EncodeToString([]byte(opentsdb.ReplaceTags(v, c.AlertKey.Group()))))
   114  	return c.schedule.SystemConf.MakeLink("/expr", &p)
   115  }
   116  
   117  // GraphLink takes an expression in the form of a string, and returns a link to
   118  // the expression page's graph tab with the time set.
   119  func (c *Context) GraphLink(v string) string {
   120  	p := url.Values{}
   121  	p.Add("expr", base64.StdEncoding.EncodeToString([]byte(v)))
   122  	p.Add("tab", "graph")
   123  	p.Add("date", c.runHistory.Start.Format(`2006-01-02`))
   124  	p.Add("time", c.runHistory.Start.Format(`15:04:05`))
   125  	return c.schedule.SystemConf.MakeLink("/expr", &p)
   126  }
   127  
   128  // Shorten uses Bosun's url shortner service to create a shortlink for
   129  // the given url
   130  func (c *Context) Shorten(link string) string {
   131  	id, err := c.schedule.DataAccess.Configs().ShortenLink(link)
   132  	if err != nil {
   133  		c.addError(err)
   134  		return ""
   135  	}
   136  	return c.schedule.SystemConf.MakeLink(fmt.Sprintf("/s/%d", id), nil)
   137  }
   138  
   139  func (c *Context) Rule() string {
   140  	p := url.Values{}
   141  	time := c.runHistory.Start
   142  	p.Add("alert", c.Alert.Name)
   143  	p.Add("fromDate", time.Format("2006-01-02"))
   144  	p.Add("fromTime", time.Format("15:04"))
   145  	p.Add("template_group", c.Tags)
   146  	return c.schedule.SystemConf.MakeLink("/config", &p)
   147  }
   148  
   149  func (c *Context) Incident() string {
   150  	return c.schedule.SystemConf.MakeLink("/incident", &url.Values{
   151  		"id": []string{fmt.Sprint(c.Id)},
   152  	})
   153  }
   154  
   155  func (c *Context) UseElastic(host string) interface{} {
   156  	c.ElasticHost = host
   157  	return nil
   158  }
   159  
   160  func (s *Schedule) ExecuteBody(rh *RunHistory, a *conf.Alert, st *models.IncidentState, isEmail bool) (string, []*models.Attachment, error) {
   161  	t := a.Template
   162  	if t == nil {
   163  		return "", nil, nil
   164  	}
   165  	tp := t.Body
   166  	if isEmail && t.CustomTemplates["emailBody"] != nil {
   167  		tp = t.CustomTemplates["emailBody"]
   168  	}
   169  	if tp == nil {
   170  		return "", nil, nil
   171  	}
   172  	c := s.Data(rh, st, a, isEmail)
   173  	return s.executeTpl(tp, c)
   174  }
   175  
   176  func (s *Schedule) executeTpl(t *template.Template, c *Context) (string, []*models.Attachment, error) {
   177  	buf := new(bytes.Buffer)
   178  	if err := t.Execute(buf, c); err != nil {
   179  		return "", nil, err
   180  	}
   181  	return buf.String(), c.Attachments, nil
   182  }
   183  
   184  func (s *Schedule) ExecuteSubject(rh *RunHistory, a *conf.Alert, st *models.IncidentState, isEmail bool) (string, error) {
   185  	t := a.Template
   186  	if t == nil {
   187  		return "", nil
   188  	}
   189  	tp := t.Subject
   190  	if isEmail && t.CustomTemplates["emailSubject"] != nil {
   191  		tp = t.CustomTemplates["emailSubject"]
   192  	}
   193  	if tp == nil {
   194  		return "", nil
   195  	}
   196  	c := s.Data(rh, st, a, isEmail)
   197  	d, _, err := s.executeTpl(tp, c)
   198  	if err != nil {
   199  		return "", err
   200  	}
   201  	// remove extra whitespace
   202  	d = strings.Join(strings.Fields(d), " ")
   203  	return d, nil
   204  }
   205  
   206  func (s *Schedule) ExecuteAll(rh *RunHistory, a *conf.Alert, st *models.IncidentState, recordTimes bool) (*models.RenderedTemplates, []error) {
   207  	ctx := func() *Context { return s.Data(rh, st, a, false) }
   208  	var errs []error
   209  	var timer func()
   210  	var category string
   211  	e := func(err error) {
   212  		if timer != nil {
   213  			timer()
   214  		}
   215  		if err != nil {
   216  			errs = append(errs, fmt.Errorf("%s: %s", category, err))
   217  		}
   218  	}
   219  	t := a.Template
   220  	rt := &models.RenderedTemplates{}
   221  
   222  	if t == nil {
   223  		return rt, nil
   224  	}
   225  	var err error
   226  
   227  	start := func(t string) func() {
   228  		category = t
   229  		if !recordTimes {
   230  			return nil
   231  		}
   232  		return collect.StartTimer("template.render", opentsdb.TagSet{"alert": a.Name, "type": t})
   233  	}
   234  
   235  	// subject
   236  	timer = start("subject")
   237  	subject, err := s.ExecuteSubject(rh, a, st, false)
   238  	e(err)
   239  	st.Subject = subject
   240  	rt.Subject = subject
   241  	// body
   242  	timer = start("body")
   243  	body, atts, err := s.ExecuteBody(rh, a, st, false)
   244  	e(err)
   245  	rt.Body = body
   246  	rt.Attachments = atts
   247  
   248  	timer = start("emailsubject")
   249  	emailSubject, err := s.ExecuteSubject(rh, a, st, true)
   250  	e(err)
   251  	rt.EmailSubject = []byte(emailSubject)
   252  
   253  	timer = start("emailbody")
   254  	emailBody, atts, err := s.ExecuteBody(rh, a, st, true)
   255  	e(err)
   256  	rt.EmailBody = []byte(emailBody)
   257  	rt.Attachments = atts
   258  
   259  	rt.Custom = map[string]string{}
   260  	for k, v := range a.AlertTemplateKeys {
   261  		// emailsubject/body get handled specially above
   262  		if k == "emailBody" || k == "emailSubject" || k == "body" || k == "subject" {
   263  			continue
   264  		}
   265  		c := ctx()
   266  		timer = start(k)
   267  		rendered, _, err := s.executeTpl(v, c)
   268  		e(err)
   269  		rt.Custom[k] = rendered
   270  	}
   271  	return rt, errs
   272  }
   273  
   274  var error_body = template.Must(template.New("body_error_template").Parse(`
   275  	<p>There was a runtime error processing alert {{.State.AlertKey}} using the {{.Alert.Template.Name}} template. The following errors occurred:</p>
   276  	<ul>
   277  	{{range .Errors}}
   278  		<li>{{.}}</li>
   279  	{{end}}
   280  	</ul>
   281  	<p>Use <a href="{{.Rule}}">this link</a> to the rule page to correct this.</p>
   282  	<h2>Generic Alert Information</h2>
   283  	<p>Status: {{.Last.Status}}</p>
   284  	<p>Alert: {{.State.AlertKey}}</p>
   285  	<h3>Computations</h3>
   286  	<table>
   287  		<tr>
   288  			<th style="text-align:left">Expression</th>
   289  			<th style="text-align:left">Value</th>
   290  		</tr>
   291  	{{range .Computations}}
   292  		<tr>
   293  			<td style="text-align:left">{{.Text}}</td>
   294  			<td style="text-align:left">{{.Value}}</td>
   295  		</tr>
   296  	{{end}}</table>`))
   297  
   298  func (s *Schedule) ExecuteBadTemplate(errs []error, rh *RunHistory, a *conf.Alert, st *models.IncidentState) (subject, body string, err error) {
   299  	sub := fmt.Sprintf("error: template rendering error for alert %v", st.AlertKey)
   300  	c := struct {
   301  		Errors []error
   302  		*Context
   303  	}{
   304  		Errors:  errs,
   305  		Context: s.Data(rh, st, a, true),
   306  	}
   307  	buf := new(bytes.Buffer)
   308  	error_body.Execute(buf, c)
   309  	return sub, buf.String(), nil
   310  }
   311  
   312  func (c *Context) evalExpr(e *expr.Expr, filter bool, series bool, autods int) (expr.ResultSlice, string, error) {
   313  	var err error
   314  	if filter {
   315  		e, err = expr.New(opentsdb.ReplaceTags(e.Text, c.AlertKey.Group()), c.schedule.RuleConf.GetFuncs(c.schedule.SystemConf.EnabledBackends()))
   316  		if err != nil {
   317  			return nil, "", err
   318  		}
   319  	}
   320  	if series && e.Root.Return() != models.TypeSeriesSet {
   321  		return nil, "", fmt.Errorf("need a series, got %T (%v)", e, e)
   322  	}
   323  	providers := &expr.BosunProviders{
   324  		Cache:     c.runHistory.Cache,
   325  		Search:    c.schedule.Search,
   326  		Squelched: c.schedule.RuleConf.AlertSquelched(c.Alert),
   327  		History:   c.schedule,
   328  	}
   329  	origin := fmt.Sprintf("Template: Alert Key: %v", c.AlertKey)
   330  	res, _, err := e.Execute(c.runHistory.Backends, providers, nil, c.runHistory.Start, autods, c.Alert.UnjoinedOK, origin)
   331  	if err != nil {
   332  		return nil, "", fmt.Errorf("%s: %v", e, err)
   333  	}
   334  	return res.Results, e.String(), nil
   335  }
   336  
   337  // eval takes an expression or string (which it turns into an expression), executes it and returns the result.
   338  // It can also takes a ResultSlice so callers can transparantly handle different inputs.
   339  // The filter argument constrains the result to matching tags in the current context.
   340  // The series argument asserts that the result is a time series.
   341  func (c *Context) eval(v interface{}, filter bool, series bool, autods int) (res expr.ResultSlice, title string, err error) {
   342  	switch v := v.(type) {
   343  	case string:
   344  		var e *expr.Expr
   345  		e, err = expr.New(v, c.schedule.RuleConf.GetFuncs(c.schedule.SystemConf.EnabledBackends()))
   346  		if err != nil {
   347  			return nil, "", fmt.Errorf("%s: %v", v, err)
   348  		}
   349  		res, title, err = c.evalExpr(e, filter, series, autods)
   350  		if err != nil {
   351  			return
   352  		}
   353  	case *expr.Expr:
   354  		res, title, err = c.evalExpr(v, filter, series, autods)
   355  		if err != nil {
   356  			return
   357  		}
   358  	case expr.ResultSlice:
   359  		res = v
   360  	default:
   361  		return nil, "", fmt.Errorf("expected string, expression or resultslice, got %T (%v)", v, v)
   362  	}
   363  	if filter {
   364  		res = res.Filter(c.AlertKey.Group())
   365  	}
   366  	if series {
   367  		for _, k := range res {
   368  			if k.Type() != models.TypeSeriesSet {
   369  				return nil, "", fmt.Errorf("need a series, got %v (%v)", k.Type(), k)
   370  			}
   371  		}
   372  	}
   373  	return res, title, err
   374  }
   375  
   376  // Lookup returns the value for a key in the lookup table for the context's tagset.
   377  // the returned string may be the representation of an error
   378  func (c *Context) Lookup(table, key string) string {
   379  	return c.LookupAll(table, key, c.AlertKey.Group())
   380  }
   381  
   382  func (c *Context) LookupAll(table, key string, group interface{}) string {
   383  	var t opentsdb.TagSet
   384  	switch v := group.(type) {
   385  	case string:
   386  		var err error
   387  		t, err = opentsdb.ParseTags(v)
   388  		if err != nil {
   389  			c.addError(err)
   390  			return err.Error()
   391  		}
   392  	case opentsdb.TagSet:
   393  		t = v
   394  	}
   395  	l := c.schedule.RuleConf.GetLookup(table)
   396  	if l == nil {
   397  		err := fmt.Errorf("unknown lookup table %v", table)
   398  		c.addError(err)
   399  		return err.Error()
   400  	}
   401  	if v, ok := l.ToExpr().Get(key, t); ok {
   402  		return v
   403  	}
   404  	err := fmt.Errorf("no entry for key %v in table %v for tagset %v", key, table, c.AlertKey.Group())
   405  	c.addError(err)
   406  	return err.Error()
   407  }
   408  
   409  func (c *Context) addError(e error) {
   410  	c.Errors = append(c.Errors, e.Error())
   411  }
   412  
   413  // LastError gets the most recent error string for the context's
   414  // Error slice or returns an empty string if the error slice is
   415  // empty
   416  func (c *Context) LastError() string {
   417  	if len(c.Errors) > 0 {
   418  		return c.Errors[len(c.Errors)-1]
   419  	}
   420  	return ""
   421  }
   422  
   423  // Eval takes a result or an expression which it evaluates to a result.
   424  // It returns a value with tags corresponding to the context's tags.
   425  // If no such result is found, the first result with
   426  // nil tags is returned. If no such result is found, nil is returned.
   427  func (c *Context) Eval(v interface{}) interface{} {
   428  	res, _, err := c.eval(v, true, false, 0)
   429  	if err != nil {
   430  		c.addError(err)
   431  		return nil
   432  	}
   433  	if len(res) == 0 {
   434  		return math.NaN()
   435  	}
   436  	// TODO: don't choose a random result, make sure there's exactly 1
   437  	return res[0].Value
   438  }
   439  
   440  // EvalAll returns the executed expression (or the given result as is).
   441  func (c *Context) EvalAll(v interface{}) interface{} {
   442  	res, _, err := c.eval(v, false, false, 0)
   443  	if err != nil {
   444  		c.addError(err)
   445  		return nil
   446  	}
   447  	return res
   448  }
   449  
   450  func (c *Context) graph(v interface{}, unit string, filter bool) (val interface{}) {
   451  	defer func() {
   452  		if p := recover(); p != nil {
   453  			err := fmt.Errorf("panic rendering graph %v", p)
   454  			c.addError(err)
   455  			slog.Error(err)
   456  			val = err.Error()
   457  		}
   458  	}()
   459  	res, exprText, err := c.eval(v, filter, true, 1000)
   460  	if err != nil {
   461  		c.addError(err)
   462  		return err.Error()
   463  	}
   464  	var buf bytes.Buffer
   465  	const width = 800
   466  	const height = 600
   467  	footerHTML := fmt.Sprintf(`<p><small>Query: %s<br>Time: %s</small></p>`,
   468  		htemplate.HTMLEscapeString(exprText),
   469  		c.runHistory.Start.Format(time.RFC3339))
   470  	if c.IsEmail {
   471  		err := c.schedule.ExprPNG(nil, &buf, width, height, unit, res)
   472  		if err != nil {
   473  			c.addError(err)
   474  			return err.Error()
   475  		}
   476  		name := fmt.Sprintf("%d.png", len(c.Attachments)+1)
   477  		c.Attachments = append(c.Attachments, &models.Attachment{
   478  			Data:        buf.Bytes(),
   479  			Filename:    name,
   480  			ContentType: "image/png",
   481  		})
   482  		return htemplate.HTML(fmt.Sprintf(`<a href="%s" style="text-decoration: none"><img alt="%s" src="cid:%s" /></a>%s`,
   483  			c.GraphLink(exprText),
   484  			htemplate.HTMLEscapeString(fmt.Sprint(v)),
   485  			name,
   486  			footerHTML,
   487  		))
   488  	}
   489  	buf.WriteString(fmt.Sprintf(`<a href="%s" style="text-decoration: none">`, c.GraphLink(exprText)))
   490  	if err := c.schedule.ExprSVG(nil, &buf, width, height, unit, res); err != nil {
   491  		c.addError(err)
   492  		return err.Error()
   493  	}
   494  	buf.WriteString(`</a>`)
   495  	buf.WriteString(footerHTML)
   496  	return htemplate.HTML(buf.String())
   497  }
   498  
   499  // Graph returns an SVG for the given result (or expression, for which it gets the result)
   500  // with same tags as the context's tags.
   501  func (c *Context) Graph(v interface{}, args ...string) interface{} {
   502  	var unit string
   503  	if len(args) > 0 {
   504  		unit = args[0]
   505  	}
   506  	return c.graph(v, unit, true)
   507  }
   508  
   509  // GraphAll returns an SVG for the given result (or expression, for which it gets the result).
   510  func (c *Context) GraphAll(v interface{}, args ...string) interface{} {
   511  	var unit string
   512  	if len(args) > 0 {
   513  		unit = args[0]
   514  	}
   515  	return c.graph(v, unit, false)
   516  }
   517  
   518  // GetMeta fetches either metric metadata (if a metric name is provided)
   519  // or metadata about a tagset key by name
   520  func (c *Context) GetMeta(metric, name string, v interface{}) interface{} {
   521  	var t opentsdb.TagSet
   522  	switch v := v.(type) {
   523  	case string:
   524  		if v == "" {
   525  			t = make(opentsdb.TagSet)
   526  		} else {
   527  			var err error
   528  			t, err = opentsdb.ParseTags(v)
   529  			if err != nil {
   530  				c.addError(err)
   531  				return nil
   532  			}
   533  		}
   534  	case opentsdb.TagSet:
   535  		t = v
   536  	}
   537  	meta, err := c.schedule.GetMetadata(metric, t)
   538  	if err != nil && name == "" {
   539  		c.addError(err)
   540  		return nil
   541  	}
   542  	if err != nil {
   543  		return err.Error()
   544  	}
   545  	if name == "" {
   546  		return meta
   547  	}
   548  	for _, m := range meta {
   549  		if m.Name == name {
   550  			return m.Value
   551  		}
   552  	}
   553  	return "metadata not found"
   554  }
   555  
   556  // LeftJoin takes slices of results and expressions for which it gets the slices of results.
   557  // Then it joins the 2nd and higher slice of results onto the first slice of results.
   558  // Joining is performed by group: a group that includes all tags (with same values) of the first group is a match.
   559  func (c *Context) LeftJoin(v ...interface{}) (interface{}, error) {
   560  	if len(v) < 2 {
   561  		// A template error is thrown here since this should be caught when defining at testing the template
   562  		return nil, fmt.Errorf("need at least two values (each can be an expression or result slice), got %v", len(v))
   563  	}
   564  	// temporarily store the results in a results[M][Ni] Result matrix:
   565  	// for M queries, tracks Ni results per each i'th query
   566  	results := make([][]*expr.Result, len(v))
   567  	for col, val := range v {
   568  		queryResults, _, err := c.eval(val, false, false, 0)
   569  		if err != nil {
   570  			c.addError(err)
   571  			return nil, nil
   572  		}
   573  		results[col] = queryResults
   574  	}
   575  
   576  	// perform the joining by storing all results in a joined[N0][M] Result matrix:
   577  	// for N tagsets (based on first query results), tracks all M Results (results with matching group, from all other queries)
   578  	joined := make([][]*expr.Result, 0)
   579  	for row, firstQueryResult := range results[0] {
   580  		joined = append(joined, make([]*expr.Result, len(v)))
   581  		joined[row][0] = firstQueryResult
   582  		// join results of 2nd to M queries
   583  		for col, queryResults := range results[1:] {
   584  			for _, laterQueryResult := range queryResults {
   585  				if firstQueryResult.Group.Subset(laterQueryResult.Group) {
   586  					joined[row][col+1] = laterQueryResult
   587  					break
   588  				}
   589  				// Fill emtpy cells with NaN Value, so calling .Value is not a nil pointer dereference
   590  				joined[row][col+1] = &expr.Result{Value: expr.Number(math.NaN())}
   591  			}
   592  		}
   593  	}
   594  	return joined, nil
   595  }
   596  
   597  func (c *Context) HTTPGet(url string) string {
   598  	resp, err := DefaultClient.Get(url)
   599  	if err != nil {
   600  		c.addError(err)
   601  		return err.Error()
   602  	}
   603  	defer resp.Body.Close()
   604  	if resp.StatusCode >= 300 {
   605  		// Drain up to 512 bytes and close the body to let the Transport reuse the connection
   606  		io.CopyN(ioutil.Discard, resp.Body, 512)
   607  		err := fmt.Errorf("%v: returned %v", url, resp.Status)
   608  		c.addError(err)
   609  		return err.Error()
   610  	}
   611  	body, err := ioutil.ReadAll(resp.Body)
   612  	if err != nil {
   613  		c.addError(err)
   614  		return err.Error()
   615  	}
   616  	return string(body)
   617  }
   618  
   619  func (c *Context) HTTPGetJSON(url string) *jsonq.JsonQuery {
   620  	req, err := http.NewRequest("GET", url, nil)
   621  	if err != nil {
   622  		c.addError(err)
   623  		return nil
   624  	}
   625  	req.Header.Set("Accept", "application/json")
   626  	resp, err := DefaultClient.Do(req)
   627  	if err != nil {
   628  		c.addError(err)
   629  		return nil
   630  	}
   631  	defer resp.Body.Close()
   632  	if resp.StatusCode >= 300 {
   633  		c.addError(fmt.Errorf("%v: returned %v", url, resp.Status))
   634  	}
   635  	body, err := ioutil.ReadAll(resp.Body)
   636  	if err != nil {
   637  		c.addError(err)
   638  		return nil
   639  	}
   640  	data := make(map[string]interface{})
   641  	err = json.Unmarshal(body, &data)
   642  	if err != nil {
   643  		c.addError(err)
   644  		return nil
   645  	}
   646  	return jsonq.NewQuery(data)
   647  }
   648  
   649  func (c *Context) HTTPPost(url, bodyType, data string) string {
   650  	resp, err := DefaultClient.Post(url, bodyType, bytes.NewBufferString(data))
   651  	if err != nil {
   652  		c.addError(err)
   653  		return err.Error()
   654  	}
   655  	defer resp.Body.Close()
   656  	if resp.StatusCode >= 300 {
   657  		// Drain up to 512 bytes and close the body to let the Transport reuse the connection
   658  		io.CopyN(ioutil.Discard, resp.Body, 512)
   659  		return fmt.Sprintf("%v: returned %v", url, resp.Status)
   660  	}
   661  	body, err := ioutil.ReadAll(resp.Body)
   662  	if err != nil {
   663  		c.addError(err)
   664  		return err.Error()
   665  	}
   666  	return string(body)
   667  }
   668  
   669  func (c *Context) ESQuery(indexRoot expr.ESIndexer, filter expr.ESQuery, sduration, eduration string, size int) interface{} {
   670  	cfg, ok := c.runHistory.Backends.ElasticHosts.Hosts[c.ElasticHost]
   671  	if !ok {
   672  		return nil
   673  	}
   674  
   675  	switch cfg.Version {
   676  	case expr.ESV2:
   677  		return c.esQuery2(indexRoot, filter, sduration, eduration, size)
   678  	case expr.ESV5:
   679  		return c.esQuery5(indexRoot, filter, sduration, eduration, size)
   680  	case expr.ESV6:
   681  		return c.esQuery6(indexRoot, filter, sduration, eduration, size)
   682  	case expr.ESV7:
   683  		return c.esQuery7(indexRoot, filter, sduration, eduration, size)
   684  	}
   685  
   686  	return nil
   687  }
   688  
   689  func (c *Context) ESQueryAll(indexRoot expr.ESIndexer, filter expr.ESQuery, sduration, eduration string, size int) interface{} {
   690  	cfg, ok := c.runHistory.Backends.ElasticHosts.Hosts[c.ElasticHost]
   691  	if !ok {
   692  		return nil
   693  	}
   694  
   695  	switch cfg.Version {
   696  	case expr.ESV2:
   697  		return c.esQueryAll2(indexRoot, filter, sduration, eduration, size)
   698  	case expr.ESV5:
   699  		return c.esQueryAll5(indexRoot, filter, sduration, eduration, size)
   700  	case expr.ESV6:
   701  		return c.esQueryAll6(indexRoot, filter, sduration, eduration, size)
   702  	case expr.ESV7:
   703  		return c.esQueryAll7(indexRoot, filter, sduration, eduration, size)
   704  	}
   705  
   706  	return nil
   707  }
   708  
   709  // AzureResourceLink create a link to Azure's Portal for the resource (https://portal.azure.com)
   710  // given the subscription identifer (bosun expression prefix), as well as the resource type, group,
   711  // and name. It uses the azrt expression function under the hood
   712  func (c *Context) AzureResourceLink(prefix, rType, rsg, name string) (link string) {
   713  	if prefix == "" {
   714  		prefix = "default"
   715  	}
   716  	// Get clients so we can get the TenantId
   717  	clients := c.schedule.SystemConf.GetAzureMonitorContext()
   718  	client, ok := clients[prefix]
   719  	if !ok {
   720  		c.addError(fmt.Errorf("client/subscription %s not found", prefix))
   721  		return
   722  	}
   723  	selectedResource, err := c.azureSelectResource(prefix, rType, rsg, name)
   724  	if err != nil {
   725  		c.addError(err)
   726  		return
   727  	}
   728  	link = fmt.Sprintf("https://portal.azure.com/#@%s/resource%s", client.TenantId, selectedResource.ID)
   729  	return
   730  }
   731  
   732  // AzureResourceTags returns the Azure tags associated with the resource as a map
   733  func (c *Context) AzureResourceTags(prefix, rType, rsg, name string) map[string]string {
   734  	selectedResource, err := c.azureSelectResource(prefix, rType, rsg, name)
   735  	if err != nil {
   736  		c.addError(err)
   737  		return nil
   738  	}
   739  	return selectedResource.Tags
   740  }
   741  
   742  func (c *Context) azureSelectResource(prefix, rType, rsg, name string) (expr.AzureResource, error) {
   743  	if prefix == "" {
   744  		prefix = "default"
   745  	}
   746  	az := expr.AzureResource{}
   747  	resList, _, err := c.eval(fmt.Sprintf(`["%s"]azrt("%s")`, prefix, rType), false, false, 0)
   748  	if err != nil {
   749  		return az, err
   750  	}
   751  	if len(resList) == 0 {
   752  		return az, fmt.Errorf("no azure resources found for subscription %s and type %s", prefix, rType)
   753  	}
   754  	resources, ok := resList[0].Value.(expr.AzureResources)
   755  	if !ok {
   756  		return az, fmt.Errorf("failed type assertion on azure resource list")
   757  	}
   758  	if selectedResource, found := resources.Get(rType, rsg, name); found {
   759  		return selectedResource, nil
   760  	}
   761  	return az, fmt.Errorf("resource with type %s, group %s, and name %s not found", rType, rsg, name)
   762  }
   763  
   764  // SlackAttachment creates a new SlackAttachment with fields initalized
   765  // from the IncidentState.
   766  func (c *Context) SlackAttachment() *slack.Attachment {
   767  	return slack.NewAttachment(c.IncidentState)
   768  }