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

     1  package web // import "bosun.org/cmd/bosun/web"
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"encoding/json"
     7  	"fmt"
     8  	"html/template"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/http/httputil"
    13  	"net/url"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	version "bosun.org/_version"
    20  	"bosun.org/annotate/backend"
    21  	"bosun.org/annotate/web"
    22  	"bosun.org/cmd/bosun/conf"
    23  	"bosun.org/cmd/bosun/conf/rule"
    24  	"bosun.org/cmd/bosun/database"
    25  	"bosun.org/cmd/bosun/sched"
    26  	"bosun.org/collect"
    27  	"bosun.org/metadata"
    28  	"bosun.org/models"
    29  	"bosun.org/opentsdb"
    30  	"bosun.org/slog"
    31  	"bosun.org/util"
    32  
    33  	"github.com/MiniProfiler/go/miniprofiler"
    34  	"github.com/NYTimes/gziphandler"
    35  	"github.com/captncraig/easyauth"
    36  	"github.com/gorilla/mux"
    37  	"github.com/justinas/alice"
    38  )
    39  
    40  var (
    41  	indexTemplate func() *template.Template
    42  	router        = mux.NewRouter()
    43  	schedule      = sched.DefaultSched
    44  	//InternetProxy is a url to use as a proxy when communicating with external services.
    45  	//currently only google's shortener.
    46  	InternetProxy   *url.URL
    47  	AnnotateBackend backend.Backend
    48  	reload          func() error
    49  
    50  	tokensEnabled bool
    51  	authEnabled   bool
    52  	startTime     time.Time
    53  )
    54  
    55  const (
    56  	tsdbFormat         = "2006/01/02-15:04"
    57  	tsdbFormatSecs     = tsdbFormat + ":05"
    58  	miniprofilerHeader = "X-Miniprofiler"
    59  )
    60  
    61  func init() {
    62  	miniprofiler.Position = "bottomleft"
    63  	miniprofiler.StartHidden = true
    64  	miniprofiler.Enable = func(r *http.Request) bool {
    65  		return r.Header.Get(miniprofilerHeader) != ""
    66  	}
    67  
    68  	metadata.AddMetricMeta("bosun.search.puts_relayed", metadata.Counter, metadata.Request,
    69  		"The count of api put requests sent to Bosun for relaying to the backend server.")
    70  	metadata.AddMetricMeta("bosun.search.datapoints_relayed", metadata.Counter, metadata.Item,
    71  		"The count of data points sent to Bosun for relaying to the backend server.")
    72  	metadata.AddMetricMeta("bosun.relay.bytes", metadata.Counter, metadata.BytesPerSecond,
    73  		"Bytes per second relayed from Bosun to the backend server.")
    74  	metadata.AddMetricMeta("bosun.relay.response", metadata.Counter, metadata.PerSecond,
    75  		"HTTP response codes from the backend server for request relayed through Bosun.")
    76  }
    77  
    78  func Listen(httpAddr, httpsAddr, certFile, keyFile string, devMode bool, tsdbHost string, reloadFunc func() error, authConfig *conf.AuthConf, st time.Time) error {
    79  	startTime = st
    80  	if devMode {
    81  		slog.Infoln("using local web assets")
    82  	}
    83  	webFS := FS(devMode)
    84  
    85  	if httpAddr == "" && httpsAddr == "" {
    86  		return fmt.Errorf("Either http or https address needs to be specified.")
    87  	}
    88  
    89  	indexTemplate = func() *template.Template {
    90  		str := FSMustString(devMode, "/templates/index.html")
    91  		templates, err := template.New("").Parse(str)
    92  		if err != nil {
    93  			slog.Fatal(err)
    94  		}
    95  		return templates
    96  	}
    97  
    98  	reload = reloadFunc
    99  
   100  	if !devMode {
   101  		tpl := indexTemplate()
   102  		indexTemplate = func() *template.Template {
   103  			return tpl
   104  		}
   105  	}
   106  
   107  	baseChain := alice.New(miniProfilerMiddleware, endpointStatsMiddleware, gziphandler.GzipHandler)
   108  
   109  	auth, tokens, err := buildAuth(authConfig)
   110  	if err != nil {
   111  		slog.Fatal(err)
   112  	}
   113  
   114  	//helpers to add routes with middleware
   115  	handle := func(route string, h http.Handler, perms easyauth.Role) *mux.Route {
   116  		return router.Handle(route, baseChain.Then(auth.Wrap(h, perms)))
   117  	}
   118  	handleFunc := func(route string, h http.HandlerFunc, perms easyauth.Role) *mux.Route {
   119  		return handle(route, h, perms)
   120  	}
   121  
   122  	const (
   123  		GET  = http.MethodGet
   124  		POST = http.MethodPost
   125  	)
   126  
   127  	if tsdbHost != "" {
   128  		handleFunc("/api/index", IndexTSDB, canPutData).Name("tsdb_index")
   129  		handle("/api/put", Relay(tsdbHost), canPutData).Name("tsdb_put")
   130  	}
   131  	router.PathPrefix("/auth/").Handler(auth.LoginHandler())
   132  	handleFunc("/api/", APIRedirect, fullyOpen).Name("api_redir")
   133  	handle("/api/action", JSON(Action), canPerformActions).Name("action").Methods(POST)
   134  	handle("/api/alerts", JSON(Alerts), canViewDash).Name("alerts").Methods(GET)
   135  	handle("/api/config", JSON(Config), canViewConfig).Name("get_config").Methods(GET)
   136  
   137  	handle("/api/config_test", JSON(ConfigTest), canViewConfig).Name("config_test").Methods(POST)
   138  	handle("/api/save_enabled", JSON(SaveEnabled), fullyOpen).Name("seve_enabled").Methods(GET)
   139  
   140  	if schedule.SystemConf.ReloadEnabled() {
   141  		handle("/api/reload", JSON(Reload), canSaveConfig).Name("can_save").Methods(POST)
   142  	}
   143  
   144  	if schedule.SystemConf.SaveEnabled() {
   145  		handle("/api/config/bulkedit", JSON(BulkEdit), canSaveConfig).Name("bulk_edit").Methods(POST)
   146  		handle("/api/config/save", JSON(SaveConfig), canSaveConfig).Name("config_save").Methods(POST)
   147  		handle("/api/config/diff", JSON(DiffConfig), canSaveConfig).Name("config_diff").Methods(POST)
   148  		handle("/api/config/running_hash", JSON(ConfigRunningHash), canViewConfig).Name("config_hash").Methods(GET)
   149  	}
   150  
   151  	handle("/api/egraph/{bs}.{format:svg|png}", JSON(ExprGraph), canRunTests).Name("expr_graph")
   152  	handle("/api/errors", JSON(ErrorHistory), canViewDash).Name("errors").Methods(GET, POST)
   153  	handle("/api/expr", JSON(Expr), canRunTests).Name("expr").Methods(POST)
   154  	handle("/api/graph", JSON(Graph), canViewDash).Name("graph").Methods(GET)
   155  
   156  	handle("/api/health", JSON(HealthCheck), fullyOpen).Name("health_check").Methods(GET)
   157  	handle("/api/host", JSON(Host), canViewDash).Name("host").Methods(GET)
   158  	handle("/api/last", JSON(Last), canViewDash).Name("last").Methods(GET)
   159  	handle("/api/quiet", JSON(Quiet), canViewDash).Name("quiet").Methods(GET)
   160  	handle("/api/incidents/open", JSON(ListOpenIncidents), canViewDash).Name("open_incidents").Methods(GET)
   161  	handle("/api/incidents/events", JSON(IncidentEvents), canViewDash).Name("incident_events").Methods(GET)
   162  	handle("/api/metadata/get", JSON(GetMetadata), canViewDash).Name("meta_get").Methods(GET)
   163  	handle("/api/metadata/metrics", JSON(MetadataMetrics), canViewDash).Name("meta_metrics").Methods(GET)
   164  	handle("/api/metadata/put", JSON(PutMetadata), canPutData).Name("meta_put").Methods(POST)
   165  	handle("/api/metadata/delete", JSON(DeleteMetadata), canPutData).Name("meta_delete").Methods(http.MethodDelete)
   166  	handle("/api/metric", JSON(UniqueMetrics), canViewDash).Name("meta_uniqe_metrics").Methods(GET)
   167  	handle("/api/metric/{tagk}", JSON(MetricsByTagKey), canViewDash).Name("meta_metrics_by_tag").Methods(GET)
   168  	handle("/api/metric/{tagk}/{tagv}", JSON(MetricsByTagPair), canViewDash).Name("meta_metric_by_tag_pair").Methods(GET)
   169  
   170  	handle("/api/rule", JSON(Rule), canRunTests).Name("rule_test").Methods(POST)
   171  	handle("/api/rule/notification/test", JSON(TestHTTPNotification), canRunTests).Name("rule__notification_test").Methods(POST)
   172  	handle("/api/shorten", JSON(Shorten), canViewDash).Name("shorten")
   173  	handle("/s/{id}", JSON(GetShortLink), canViewDash).Name("shortlink")
   174  	handle("/api/silence/clear", JSON(SilenceClear), canSilence).Name("silence_clear")
   175  	handle("/api/silence/get", JSON(SilenceGet), canViewDash).Name("silence_get").Methods(GET)
   176  	handle("/api/silence/set", JSON(SilenceSet), canSilence).Name("silence_set")
   177  	handle("/api/status", JSON(Status), canViewDash).Name("status").Methods(GET)
   178  	handle("/api/tagk/{metric}", JSON(TagKeysByMetric), canViewDash).Name("search_tkeys_by_metric").Methods(GET)
   179  	handle("/api/tagv/{tagk}", JSON(TagValuesByTagKey), canViewDash).Name("search_tvals_by_metric").Methods(GET)
   180  	handle("/api/tagv/{tagk}/{metric}", JSON(TagValuesByMetricTagKey), canViewDash).Name("search_tvals_by_metrictagkey").Methods(GET)
   181  	handle("/api/tagsets/{metric}", JSON(FilteredTagsetsByMetric), canViewDash).Name("search_tagsets_by_metric").Methods(GET)
   182  	handle("/api/opentsdb/version", JSON(OpenTSDBVersion), fullyOpen).Name("otsdb_version").Methods(GET)
   183  	handle("/api/annotate", JSON(AnnotateEnabled), fullyOpen).Name("annotate_enabled").Methods(GET)
   184  
   185  	// Annotations
   186  	if schedule.SystemConf.AnnotateEnabled() {
   187  		read := baseChain.Append(auth.Wrapper(canViewAnnotations)).ThenFunc
   188  		write := baseChain.Append(auth.Wrapper(canCreateAnnotations)).ThenFunc
   189  		web.AddRoutesWithMiddleware(router, "/api", []backend.Backend{AnnotateBackend}, false, false, read, write)
   190  	}
   191  
   192  	//auth specific stuff
   193  	if auth != nil {
   194  		router.PathPrefix("/login").Handler(http.StripPrefix("/login", auth.LoginHandler())).Name("auth")
   195  	}
   196  	if tokens != nil {
   197  		handle("/api/tokens", tokens.AdminHandler(), canManageTokens).Name("tokens")
   198  	}
   199  
   200  	router.Handle("/api/version", baseChain.ThenFunc(Version)).Name("version").Methods(GET)
   201  	fs := http.FileServer(webFS)
   202  	router.PathPrefix("/partials/").Handler(baseChain.Then(fs)).Name("partials")
   203  	router.PathPrefix("/static/").Handler(baseChain.Then(http.StripPrefix("/static/", fs))).Name("static")
   204  	router.PathPrefix("/favicon.ico").Handler(baseChain.Then(fs)).Name("favicon")
   205  
   206  	var miniprofilerRoutes = http.StripPrefix(miniprofiler.PATH, http.HandlerFunc(miniprofiler.MiniProfilerHandler))
   207  	router.PathPrefix(miniprofiler.PATH).Handler(baseChain.Then(miniprofilerRoutes)).Name("miniprofiler")
   208  
   209  	//use default mux for pprof
   210  	router.PathPrefix("/debug/pprof").Handler(http.DefaultServeMux)
   211  
   212  	router.PathPrefix("/api").HandlerFunc(http.NotFound)
   213  	//MUST BE LAST!
   214  	router.PathPrefix("/").Handler(baseChain.Then(auth.Wrap(JSON(Index), canViewDash))).Name("index")
   215  
   216  	slog.Infoln("tsdb host:", tsdbHost)
   217  	errChan := make(chan error, 1)
   218  	if httpAddr != "" {
   219  		go func() {
   220  			slog.Infoln("bosun web listening http on:", httpAddr)
   221  			errChan <- http.ListenAndServe(httpAddr, router)
   222  		}()
   223  	}
   224  	if httpsAddr != "" {
   225  		go func() {
   226  			slog.Infoln("bosun web listening https on:", httpsAddr)
   227  			if certFile == "" || keyFile == "" {
   228  				errChan <- fmt.Errorf("certFile and keyfile must be specified to use https")
   229  			}
   230  			errChan <- http.ListenAndServeTLS(httpsAddr, certFile, keyFile, router)
   231  		}()
   232  	}
   233  	return <-errChan
   234  }
   235  
   236  type relayProxy struct {
   237  	*httputil.ReverseProxy
   238  }
   239  
   240  func ResetSchedule() {
   241  	schedule = sched.DefaultSched
   242  }
   243  
   244  type passthru struct {
   245  	io.ReadCloser
   246  	buf bytes.Buffer
   247  }
   248  
   249  func (p *passthru) Read(b []byte) (int, error) {
   250  	n, err := p.ReadCloser.Read(b)
   251  	p.buf.Write(b[:n])
   252  	return n, err
   253  }
   254  
   255  type relayWriter struct {
   256  	http.ResponseWriter
   257  	code int
   258  }
   259  
   260  func (rw *relayWriter) WriteHeader(code int) {
   261  	rw.code = code
   262  	rw.ResponseWriter.WriteHeader(code)
   263  }
   264  
   265  func (rp *relayProxy) ServeHTTP(responseWriter http.ResponseWriter, r *http.Request) {
   266  	clean := func(s string) string {
   267  		return opentsdb.MustReplace(s, "_")
   268  	}
   269  	reader := &passthru{ReadCloser: r.Body}
   270  	r.Body = reader
   271  	w := &relayWriter{ResponseWriter: responseWriter}
   272  	rp.ReverseProxy.ServeHTTP(w, r)
   273  	indexTSDB(r, reader.buf.Bytes())
   274  	tags := opentsdb.TagSet{"path": clean(r.URL.Path), "remote": clean(strings.Split(r.RemoteAddr, ":")[0])}
   275  	collect.Add("relay.bytes", tags, int64(reader.buf.Len()))
   276  	tags["status"] = strconv.Itoa(w.code)
   277  	collect.Add("relay.response", tags, 1)
   278  }
   279  
   280  func Relay(dest string) http.Handler {
   281  	return &relayProxy{ReverseProxy: util.NewSingleHostProxy(&url.URL{
   282  		Scheme: "http",
   283  		Host:   dest,
   284  	})}
   285  }
   286  
   287  func indexTSDB(r *http.Request, body []byte) {
   288  	clean := func(s string) string {
   289  		return opentsdb.MustReplace(s, "_")
   290  	}
   291  	if r, err := gzip.NewReader(bytes.NewReader(body)); err == nil {
   292  		body, _ = ioutil.ReadAll(r)
   293  		r.Close()
   294  	}
   295  	var dp opentsdb.DataPoint
   296  	var mdp opentsdb.MultiDataPoint
   297  	if err := json.Unmarshal(body, &mdp); err == nil {
   298  	} else if err = json.Unmarshal(body, &dp); err == nil {
   299  		mdp = opentsdb.MultiDataPoint{&dp}
   300  	}
   301  	if len(mdp) > 0 {
   302  		ra := strings.Split(r.RemoteAddr, ":")[0]
   303  		tags := opentsdb.TagSet{"remote": clean(ra)}
   304  		collect.Add("search.puts_relayed", tags, 1)
   305  		collect.Add("search.datapoints_relayed", tags, int64(len(mdp)))
   306  		schedule.Search.Index(mdp)
   307  	}
   308  }
   309  
   310  func IndexTSDB(w http.ResponseWriter, r *http.Request) {
   311  	body, err := ioutil.ReadAll(r.Body)
   312  	if err != nil {
   313  		slog.Error(err)
   314  	}
   315  	indexTSDB(r, body)
   316  }
   317  
   318  type appSetings struct {
   319  	SaveEnabled       bool
   320  	AnnotateEnabled   bool
   321  	Quiet             bool
   322  	Version           opentsdb.Version
   323  	ExampleExpression string
   324  
   325  	AuthEnabled   bool
   326  	TokensEnabled bool
   327  	Username      string
   328  	Permissions   easyauth.Role
   329  	Roles         *roleMetadata
   330  }
   331  
   332  type indexVariables struct {
   333  	Includes template.HTML
   334  	Settings string
   335  }
   336  
   337  func Index(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   338  	if r.URL.Path == "/graph" {
   339  		r.ParseForm()
   340  		if _, present := r.Form["png"]; present {
   341  			if _, err := Graph(t, w, r); err != nil {
   342  				return nil, err
   343  			}
   344  			return nil, nil
   345  		}
   346  	}
   347  	r.Header.Set(miniprofilerHeader, "true")
   348  	// Set some global settings for the UI to know about. This saves us from
   349  	// having to make an HTTP call to see what features should be enabled
   350  	// in the UI
   351  	openTSDBVersion := opentsdb.Version{}
   352  	if schedule.SystemConf.GetTSDBContext() != nil {
   353  		openTSDBVersion = schedule.SystemConf.GetTSDBContext().Version()
   354  	}
   355  	u := easyauth.GetUser(r)
   356  	as := &appSetings{
   357  		SaveEnabled:       schedule.SystemConf.SaveEnabled(),
   358  		AnnotateEnabled:   schedule.SystemConf.AnnotateEnabled(),
   359  		Quiet:             schedule.GetQuiet(),
   360  		Version:           openTSDBVersion,
   361  		AuthEnabled:       authEnabled,
   362  		TokensEnabled:     tokensEnabled,
   363  		Roles:             roleDefs,
   364  		ExampleExpression: schedule.SystemConf.GetExampleExpression(),
   365  	}
   366  	if u != nil {
   367  		as.Username = u.Username
   368  		as.Permissions = u.Access
   369  	}
   370  	settings, err := json.Marshal(as)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	err = indexTemplate().Execute(w, indexVariables{
   375  		t.Includes(),
   376  		string(settings),
   377  	})
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	return nil, nil
   382  }
   383  
   384  func serveError(w http.ResponseWriter, err error) {
   385  	http.Error(w, err.Error(), http.StatusInternalServerError)
   386  }
   387  
   388  func JSON(h func(miniprofiler.Timer, http.ResponseWriter, *http.Request) (interface{}, error)) http.Handler {
   389  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   390  		t := miniprofiler.GetTimer(r)
   391  		d, err := h(t, w, r)
   392  		if err != nil {
   393  			serveError(w, err)
   394  			return
   395  		}
   396  		if d == nil {
   397  			return
   398  		}
   399  		buf := new(bytes.Buffer)
   400  		enc := json.NewEncoder(buf)
   401  		if strings.Contains(r.Header.Get("Accept"), "html") || strings.Contains(r.Host, "localhost") {
   402  			enc.SetIndent("", "  ")
   403  		}
   404  		if err := enc.Encode(d); err != nil {
   405  			slog.Error(err)
   406  			serveError(w, err)
   407  			return
   408  		}
   409  		w.Header().Set("Content-Type", "application/json")
   410  		buf.WriteTo(w)
   411  	})
   412  }
   413  
   414  func Shorten(_ miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   415  	id, err := schedule.DataAccess.Configs().ShortenLink(r.Referer())
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  	return struct {
   420  		ID string `json:"id"`
   421  	}{schedule.SystemConf.MakeLink(fmt.Sprintf("/s/%d", id), nil)}, nil
   422  }
   423  
   424  func GetShortLink(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   425  	// on any error or bad param, just redirect to index. Otherwise 302 to stored url
   426  	vars := mux.Vars(r)
   427  	idv := vars["id"]
   428  	id, err := strconv.Atoi(idv)
   429  	targetURL := ""
   430  	if err != nil {
   431  		return Index(t, w, r)
   432  	}
   433  	targetURL, err = schedule.DataAccess.Configs().GetShortLink(id)
   434  	if err != nil {
   435  		return Index(t, w, r)
   436  	}
   437  	http.Redirect(w, r, targetURL, 302)
   438  	return nil, nil
   439  }
   440  
   441  type Health struct {
   442  	// RuleCheck is true if last check happened within the check frequency window.
   443  	RuleCheck     bool
   444  	Quiet         bool
   445  	UptimeSeconds int64
   446  	StartEpoch    int64
   447  	Notifications NotificationStats
   448  }
   449  
   450  type NotificationStats struct {
   451  	// Post and email notifiaction stats
   452  	PostNotificationsSuccess  int64
   453  	PostNotificationsFailed   int64
   454  	EmailNotificationsSuccess int64
   455  	EmailNotificationsFailed  int64
   456  }
   457  
   458  func Reload(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   459  	d := json.NewDecoder(r.Body)
   460  	var sane struct {
   461  		Reload bool
   462  	}
   463  	if err := d.Decode(&sane); err != nil {
   464  		return nil, fmt.Errorf("failed to decode post body: %v", err)
   465  	}
   466  	if !sane.Reload {
   467  		return nil, fmt.Errorf("reload must be set to true in post body")
   468  	}
   469  	err := reload()
   470  	if err != nil {
   471  		return nil, fmt.Errorf("failed to reload: %v", err)
   472  	}
   473  	return "reloaded", nil
   474  
   475  }
   476  
   477  func Quiet(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   478  	return schedule.GetQuiet(), nil
   479  }
   480  
   481  func HealthCheck(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   482  	var h Health
   483  	var n NotificationStats
   484  	h.RuleCheck = schedule.LastCheck.After(time.Now().Add(-schedule.SystemConf.GetCheckFrequency()))
   485  	h.Quiet = schedule.GetQuiet()
   486  	h.UptimeSeconds = int64(time.Since(startTime).Seconds())
   487  	h.StartEpoch = startTime.Unix()
   488  
   489  	//notifications stats
   490  	n.PostNotificationsSuccess = collect.Get("post.sent", nil)
   491  	n.PostNotificationsFailed = collect.Get("post.sent_failed", nil)
   492  	n.EmailNotificationsSuccess = collect.Get("email.sent", nil)
   493  	n.EmailNotificationsFailed = collect.Get("email.sent_failed", nil)
   494  
   495  	h.Notifications = n
   496  	return h, nil
   497  }
   498  
   499  func OpenTSDBVersion(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   500  	if schedule.SystemConf.GetTSDBContext() != nil {
   501  		return schedule.SystemConf.GetTSDBContext().Version(), nil
   502  	}
   503  	return opentsdb.Version{Major: 0, Minor: 0}, nil
   504  }
   505  
   506  func AnnotateEnabled(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   507  	return schedule.SystemConf.AnnotateEnabled(), nil
   508  }
   509  
   510  func PutMetadata(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   511  	d := json.NewDecoder(r.Body)
   512  	var ms []metadata.Metasend
   513  	if err := d.Decode(&ms); err != nil {
   514  		return nil, err
   515  	}
   516  	for _, m := range ms {
   517  		err := schedule.PutMetadata(metadata.Metakey{
   518  			Metric: m.Metric,
   519  			Tags:   m.Tags.Tags(),
   520  			Name:   m.Name,
   521  		}, m.Value)
   522  		if err != nil {
   523  			return nil, err
   524  		}
   525  	}
   526  	w.WriteHeader(204)
   527  	return nil, nil
   528  }
   529  
   530  func DeleteMetadata(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   531  	d := json.NewDecoder(r.Body)
   532  	var ms []struct {
   533  		Tags opentsdb.TagSet
   534  		Name string
   535  	}
   536  	if err := d.Decode(&ms); err != nil {
   537  		return nil, err
   538  	}
   539  	for _, m := range ms {
   540  		err := schedule.DeleteMetadata(m.Tags, m.Name)
   541  		if err != nil {
   542  			return nil, err
   543  		}
   544  	}
   545  	return nil, nil
   546  }
   547  
   548  func GetMetadata(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   549  	tags := make(opentsdb.TagSet)
   550  	r.ParseForm()
   551  	vals := r.Form["tagv"]
   552  	for i, k := range r.Form["tagk"] {
   553  		if len(vals) <= i {
   554  			return nil, fmt.Errorf("unpaired tagk/tagv")
   555  		}
   556  		tags[k] = vals[i]
   557  	}
   558  	return schedule.GetMetadata(r.FormValue("metric"), tags)
   559  }
   560  
   561  type MetricMetaTagKeys struct {
   562  	*database.MetricMetadata
   563  	TagKeys []string
   564  }
   565  
   566  func MetadataMetrics(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   567  	metric := r.FormValue("metric")
   568  	if metric != "" {
   569  		m, err := schedule.MetadataMetrics(metric)
   570  		if err != nil {
   571  			return nil, err
   572  		}
   573  		keymap, err := schedule.DataAccess.Search().GetTagKeysForMetric(metric)
   574  		if err != nil {
   575  			return nil, err
   576  		}
   577  		var keys []string
   578  		for k := range keymap {
   579  			keys = append(keys, k)
   580  		}
   581  		return &MetricMetaTagKeys{
   582  			MetricMetadata: m,
   583  			TagKeys:        keys,
   584  		}, nil
   585  	}
   586  	all := make(map[string]*MetricMetaTagKeys)
   587  	metrics, err := schedule.DataAccess.Search().GetAllMetrics()
   588  	if err != nil {
   589  		return nil, err
   590  	}
   591  	for metric := range metrics {
   592  		if strings.HasPrefix(metric, "__") {
   593  			continue
   594  		}
   595  		m, err := schedule.MetadataMetrics(metric)
   596  		if err != nil {
   597  			return nil, err
   598  		}
   599  		keymap, err := schedule.DataAccess.Search().GetTagKeysForMetric(metric)
   600  		if err != nil {
   601  			return nil, err
   602  		}
   603  		var keys []string
   604  		for k := range keymap {
   605  			keys = append(keys, k)
   606  		}
   607  		all[metric] = &MetricMetaTagKeys{
   608  			MetricMetadata: m,
   609  			TagKeys:        keys,
   610  		}
   611  	}
   612  	return all, nil
   613  }
   614  
   615  func Alerts(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   616  	return schedule.MarshalGroups(t, r.FormValue("filter"))
   617  }
   618  
   619  type ExtStatus struct {
   620  	AlertName string
   621  	Subject   string
   622  	*models.IncidentState
   623  	*models.RenderedTemplates
   624  }
   625  
   626  type ExtIncidentStatus struct {
   627  	ExtStatus
   628  	IsActive  bool
   629  	Silence   *models.Silence
   630  	SilenceId string
   631  }
   632  
   633  func IncidentEvents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   634  	id := r.FormValue("id")
   635  	if id == "" {
   636  		return nil, fmt.Errorf("id must be specified")
   637  	}
   638  	num, err := strconv.ParseInt(id, 10, 64)
   639  	if err != nil {
   640  		return nil, err
   641  	}
   642  	state, err := schedule.DataAccess.State().GetIncidentState(num)
   643  	if err != nil {
   644  		return nil, err
   645  	}
   646  	rt, err := schedule.DataAccess.State().GetRenderedTemplates(state.Id)
   647  	if err != nil {
   648  		return nil, err
   649  	}
   650  	st := ExtIncidentStatus{
   651  		ExtStatus: ExtStatus{IncidentState: state, RenderedTemplates: rt, Subject: state.Subject},
   652  		IsActive:  state.IsActive(),
   653  	}
   654  	silence := schedule.GetSilence(t, state.AlertKey)
   655  	if silence != nil {
   656  		st.Silence = silence
   657  		st.SilenceId = silence.ID()
   658  	}
   659  	return st, nil
   660  }
   661  
   662  func Status(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   663  	r.ParseForm()
   664  	m := make(map[string]ExtStatus)
   665  	for _, k := range r.Form["ak"] {
   666  		ak, err := models.ParseAlertKey(k)
   667  		if err != nil {
   668  			return nil, err
   669  		}
   670  		var state *models.IncidentState
   671  		if r.FormValue("all") != "" {
   672  			allInc, err := schedule.DataAccess.State().GetAllIncidentsByAlertKey(ak)
   673  			if err != nil {
   674  				return nil, err
   675  			}
   676  			if len(allInc) == 0 {
   677  				return nil, fmt.Errorf("No incidents for alert key")
   678  			}
   679  			state = allInc[0]
   680  			allEvents := models.EventsByTime{}
   681  			for _, inc := range allInc {
   682  				for _, e := range inc.Events {
   683  					allEvents = append(allEvents, e)
   684  				}
   685  			}
   686  			sort.Sort(allEvents)
   687  			state.Events = allEvents
   688  		} else {
   689  			state, err = schedule.DataAccess.State().GetLatestIncident(ak)
   690  			if err != nil {
   691  				return nil, err
   692  			}
   693  			if state == nil {
   694  				return nil, fmt.Errorf("alert key %v wasn't found", k)
   695  			}
   696  		}
   697  		rt, err := schedule.DataAccess.State().GetRenderedTemplates(state.Id)
   698  		if err != nil {
   699  			return nil, err
   700  		}
   701  		st := ExtStatus{IncidentState: state, RenderedTemplates: rt}
   702  		if st.IncidentState == nil {
   703  			return nil, fmt.Errorf("unknown alert key: %v", k)
   704  		}
   705  		st.AlertName = ak.Name()
   706  		m[k] = st
   707  	}
   708  	return m, nil
   709  }
   710  
   711  func getUsername(r *http.Request) string {
   712  	user := easyauth.GetUser(r)
   713  	if user != nil {
   714  		return user.Username
   715  	}
   716  	return "unknown"
   717  }
   718  
   719  func userCanOverwriteUsername(r *http.Request) bool {
   720  	user := easyauth.GetUser(r)
   721  	if user != nil {
   722  		return user.Access&canOverwriteUsername != 0
   723  	}
   724  	return false
   725  }
   726  
   727  func Action(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   728  	var data struct {
   729  		Type    string
   730  		Message string
   731  		Keys    []string
   732  		Ids     []int64
   733  		Notify  bool
   734  		User    string
   735  		Time    *time.Time
   736  	}
   737  	j := json.NewDecoder(r.Body)
   738  	if err := j.Decode(&data); err != nil {
   739  		return nil, err
   740  	}
   741  	var at models.ActionType
   742  	// TODO Make constants in the JS code for these that *match* the names the string Method for ActionType
   743  	switch data.Type {
   744  	case "ack":
   745  		at = models.ActionAcknowledge
   746  	case "close":
   747  		at = models.ActionClose
   748  	case "cancelClose":
   749  		at = models.ActionCancelClose
   750  	case "forget":
   751  		at = models.ActionForget
   752  	case "forceClose":
   753  		at = models.ActionForceClose
   754  	case "purge":
   755  		at = models.ActionPurge
   756  	case "note":
   757  		at = models.ActionNote
   758  	}
   759  	errs := make(MultiError)
   760  	r.ParseForm()
   761  	successful := []models.AlertKey{}
   762  
   763  	if data.User != "" && !userCanOverwriteUsername(r) {
   764  		http.Error(w, "Not Authorized to set User", 400)
   765  		return nil, nil
   766  	} else if data.User == "" {
   767  		data.User = getUsername(r)
   768  	}
   769  
   770  	for _, key := range data.Keys {
   771  		ak, err := models.ParseAlertKey(key)
   772  		if err != nil {
   773  			return nil, err
   774  		}
   775  		err = schedule.ActionByAlertKey(data.User, data.Message, at, data.Time, ak)
   776  		if err != nil {
   777  			errs[key] = err
   778  		} else {
   779  			successful = append(successful, ak)
   780  		}
   781  	}
   782  	for _, id := range data.Ids {
   783  		ak, err := schedule.ActionByIncidentId(data.User, data.Message, at, data.Time, id)
   784  		if err != nil {
   785  			errs[fmt.Sprintf("%v", id)] = err
   786  		} else {
   787  			successful = append(successful, ak)
   788  		}
   789  	}
   790  	if len(errs) != 0 {
   791  		return nil, errs
   792  	}
   793  	if data.Notify && len(successful) != 0 {
   794  		err := schedule.ActionNotify(at, data.User, data.Message, successful)
   795  		if err != nil {
   796  			return nil, err
   797  		}
   798  	} else {
   799  		slog.Infof("action without notification. user: %s, type: %s, keys: %v, ids: %v", data.User, data.Type, data.Keys, data.Ids)
   800  	}
   801  	return nil, nil
   802  }
   803  
   804  type MultiError map[string]error
   805  
   806  func (m MultiError) Error() string {
   807  	return fmt.Sprint(map[string]error(m))
   808  }
   809  
   810  func SilenceGet(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   811  	endingAfter := time.Now().UTC().Unix()
   812  	if t := r.FormValue("t"); t != "" {
   813  		endingAfter, _ = strconv.ParseInt(t, 10, 64)
   814  	}
   815  	return schedule.DataAccess.Silence().ListSilences(endingAfter)
   816  }
   817  
   818  var silenceLayouts = []string{
   819  	tsdbFormat,
   820  	tsdbFormatSecs,
   821  	"2006-01-02 15:04:05 MST",
   822  	"2006-01-02 15:04:05 -0700",
   823  	"2006-01-02 15:04 MST",
   824  	"2006-01-02 15:04 -0700",
   825  	"2006-01-02 15:04:05",
   826  	"2006-01-02 15:04",
   827  }
   828  
   829  func SilenceSet(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   830  	var start, end time.Time
   831  	var err error
   832  	var data map[string]string
   833  	j := json.NewDecoder(r.Body)
   834  	if err := j.Decode(&data); err != nil {
   835  		return nil, err
   836  	}
   837  	if s := data["start"]; s != "" {
   838  		for _, layout := range silenceLayouts {
   839  			start, err = time.Parse(layout, s)
   840  			if err == nil {
   841  				break
   842  			}
   843  		}
   844  		if start.IsZero() {
   845  			return nil, fmt.Errorf("unrecognized start time format: %s", s)
   846  		}
   847  	}
   848  	if s := data["end"]; s != "" {
   849  		for _, layout := range silenceLayouts {
   850  			end, err = time.Parse(layout, s)
   851  			if err == nil {
   852  				break
   853  			}
   854  		}
   855  		if end.IsZero() {
   856  			return nil, fmt.Errorf("unrecognized end time format: %s", s)
   857  		}
   858  	}
   859  	if start.IsZero() {
   860  		start = time.Now().UTC()
   861  	}
   862  	if end.IsZero() {
   863  		d, err := opentsdb.ParseDuration(data["duration"])
   864  		if err != nil {
   865  			return nil, err
   866  		}
   867  		end = start.Add(time.Duration(d))
   868  	}
   869  	username := getUsername(r)
   870  	if _, ok := data["user"]; ok && !userCanOverwriteUsername(r) {
   871  		http.Error(w, "Not authorized to set 'user' parameter", 400)
   872  		return nil, nil
   873  	} else if ok {
   874  		username = data["user"]
   875  	}
   876  	return schedule.AddSilence(start, end, data["alert"], data["tags"], data["forget"] == "true", len(data["confirm"]) > 0, data["edit"], username, data["message"])
   877  }
   878  
   879  func SilenceClear(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   880  	id := r.FormValue("id")
   881  	return nil, schedule.ClearSilence(id)
   882  }
   883  
   884  func ConfigTest(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   885  	b, err := ioutil.ReadAll(r.Body)
   886  	if err != nil {
   887  		return nil, err
   888  	}
   889  	if len(b) == 0 {
   890  		return nil, fmt.Errorf("empty config")
   891  	}
   892  	_, err = rule.NewConf("test", schedule.SystemConf.EnabledBackends(), schedule.SystemConf.GetRuleVars(), string(b))
   893  	if err != nil {
   894  		fmt.Fprintf(w, err.Error())
   895  	}
   896  	return nil, nil
   897  }
   898  
   899  func Config(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   900  	var text string
   901  	var err error
   902  	if hash := r.FormValue("hash"); hash != "" {
   903  		text, err = schedule.DataAccess.Configs().GetTempConfig(hash)
   904  		if err != nil {
   905  			return nil, err
   906  		}
   907  	} else {
   908  		text = schedule.RuleConf.GetRawText()
   909  	}
   910  	fmt.Fprint(w, text)
   911  	return nil, nil
   912  }
   913  
   914  func APIRedirect(w http.ResponseWriter, req *http.Request) {
   915  	http.Redirect(w, req, "http://bosun.org/api.html", 302)
   916  }
   917  
   918  func Host(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   919  	return schedule.Host(r.FormValue("filter"))
   920  }
   921  
   922  // Last returns the most recent datapoint for a metric+tagset. The metric+tagset
   923  // string should be formated like os.cpu{host=foo}. The tag porition expects the
   924  // that the keys will be in alphabetical order.
   925  func Last(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   926  	var counter bool
   927  	if r.FormValue("counter") != "" {
   928  		counter = true
   929  	}
   930  	val, timestamp, err := schedule.Search.GetLast(r.FormValue("metric"), r.FormValue("tagset"), counter)
   931  	return struct {
   932  		Value     float64
   933  		Timestamp int64
   934  	}{
   935  		val,
   936  		timestamp,
   937  	}, err
   938  }
   939  
   940  func Version(w http.ResponseWriter, r *http.Request) {
   941  	io.WriteString(w, version.GetVersionInfo("bosun"))
   942  }
   943  
   944  func ErrorHistory(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   945  	if r.Method == "GET" {
   946  		data, err := schedule.DataAccess.Errors().GetFullErrorHistory()
   947  		if err != nil {
   948  			return nil, err
   949  		}
   950  		type AlertStatus struct {
   951  			Success bool
   952  			Errors  []*models.AlertError
   953  		}
   954  		failingAlerts, err := schedule.DataAccess.Errors().GetFailingAlerts()
   955  		if err != nil {
   956  			return nil, err
   957  		}
   958  		m := make(map[string]*AlertStatus, len(data))
   959  		for a, list := range data {
   960  			m[a] = &AlertStatus{
   961  				Success: !failingAlerts[a],
   962  				Errors:  list,
   963  			}
   964  		}
   965  		return m, nil
   966  	}
   967  	if r.Method == "POST" {
   968  		data := []string{}
   969  		decoder := json.NewDecoder(r.Body)
   970  		if err := decoder.Decode(&data); err != nil {
   971  			return nil, err
   972  		}
   973  		for _, key := range data {
   974  			if err := schedule.ClearErrors(key); err != nil {
   975  				return nil, err
   976  			}
   977  		}
   978  	}
   979  	return nil, nil
   980  }