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 }