| // Package frontend contains the Go code that servers the Perf web UI. |
| package frontend |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "html/template" |
| "io/ioutil" |
| "math/rand" |
| "net/http" |
| "net/http/pprof" |
| "net/url" |
| "os" |
| "runtime" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "cloud.google.com/go/storage" |
| "github.com/gorilla/mux" |
| "go.opencensus.io/trace" |
| "go.skia.org/infra/go/auditlog" |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/calc" |
| "go.skia.org/infra/go/email" |
| "go.skia.org/infra/go/gitauth" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/login" |
| "go.skia.org/infra/go/metrics2" |
| "go.skia.org/infra/go/paramtools" |
| "go.skia.org/infra/go/query" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/sklog/sklog_impl" |
| "go.skia.org/infra/go/util" |
| "go.skia.org/infra/perf/go/alertfilter" |
| "go.skia.org/infra/perf/go/alerts" |
| "go.skia.org/infra/perf/go/bug" |
| "go.skia.org/infra/perf/go/builders" |
| "go.skia.org/infra/perf/go/config" |
| "go.skia.org/infra/perf/go/dataframe" |
| "go.skia.org/infra/perf/go/dfbuilder" |
| "go.skia.org/infra/perf/go/dist" |
| "go.skia.org/infra/perf/go/dryrun" |
| perfgit "go.skia.org/infra/perf/go/git" |
| "go.skia.org/infra/perf/go/notify" |
| "go.skia.org/infra/perf/go/progress" |
| "go.skia.org/infra/perf/go/psrefresh" |
| "go.skia.org/infra/perf/go/regression" |
| "go.skia.org/infra/perf/go/regression/continuous" |
| "go.skia.org/infra/perf/go/shortcut" |
| "go.skia.org/infra/perf/go/tracestore" |
| "go.skia.org/infra/perf/go/tracing" |
| "go.skia.org/infra/perf/go/trybot/results" |
| "go.skia.org/infra/perf/go/trybot/results/dfloader" |
| "go.skia.org/infra/perf/go/types" |
| "google.golang.org/api/option" |
| ) |
| |
| const ( |
| // regressionCountDuration is how far back we look for regression in the /_/reg/count endpoint. |
| regressionCountDuration = -14 * 24 * time.Hour |
| |
| // defaultAlertCategory is the category that will be used by the /_/alerts/ endpoint. |
| defaultAlertCategory = "Prod" |
| |
| // paramsetRefresherPeriod is how often we refresh our canonical paramset from the OPS's |
| // stored in the last two tiles. |
| paramsetRefresherPeriod = 5 * time.Minute |
| |
| // startClusterDelay is the time we wait between starting each clusterer, to avoid hammering |
| // the trace store all at once. |
| startClusterDelay = 2 * time.Second |
| |
| // defaultBugURLTemplate is the URL template to use if the user |
| // doesn't supply one. |
| defaultBugURLTemplate = "https://bugs.chromium.org/p/skia/issues/entry?comment=This+bug+was+found+via+SkiaPerf.%0A%0AVisit+this+URL+to+see+the+details+of+the+suspicious+cluster%3A%0A%0A++{cluster_url}%0A%0AThe+suspect+commit+is%3A%0A%0A++{commit_url}%0A%0A++{message}&labels=FromSkiaPerf%2CType-Defect%2CPriority-Medium" |
| |
| // longRunningRequestTimeout is a limit on long running processes. |
| longRunningRequestTimeout = 20 * time.Minute |
| ) |
| |
| // Frontend is the server for the Perf web UI. |
| type Frontend struct { |
| perfGit *perfgit.Git |
| |
| templates *template.Template |
| |
| loadTemplatesOnce sync.Once |
| |
| regStore regression.Store |
| |
| continuous []*continuous.Continuous |
| |
| storageClient *storage.Client |
| |
| alertStore alerts.Store |
| |
| shortcutStore shortcut.Store |
| |
| configProvider continuous.ConfigProvider |
| |
| notifier *notify.Notifier |
| |
| traceStore tracestore.TraceStore |
| |
| emailAuth *email.GMail |
| |
| dryrunRequests *dryrun.Requests |
| |
| paramsetRefresher *psrefresh.ParamSetRefresher |
| |
| dfBuilder dataframe.DataFrameBuilder |
| |
| trybotResultsLoader results.Loader |
| |
| // distFileSystem is the ./dist directory of files produced by webpack. |
| distFileSystem http.FileSystem |
| |
| flags *config.FrontendFlags |
| |
| // progressTracker tracks long running web requests. |
| progressTracker progress.Tracker |
| } |
| |
| // New returns a new Frontend instance. |
| func New(flags *config.FrontendFlags) (*Frontend, error) { |
| f := &Frontend{ |
| flags: flags, |
| } |
| f.initialize() |
| |
| return f, nil |
| } |
| |
| func fileContentsFromFileSystem(fileSystem http.FileSystem, filename string) (string, error) { |
| f, err := fileSystem.Open(filename) |
| if err != nil { |
| return "", skerr.Wrapf(err, "Failed to open %q", filename) |
| } |
| b, err := ioutil.ReadAll(f) |
| if err != nil { |
| return "", skerr.Wrapf(err, "Failed to read %q", filename) |
| } |
| if err := f.Close(); err != nil { |
| return "", skerr.Wrapf(err, "Failed to close %q", filename) |
| } |
| return string(b), nil |
| } |
| |
| var templateFilenames = []string{ |
| "newindex.html", |
| "clusters2.html", |
| "triage.html", |
| "alerts.html", |
| "help.html", |
| "dryRunAlert.html", |
| "trybot.html", |
| } |
| |
| func (f *Frontend) loadTemplatesImpl() { |
| f.templates = template.New("") |
| for _, filename := range templateFilenames { |
| contents, err := fileContentsFromFileSystem(f.distFileSystem, filename) |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| f.templates = f.templates.New(filename) |
| _, err = f.templates.Parse(contents) |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| } |
| } |
| |
| func (f *Frontend) loadTemplates() { |
| if f.flags.Local { |
| f.loadTemplatesImpl() |
| return |
| } |
| f.loadTemplatesOnce.Do(f.loadTemplatesImpl) |
| } |
| |
| // SkPerfConfig is the configuration data that will appear |
| // in Javascript under the sk.perf variable. |
| type SkPerfConfig struct { |
| Radius int `json:"radius"` // The number of commits when doing clustering. |
| KeyOrder []string `json:"key_order"` // The order of the keys to appear first in query-sk elements. |
| NumShift int `json:"num_shift"` // The number of commits the shift navigation buttons should jump. |
| Interesting float32 `json:"interesting"` // The threshold for a cluster to be interesting. |
| StepUpOnly bool `json:"step_up_only"` // If true then only regressions that are a step up are displayed. |
| CommitRangeURL string `json:"commit_range_url"` // A URI Template to be used for expanding details on a range of commits. See cluster-summary2-sk. |
| Demo bool `json:"demo"` // True if this is a demo page, as opposed to being in production. Used to make puppeteer tests deterministic. |
| DisplayGroupBy bool `json:"display_group_by"` // True if the Group By section of Alert config should be displayed. |
| } |
| |
| func (f *Frontend) templateHandler(name string) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "text/html") |
| f.loadTemplates() |
| context := SkPerfConfig{ |
| Radius: f.flags.Radius, |
| KeyOrder: strings.Split(f.flags.KeyOrder, ","), |
| NumShift: f.flags.NumShift, |
| Interesting: float32(f.flags.Interesting), |
| StepUpOnly: f.flags.StepUpOnly, |
| CommitRangeURL: f.flags.CommitRangeURL, |
| DisplayGroupBy: f.flags.DisplayGroupBy, |
| } |
| b, err := json.MarshalIndent(context, "", " ") |
| if err != nil { |
| sklog.Errorf("Failed to JSON encode sk.perf context: %s", err) |
| } |
| if err := f.templates.ExecuteTemplate(w, name, map[string]template.JS{"context": template.JS(string(b))}); err != nil { |
| sklog.Error("Failed to expand template:", err) |
| } |
| } |
| } |
| |
| // scriptHandler serves up a template as a script. |
| func (f *Frontend) scriptHandler(name string) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "text/javascript") |
| f.loadTemplates() |
| if err := f.templates.ExecuteTemplate(w, name, nil); err != nil { |
| sklog.Error("Failed to expand template:", err) |
| } |
| } |
| } |
| |
| // newParamsetProvider returns a regression.ParamsetProvider which produces a paramset |
| // for the current tiles. |
| // |
| func newParamsetProvider(pf *psrefresh.ParamSetRefresher) regression.ParamsetProvider { |
| return func() paramtools.ReadOnlyParamSet { |
| return pf.Get() |
| } |
| } |
| |
| // newAlertsConfigProvider returns a regression.ConfigProvider which produces a slice |
| // of alerts.Config to run continuous clustering against. |
| func (f *Frontend) newAlertsConfigProvider() continuous.ConfigProvider { |
| return func() ([]*alerts.Alert, error) { |
| return f.alertStore.List(context.Background(), false) |
| } |
| } |
| |
| // initialize the application. |
| func (f *Frontend) initialize() { |
| rand.Seed(time.Now().UnixNano()) |
| |
| runtime.GOMAXPROCS(runtime.NumCPU()) |
| |
| if err := tracing.Init(f.flags.Local); err != nil { |
| sklog.Fatalf("Failed to start tracing: %s", err) |
| } |
| |
| // Record UID and GID. |
| sklog.Infof("Running as %d:%d", os.Getuid(), os.Getgid()) |
| |
| // Init metrics. |
| metrics2.InitPrometheus(f.flags.PromPort) |
| _ = metrics2.NewLiveness("uptime", nil) |
| |
| // Init auth. |
| redirectURL := fmt.Sprintf("http://localhost%s/oauth2callback/", f.flags.Port) |
| if !f.flags.Local { |
| redirectURL = login.DEFAULT_REDIRECT_URL |
| } |
| if f.flags.AuthBypassList == "" { |
| f.flags.AuthBypassList = login.DEFAULT_ALLOWED_DOMAINS |
| } |
| if err := login.Init(redirectURL, f.flags.AuthBypassList, ""); err != nil { |
| sklog.Fatalf("Failed to initialize the login system: %s", err) |
| } |
| |
| var err error |
| // Add tracker for long running requests. |
| f.progressTracker, err = progress.NewTracker("/_/status/") |
| if err != nil { |
| sklog.Fatalf("Failed to initialize Tracker: %s", err) |
| } |
| |
| // Keep HTTP request metrics. |
| severities := sklog_impl.AllSeverities() |
| metricLookup := make([]metrics2.Counter, len(severities)) |
| for _, sev := range severities { |
| metricLookup[sev] = metrics2.GetCounter("num_log_lines", map[string]string{"level": sev.String()}) |
| } |
| metricsCallback := func(severity sklog_impl.Severity) { |
| metricLookup[severity].Inc(1) |
| } |
| sklog_impl.SetMetricsCallback(metricsCallback) |
| |
| // Load the config file. |
| if err := config.Init(f.flags.ConfigFilename); err != nil { |
| sklog.Fatal(err) |
| } |
| if f.flags.ConnectionString != "" { |
| config.Config.DataStoreConfig.ConnectionString = f.flags.ConnectionString |
| } |
| cfg := config.Config |
| |
| ctx := context.Background() |
| f.distFileSystem, err = dist.New() |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| |
| scopes := []string{storage.ScopeReadOnly, auth.SCOPE_GERRIT} |
| |
| sklog.Info("About to create token source.") |
| ts, err := auth.NewDefaultTokenSource(f.flags.Local, scopes...) |
| if err != nil { |
| sklog.Fatalf("Failed to get TokenSource: %s", err) |
| } |
| |
| if !f.flags.Local { |
| if _, err := gitauth.New(ts, "/tmp/git-cookie", true, ""); err != nil { |
| sklog.Fatal(err) |
| } |
| } |
| |
| sklog.Info("About to init GCS.") |
| f.storageClient, err = storage.NewClient(ctx, option.WithTokenSource(ts)) |
| if err != nil { |
| sklog.Fatalf("Failed to authenicate to cloud storage: %s", err) |
| } |
| |
| sklog.Info("About to parse templates.") |
| f.loadTemplates() |
| |
| sklog.Info("About to build trace store.") |
| |
| f.traceStore, err = builders.NewTraceStoreFromConfig(ctx, f.flags.Local, config.Config) |
| if err != nil { |
| sklog.Fatalf("Failed to build TraceStore: %s", err) |
| } |
| |
| sklog.Info("About to build paramset refresher.") |
| |
| f.paramsetRefresher = psrefresh.NewParamSetRefresher(f.traceStore) |
| if err := f.paramsetRefresher.Start(paramsetRefresherPeriod); err != nil { |
| sklog.Fatalf("Failed to build paramsetRefresher: %s", err) |
| } |
| |
| sklog.Info("About to build perfgit.") |
| |
| f.perfGit, err = builders.NewPerfGitFromConfig(ctx, f.flags.Local, config.Config) |
| if err != nil { |
| sklog.Fatalf("Failed to build perfgit.Git: %s", err) |
| } |
| |
| sklog.Info("About to build dfbuilder.") |
| f.dfBuilder = dfbuilder.NewDataFrameBuilderFromTraceStore(f.perfGit, f.traceStore) |
| |
| // TODO(jcgregorio) Implement store.TryBotStore and add a reference to it here. |
| f.trybotResultsLoader = dfloader.New(f.dfBuilder, nil, f.perfGit) |
| |
| alerts.DefaultSparse = f.flags.DefaultSparse |
| |
| sklog.Info("About to build alertStore.") |
| f.alertStore, err = builders.NewAlertStoreFromConfig(ctx, f.flags.Local, config.Config) |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| f.shortcutStore, err = builders.NewShortcutStoreFromConfig(ctx, f.flags.Local, config.Config) |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| |
| if !f.flags.NoEmail { |
| f.emailAuth, err = email.NewFromFiles(f.flags.EmailTokenCacheFile, f.flags.EmailClientSecretFile) |
| if err != nil { |
| sklog.Fatalf("Failed to create email auth: %v", err) |
| } |
| f.notifier = notify.New(f.emailAuth, config.Config.URL) |
| } else { |
| f.notifier = notify.New(notify.NoEmail{}, config.Config.URL) |
| } |
| |
| f.regStore, err = builders.NewRegressionStoreFromConfig(ctx, f.flags.Local, cfg) |
| if err != nil { |
| sklog.Fatalf("Failed to build regression.Store: %s", err) |
| } |
| f.configProvider = f.newAlertsConfigProvider() |
| paramsProvider := newParamsetProvider(f.paramsetRefresher) |
| |
| f.dryrunRequests = dryrun.New(f.perfGit, f.progressTracker, f.shortcutStore, f.dfBuilder, paramsProvider) |
| |
| if f.flags.DoClustering { |
| go func() { |
| for i := 0; i < f.flags.NumContinuousParallel; i++ { |
| // Start running continuous clustering looking for regressions. |
| time.Sleep(startClusterDelay) |
| c := continuous.New(f.perfGit, f.shortcutStore, f.configProvider, f.regStore, f.notifier, paramsProvider, f.dfBuilder, |
| cfg, f.flags) |
| f.continuous = append(f.continuous, c) |
| go c.Run(context.Background()) |
| } |
| }() |
| } |
| } |
| |
| // helpHandler handles the GET of the main page. |
| func (f *Frontend) helpHandler(w http.ResponseWriter, r *http.Request) { |
| sklog.Infof("Help Handler: %q\n", r.URL.Path) |
| f.loadTemplates() |
| if r.Method == "GET" { |
| w.Header().Set("Content-Type", "text/html") |
| ctx := calc.NewContext(nil, nil) |
| if err := f.templates.ExecuteTemplate(w, "help.html", ctx); err != nil { |
| sklog.Error("Failed to expand template:", err) |
| } |
| } |
| } |
| |
| func (f *Frontend) alertsHandler(w http.ResponseWriter, r *http.Request) { |
| count, err := f.regressionCount(r.Context(), defaultAlertCategory) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load untriaged count.", http.StatusInternalServerError) |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| w.Header().Add("Access-Control-Allow-Origin", "*") |
| resp := alerts.AlertsStatus{ |
| Alerts: count, |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| func (f *Frontend) initpageHandler(w http.ResponseWriter, r *http.Request) { |
| resp := &dataframe.FrameResponse{ |
| DataFrame: &dataframe.DataFrame{ |
| ParamSet: f.paramsetRefresher.Get(), |
| }, |
| Skps: []int{}, |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| func (f *Frontend) trybotLoadHandler(w http.ResponseWriter, r *http.Request) { |
| var req results.TryBotRequest |
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| prog := progress.New() |
| f.progressTracker.Add(prog) |
| go func() { |
| ctx, span := trace.StartSpan(context.Background(), "trybotLoadHandler") |
| defer span.End() |
| |
| ctx, cancel := context.WithTimeout(ctx, longRunningRequestTimeout) |
| defer cancel() |
| |
| resp, err := f.trybotResultsLoader.Load(ctx, req, nil) |
| if err != nil { |
| prog.Error("Failed to load results.") |
| sklog.Errorf("trybot failed to load results: %s", err) |
| return |
| } |
| prog.FinishedWithResults(resp) |
| }() |
| w.Header().Set("Content-Type", "application/json") |
| if err := prog.JSON(w); err != nil { |
| sklog.Errorf("Failed to encode trybot results: %s", err) |
| } |
| } |
| |
| // RangeRequest is used in cidRangeHandler and is used to query for a range of |
| // cid.CommitIDs that include the range between [begin, end) and include the |
| // explicit CommitID of "Source, Offset". |
| type RangeRequest struct { |
| Offset types.CommitNumber `json:"offset"` |
| Begin int64 `json:"begin"` |
| End int64 `json:"end"` |
| } |
| |
| // cidRangeHandler accepts a POST'd JSON serialized RangeRequest |
| // and returns a serialized JSON slice of cid.CommitDetails. |
| func (f *Frontend) cidRangeHandler(w http.ResponseWriter, r *http.Request) { |
| ctx := r.Context() |
| w.Header().Set("Content-Type", "application/json") |
| var rr RangeRequest |
| if err := json.NewDecoder(r.Body).Decode(&rr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| |
| resp, err := f.perfGit.CommitSliceFromTimeRange(ctx, time.Unix(rr.Begin, 0), time.Unix(rr.End, 0)) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to look up commits", http.StatusInternalServerError) |
| return |
| } |
| |
| if rr.Offset != types.BadCommitNumber { |
| details, err := f.perfGit.CommitFromCommitNumber(ctx, rr.Offset) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to look up commit", http.StatusInternalServerError) |
| return |
| } |
| resp = append(resp, details) |
| } |
| |
| // Filter if we have a restricted set of branches. |
| ret := []perfgit.Commit{} |
| if len(config.Config.IngestionConfig.Branches) != 0 { |
| for _, details := range resp { |
| for _, branch := range config.Config.IngestionConfig.Branches { |
| if strings.HasSuffix(details.Subject, branch) { |
| ret = append(ret, details) |
| continue |
| } |
| } |
| } |
| } else { |
| ret = resp |
| } |
| |
| if err := json.NewEncoder(w).Encode(ret); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| // frameStartResponse is serialized as JSON for the response in |
| // frameStartHandler. |
| type frameStartResponse struct { |
| ID string `json:"id"` |
| } |
| |
| // frameStartHandler starts a FrameRequest running and returns the ID |
| // of the Go routine doing the work. |
| // |
| // Building a DataFrame can take a long time to complete, so we run the request |
| // in a Go routine and break the building of DataFrames into three separate |
| // requests: |
| // * Start building the DataFrame (_/frame/start), which returns an identifier of the long |
| // running request, {id}. |
| // * Query the status of the running request (_/frame/status/{id}). |
| // * Finally return the constructed DataFrame (_/frame/results/{id}). |
| func (f *Frontend) frameStartHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| fr := dataframe.NewFrameRequest() |
| if err := json.NewDecoder(r.Body).Decode(fr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| auditlog.Log(r, "query", fr) |
| // Remove all empty queries. |
| q := []string{} |
| for _, s := range fr.Queries { |
| if strings.TrimSpace(s) != "" { |
| q = append(q, s) |
| } |
| } |
| fr.Queries = q |
| |
| if len(fr.Formulas) == 0 && len(fr.Queries) == 0 && fr.Keys == "" { |
| httputils.ReportError(w, fmt.Errorf("Invalid query."), "Empty queries are not allowed.", http.StatusInternalServerError) |
| return |
| } |
| |
| ctx, span := trace.StartSpan(context.Background(), "frameStartRequest") |
| defer span.End() |
| f.progressTracker.Add(fr.Progress) |
| |
| go func() { |
| err := dataframe.ProcessFrameRequest(ctx, fr, f.perfGit, f.dfBuilder, f.shortcutStore) |
| if err != nil { |
| fr.Progress.Error(err.Error()) |
| } else { |
| fr.Progress.Finished() |
| } |
| }() |
| |
| if err := fr.Progress.JSON(w); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| // CountHandlerRequest is the JSON format for the countHandler request. |
| type CountHandlerRequest struct { |
| Q string `json:"q"` |
| Begin int `json:"begin"` |
| End int `json:"end"` |
| } |
| |
| // CountHandlerResponse is the JSON format if the countHandler response. |
| type CountHandlerResponse struct { |
| Count int `json:"count"` |
| Paramset paramtools.ReadOnlyParamSet `json:"paramset"` |
| } |
| |
| // countHandler takes the POST'd query and runs that against the current |
| // dataframe and returns how many traces match the query. |
| func (f *Frontend) countHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| |
| var cr CountHandlerRequest |
| if err := json.NewDecoder(r.Body).Decode(&cr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| |
| u, err := url.ParseQuery(cr.Q) |
| if err != nil { |
| httputils.ReportError(w, err, "Invalid URL query.", http.StatusInternalServerError) |
| return |
| } |
| q, err := query.New(u) |
| if err != nil { |
| httputils.ReportError(w, err, "Invalid query.", http.StatusInternalServerError) |
| return |
| } |
| resp := CountHandlerResponse{} |
| fullPS := f.paramsetRefresher.Get() |
| if cr.Q == "" { |
| resp.Count = 0 |
| resp.Paramset = fullPS |
| } else { |
| count, ps, err := f.dfBuilder.PreflightQuery(r.Context(), q, fullPS) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to Preflight the query, too many key-value pairs selected. Limit is 200.", http.StatusBadRequest) |
| return |
| } |
| |
| resp.Count = int(count) |
| resp.Paramset = ps.Freeze() |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| // cidHandler takes the POST'd list of dataframe.ColumnHeaders, and returns a |
| // serialized slice of cid.CommitDetails. |
| func (f *Frontend) cidHandler(w http.ResponseWriter, r *http.Request) { |
| ctx := r.Context() |
| w.Header().Set("Content-Type", "application/json") |
| cids := []types.CommitNumber{} |
| if err := json.NewDecoder(r.Body).Decode(&cids); err != nil { |
| httputils.ReportError(w, err, "Could not decode POST body.", http.StatusInternalServerError) |
| return |
| } |
| resp := make([]perfgit.Commit, len(cids)) |
| resp, err := f.perfGit.CommitSliceFromCommitNumberSlice(ctx, cids) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to lookup all commit ids", http.StatusInternalServerError) |
| return |
| } |
| |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| // ClusterStartResponse is serialized as JSON for the response in |
| // clusterStartHandler. |
| type ClusterStartResponse struct { |
| ID string `json:"id"` |
| } |
| |
| // clusterStartHandler takes a POST'd RegressionDetectionRequest and starts a |
| // long running Go routine to do the actual regression detection. |
| // |
| // The results of the long running process are stored in the |
| // RegressionDetectionProcess.Progress.Results. |
| func (f *Frontend) clusterStartHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| |
| req := regression.NewRegressionDetectionRequest() |
| if err := json.NewDecoder(r.Body).Decode(req); err != nil { |
| httputils.ReportError(w, err, "Could not decode POST body.", http.StatusInternalServerError) |
| return |
| } |
| auditlog.Log(r, "cluster", req) |
| |
| cb := func(_ *regression.RegressionDetectionRequest, clusterResponse []*regression.RegressionDetectionResponse, _ string) { |
| // We don't do GroupBy clustering, so there will only be one clusterResponse. |
| req.Progress.Results(clusterResponse[0]) |
| } |
| f.progressTracker.Add(req.Progress) |
| |
| go func() { |
| err := regression.ProcessRegressions(context.Background(), req, cb, f.perfGit, f.shortcutStore, f.dfBuilder, f.paramsetRefresher.Get(), regression.ExpandBaseAlertByGroupBy) |
| if err != nil { |
| req.Progress.Error(err.Error()) |
| } else { |
| req.Progress.Finished() |
| } |
| }() |
| |
| if err := req.Progress.JSON(w); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| // keysHandler handles the POST requests of a list of keys. |
| // |
| // { |
| // "keys": [ |
| // ",arch=x86,...", |
| // ",arch=x86,...", |
| // ] |
| // } |
| // |
| // And returns the ID of the new shortcut to that list of keys: |
| // |
| // { |
| // "id": 123456, |
| // } |
| func (f *Frontend) keysHandler(w http.ResponseWriter, r *http.Request) { |
| id, err := f.shortcutStore.Insert(r.Context(), r.Body) |
| if err != nil { |
| httputils.ReportError(w, err, "Error inserting shortcut.", http.StatusInternalServerError) |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err := json.NewEncoder(w).Encode(map[string]string{"id": id}); err != nil { |
| sklog.Errorf("Failed to write or encode output: %s", err) |
| } |
| } |
| |
| // gotoHandler handles redirecting from a git hash to either the explore, |
| // clustering, or triage page. |
| // |
| // Sets begin and end to a range of commits on either side of the selected |
| // commit. |
| // |
| // Preserves query parameters that are passed into /g/ and passes them onto the |
| // target URL. |
| func (f *Frontend) gotoHandler(w http.ResponseWriter, r *http.Request) { |
| if r.Method != "GET" { |
| http.Error(w, "Method not allowed.", http.StatusMethodNotAllowed) |
| return |
| } |
| if err := r.ParseForm(); err != nil { |
| httputils.ReportError(w, err, "Could not parse query parameters.", http.StatusInternalServerError) |
| return |
| } |
| ctx := context.Background() |
| query := r.Form |
| hash := mux.Vars(r)["hash"] |
| dest := mux.Vars(r)["dest"] |
| index, err := f.perfGit.CommitNumberFromGitHash(ctx, hash) |
| if err != nil { |
| httputils.ReportError(w, err, "Could not look up git hash.", http.StatusInternalServerError) |
| return |
| } |
| lastIndex, err := f.perfGit.CommitNumberFromTime(ctx, time.Time{}) |
| if err != nil { |
| httputils.ReportError(w, fmt.Errorf("Failed to find last commit"), "Failed to find last commit.", http.StatusInternalServerError) |
| return |
| } |
| |
| delta := config.GotoRange |
| // If redirecting to the Triage page then always show just a single commit. |
| if dest == "t" { |
| delta = 0 |
| } |
| begin := int(index) - delta |
| if begin < 0 { |
| begin = 0 |
| } |
| end := int(index) + delta |
| if end > int(lastIndex) { |
| end = int(lastIndex) |
| } |
| details, err := f.perfGit.CommitSliceFromCommitNumberSlice(ctx, []types.CommitNumber{ |
| types.CommitNumber(begin), |
| types.CommitNumber(end)}) |
| if err != nil { |
| httputils.ReportError(w, err, "Could not convert indices to hashes.", http.StatusInternalServerError) |
| return |
| } |
| // Always back up on second since we had an issue with duplicate times for |
| // commits: skbug.com/10698. |
| beginTime := details[0].Timestamp - 1 |
| endTime := details[1].Timestamp + 1 |
| query.Set("begin", fmt.Sprintf("%d", beginTime)) |
| query.Set("end", fmt.Sprintf("%d", endTime)) |
| |
| if dest == "e" { |
| http.Redirect(w, r, fmt.Sprintf("/e/?%s", query.Encode()), http.StatusFound) |
| } else if dest == "c" { |
| query.Set("offset", fmt.Sprintf("%d", index)) |
| http.Redirect(w, r, fmt.Sprintf("/c/?%s", query.Encode()), http.StatusFound) |
| } else if dest == "t" { |
| query.Set("subset", "all") |
| http.Redirect(w, r, fmt.Sprintf("/t/?%s", query.Encode()), http.StatusFound) |
| } |
| } |
| |
| // TriageRequest is used in triageHandler. |
| type TriageRequest struct { |
| Cid types.CommitNumber `json:"cid"` |
| Alert alerts.Alert `json:"alert"` |
| Triage regression.TriageStatus `json:"triage"` |
| ClusterType string `json:"cluster_type"` |
| } |
| |
| // TriageResponse is used in triageHandler. |
| type TriageResponse struct { |
| Bug string `json:"bug"` // URL to bug reporting page. |
| } |
| |
| // triageHandler takes a POST'd TriageRequest serialized as JSON |
| // and performs the triage. |
| // |
| // If successful it returns a 200, or an HTTP status code of 500 otherwise. |
| func (f *Frontend) triageHandler(w http.ResponseWriter, r *http.Request) { |
| ctx := r.Context() |
| w.Header().Set("Content-Type", "application/json") |
| if login.LoggedInAs(r) == "" { |
| httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to triage.", http.StatusInternalServerError) |
| return |
| } |
| tr := &TriageRequest{} |
| if err := json.NewDecoder(r.Body).Decode(tr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| auditlog.Log(r, "triage", tr) |
| detail, err := f.perfGit.CommitFromCommitNumber(ctx, tr.Cid) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to find CommitID.", http.StatusInternalServerError) |
| return |
| } |
| |
| key := tr.Alert.IDAsString |
| if tr.ClusterType == "low" { |
| err = f.regStore.TriageLow(r.Context(), detail.CommitNumber, key, tr.Triage) |
| } else { |
| err = f.regStore.TriageHigh(r.Context(), detail.CommitNumber, key, tr.Triage) |
| } |
| |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to triage.", http.StatusInternalServerError) |
| return |
| } |
| link := fmt.Sprintf("%s/t/?begin=%d&end=%d&subset=all", r.Header.Get("Origin"), detail.Timestamp, detail.Timestamp+1) |
| |
| resp := &TriageResponse{} |
| |
| if tr.Triage.Status == regression.Negative { |
| cfgs, err := f.configProvider() |
| if err != nil { |
| sklog.Errorf("Failed to load configs looking for BugURITemplate: %s", err) |
| } |
| uritemplate := defaultBugURLTemplate |
| for _, c := range cfgs { |
| if c.IDAsString == tr.Alert.IDAsString { |
| if c.BugURITemplate != "" { |
| uritemplate = c.BugURITemplate |
| } |
| break |
| } |
| } |
| resp.Bug = bug.Expand(uritemplate, link, detail, tr.Triage.Message) |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to write or encode output: %s", err) |
| } |
| } |
| |
| // unixTimestampRangeToCommitNumberRange converts a range of commits given in |
| // Unit timestamps into a range of types.CommitNumbers. |
| // |
| // Note this could return two equal commitNumbers. |
| func (f *Frontend) unixTimestampRangeToCommitNumberRange(ctx context.Context, begin, end int64) (types.CommitNumber, types.CommitNumber, error) { |
| beginCommitNumber, err := f.perfGit.CommitNumberFromTime(ctx, time.Unix(begin, 0)) |
| if err != nil { |
| return types.BadCommitNumber, types.BadCommitNumber, skerr.Fmt("Didn't find any commit for begin: %d", begin) |
| } |
| endCommitNumber, err := f.perfGit.CommitNumberFromTime(ctx, time.Unix(end, 0)) |
| if err != nil { |
| return types.BadCommitNumber, types.BadCommitNumber, skerr.Fmt("Didn't find any commit for end: %d", end) |
| } |
| return beginCommitNumber, endCommitNumber, nil |
| } |
| |
| // regressionCount returns the number of commits that have regressions for alerts |
| // in the given category. The time range of commits is REGRESSION_COUNT_DURATION. |
| func (f *Frontend) regressionCount(ctx context.Context, category string) (int, error) { |
| configs, err := f.configProvider() |
| if err != nil { |
| return 0, err |
| } |
| |
| // Query for Regressions in the range. |
| end := time.Now() |
| |
| begin := end.Add(regressionCountDuration) |
| commitNumberBegin, commitNumberEnd, err := f.unixTimestampRangeToCommitNumberRange(ctx, begin.Unix(), end.Unix()) |
| if err != nil { |
| return 0, err |
| } |
| regMap, err := f.regStore.Range(context.Background(), commitNumberBegin, commitNumberEnd) |
| if err != nil { |
| return 0, err |
| } |
| count := 0 |
| for _, regs := range regMap { |
| for _, cfg := range configs { |
| if reg, ok := regs.ByAlertID[cfg.IDAsString]; ok { |
| if cfg.Category == category && !reg.Triaged() { |
| // If any alert for the commit is in the category and is untriaged then we count that row only once. |
| count += 1 |
| break |
| } |
| } |
| } |
| } |
| return count, nil |
| } |
| |
| // regressionCountHandler returns a JSON object with the number of untriaged |
| // alerts that appear in the REGRESSION_COUNT_DURATION. The category |
| // can be supplied by the 'cat' query parameter and defaults to "". |
| func (f *Frontend) regressionCountHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| category := r.FormValue("cat") |
| |
| count, err := f.regressionCount(r.Context(), category) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to count regressions.", http.StatusInternalServerError) |
| } |
| |
| if err := json.NewEncoder(w).Encode(struct{ Count int }{Count: count}); err != nil { |
| sklog.Errorf("Failed to write or encode output: %s", err) |
| } |
| } |
| |
| // Subset is the Subset of regressions we are querying for. |
| type Subset string |
| |
| const ( |
| SubsetAll Subset = "all" // Include all regressions in a range. |
| SubsetRegressions Subset = "regressions" // Only include regressions in a range that are alerting. |
| SubsetUntriaged Subset = "untriaged" // All untriaged alerting regressions regardless of range. |
| ) |
| |
| var AllRegressionSubset = []Subset{SubsetAll, SubsetRegressions, SubsetUntriaged} |
| |
| // RegressionRangeRequest is used in regressionRangeHandler and is used to query for a range of |
| // of Regressions. |
| // |
| // Begin and End are Unix timestamps in seconds. |
| type RegressionRangeRequest struct { |
| Begin int64 `json:"begin"` |
| End int64 `json:"end"` |
| Subset Subset `json:"subset"` |
| AlertFilter string `json:"alert_filter"` // Can be an alertfilter constant, or a category prefixed with "cat:". |
| } |
| |
| // RegressionRow are all the Regression's for a specific commit. It is used in |
| // RegressionRangeResponse. |
| // |
| // The Columns have the same order as RegressionRangeResponse.Header. |
| type RegressionRow struct { |
| Commit perfgit.Commit `json:"cid"` |
| Columns []*regression.Regression `json:"columns"` |
| } |
| |
| // RegressionRangeResponse is the response from regressionRangeHandler. |
| type RegressionRangeResponse struct { |
| Header []*alerts.Alert `json:"header"` |
| Table []*RegressionRow `json:"table"` |
| Categories []string `json:"categories"` |
| } |
| |
| // regressionRangeHandler accepts a POST'd JSON serialized RegressionRangeRequest |
| // and returns a serialized JSON RegressionRangeResponse: |
| // |
| // { |
| // header: [ "query1", "query2", "query3", ...], |
| // table: [ |
| // { cid: cid1, columns: [ Regression, Regression, Regression, ...], }, |
| // { cid: cid2, columns: [ Regression, null, Regression, ...], }, |
| // { cid: cid3, columns: [ Regression, Regression, Regression, ...], }, |
| // ] |
| // } |
| // |
| // Note that there will be nulls in the columns slice where no Regression have been found. |
| func (f *Frontend) regressionRangeHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| ctx := context.Background() |
| rr := &RegressionRangeRequest{} |
| if err := json.NewDecoder(r.Body).Decode(rr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| commitNumberBegin, commitNumberEnd, err := f.unixTimestampRangeToCommitNumberRange(r.Context(), rr.Begin, rr.End) |
| if err != nil { |
| httputils.ReportError(w, err, "Invalid time range.", http.StatusInternalServerError) |
| return |
| } |
| |
| // Query for Regressions in the range. |
| regMap, err := f.regStore.Range(r.Context(), commitNumberBegin, commitNumberEnd) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to retrieve clusters.", http.StatusInternalServerError) |
| return |
| } |
| |
| headers, err := f.configProvider() |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to retrieve alert configs.", http.StatusInternalServerError) |
| return |
| } |
| |
| // Build the full list of categories. |
| categorySet := util.StringSet{} |
| for _, header := range headers { |
| categorySet[header.Category] = true |
| } |
| |
| // Filter down the alerts according to rr.AlertFilter. |
| if rr.AlertFilter == alertfilter.OWNER { |
| user := login.LoggedInAs(r) |
| filteredHeaders := []*alerts.Alert{} |
| for _, a := range headers { |
| if a.Owner == user { |
| filteredHeaders = append(filteredHeaders, a) |
| } |
| } |
| if len(filteredHeaders) > 0 { |
| headers = filteredHeaders |
| } else { |
| sklog.Infof("User doesn't own any alerts.") |
| } |
| } else if strings.HasPrefix(rr.AlertFilter, "cat:") { |
| selectedCategory := rr.AlertFilter[4:] |
| filteredHeaders := []*alerts.Alert{} |
| for _, a := range headers { |
| if a.Category == selectedCategory { |
| filteredHeaders = append(filteredHeaders, a) |
| } |
| } |
| if len(filteredHeaders) > 0 { |
| headers = filteredHeaders |
| } else { |
| sklog.Infof("No alert in that category: %q", selectedCategory) |
| } |
| } |
| |
| // Get a list of commits for the range. |
| var commits []perfgit.Commit |
| if rr.Subset == SubsetAll { |
| commits, err = f.perfGit.CommitSliceFromTimeRange(r.Context(), time.Unix(rr.Begin, 0), time.Unix(rr.End, 0)) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load git info.", http.StatusInternalServerError) |
| return |
| } |
| } else { |
| // If rr.Subset == UNTRIAGED_QS or FLAGGED_QS then only get the commits that |
| // exactly line up with the regressions in regMap. |
| keys := []types.CommitNumber{} |
| for k := range regMap { |
| keys = append(keys, k) |
| } |
| sort.Slice(keys, func(i, j int) bool { |
| return keys[i] < keys[j] |
| }) |
| commits, err = f.perfGit.CommitSliceFromCommitNumberSlice(ctx, keys) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load git info.", http.StatusInternalServerError) |
| return |
| } |
| |
| } |
| |
| // Reverse the order of the cids, so the latest |
| // commit shows up first in the UI display. |
| revCids := make([]perfgit.Commit, len(commits), len(commits)) |
| for i, c := range commits { |
| revCids[len(commits)-1-i] = c |
| } |
| |
| categories := categorySet.Keys() |
| sort.Strings(categories) |
| |
| // Build the RegressionRangeResponse. |
| ret := RegressionRangeResponse{ |
| Header: headers, |
| Table: []*RegressionRow{}, |
| Categories: categories, |
| } |
| |
| for _, cid := range revCids { |
| row := &RegressionRow{ |
| Commit: cid, |
| Columns: make([]*regression.Regression, len(headers), len(headers)), |
| } |
| count := 0 |
| if r, ok := regMap[cid.CommitNumber]; ok { |
| for i, h := range headers { |
| key := h.IDAsString |
| if reg, ok := r.ByAlertID[key]; ok { |
| if rr.Subset == SubsetUntriaged && reg.Triaged() { |
| continue |
| } |
| row.Columns[i] = reg |
| count += 1 |
| } |
| } |
| } |
| if count == 0 && rr.Subset != SubsetAll { |
| continue |
| } |
| ret.Table = append(ret.Table, row) |
| } |
| if err := json.NewEncoder(w).Encode(ret); err != nil { |
| sklog.Errorf("Failed to write or encode output: %s", err) |
| } |
| } |
| |
| func (f *Frontend) regressionCurrentHandler(w http.ResponseWriter, r *http.Request) { |
| status := []continuous.Current{} |
| for _, c := range f.continuous { |
| status = append(status, c.CurrentStatus()) |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err := json.NewEncoder(w).Encode(status); err != nil { |
| sklog.Errorf("Failed to encode status: %s", err) |
| } |
| } |
| |
| // CommitDetailsRequest is for deserializing incoming POST requests |
| // in detailsHandler. |
| type CommitDetailsRequest struct { |
| CommitNumber types.CommitNumber `json:"cid"` |
| TraceID string `json:"traceid"` |
| } |
| |
| func (f *Frontend) detailsHandler(w http.ResponseWriter, r *http.Request) { |
| includeResults := r.FormValue("results") != "false" |
| w.Header().Set("Content-Type", "application/json") |
| dr := &CommitDetailsRequest{} |
| if err := json.NewDecoder(r.Body).Decode(dr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| |
| var err error |
| name := "" |
| name, err = f.traceStore.GetSource(r.Context(), dr.CommitNumber, dr.TraceID) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to load details", http.StatusInternalServerError) |
| return |
| } |
| |
| sklog.Infof("Full URL to source: %q", name) |
| u, err := url.Parse(name) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to parse source file location.", http.StatusInternalServerError) |
| return |
| } |
| if u.Host == "" || u.Path == "" { |
| httputils.ReportError(w, fmt.Errorf("Invalid source location: %q", name), "Invalid source location.", http.StatusInternalServerError) |
| return |
| } |
| sklog.Infof("Host: %q Path: %q", u.Host, u.Path) |
| reader, err := f.storageClient.Bucket(u.Host).Object(u.Path[1:]).NewReader(context.Background()) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to get reader for source file location", http.StatusInternalServerError) |
| return |
| } |
| defer util.Close(reader) |
| res := map[string]interface{}{} |
| if err := json.NewDecoder(reader).Decode(&res); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON source file", http.StatusInternalServerError) |
| return |
| } |
| if !includeResults { |
| delete(res, "results") |
| } |
| b, err := json.MarshalIndent(res, "", " ") |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to re-encode JSON source file", http.StatusInternalServerError) |
| return |
| } |
| if _, err := w.Write(b); err != nil { |
| sklog.Errorf("Failed to write JSON source file: %s", err) |
| } |
| } |
| |
| // ShiftRequest is a request to find the timestamps of a range of commits. |
| type ShiftRequest struct { |
| // Begin is the commit number at the beginning of the range. |
| Begin types.CommitNumber `json:"begin"` |
| |
| // End is the commit number at the end of the range. |
| End types.CommitNumber `json:"end"` |
| } |
| |
| // ShiftResponse are the timestamps from a ShiftRequest. |
| type ShiftResponse struct { |
| Begin int64 `json:"begin"` // In seconds from the epoch. |
| End int64 `json:"end"` // In seconds from the epoch. |
| } |
| |
| // shiftHandler computes a new begin and end timestamp for a dataframe given |
| // the current begin and end offsets. |
| func (f *Frontend) shiftHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| var sr ShiftRequest |
| if err := json.NewDecoder(r.Body).Decode(&sr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| sklog.Infof("ShiftRequest: %#v", &sr) |
| |
| ctx := r.Context() |
| var begin time.Time |
| var end time.Time |
| var err error |
| |
| commit, err := f.perfGit.CommitFromCommitNumber(ctx, sr.Begin) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to look up begin commit.", http.StatusBadRequest) |
| return |
| } |
| begin = time.Unix(commit.Timestamp, 0) |
| |
| commit, err = f.perfGit.CommitFromCommitNumber(ctx, sr.End) |
| if err != nil { |
| // If sr.End isn't a valid offset then just use the most recent commit. |
| lastCommitNumber, err := f.perfGit.CommitNumberFromTime(ctx, time.Time{}) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to look up last commit.", http.StatusBadRequest) |
| return |
| } |
| commit, err = f.perfGit.CommitFromCommitNumber(ctx, lastCommitNumber) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to look up end commit.", http.StatusBadRequest) |
| return |
| } |
| } |
| end = time.Unix(commit.Timestamp, 0) |
| |
| resp := ShiftResponse{ |
| Begin: begin.Unix(), |
| End: end.Unix(), |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to write JSON response: %s", err) |
| } |
| } |
| |
| func (f *Frontend) alertListHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| show := mux.Vars(r)["show"] |
| resp, err := f.alertStore.List(r.Context(), show == "true") |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to retrieve alert configs.", http.StatusInternalServerError) |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to write JSON response: %s", err) |
| } |
| } |
| |
| func alertNewHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| if err := json.NewEncoder(w).Encode(alerts.NewConfig()); err != nil { |
| sklog.Errorf("Failed to write JSON response: %s", err) |
| } |
| } |
| |
| // AlertUpdateResponse is the JSON response when an Alert is created or udpated. |
| type AlertUpdateResponse struct { |
| IDAsString string |
| } |
| |
| func (f *Frontend) alertUpdateHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| if login.LoggedInAs(r) == "" { |
| httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to edit alerts.", http.StatusInternalServerError) |
| return |
| } |
| |
| cfg := &alerts.Alert{} |
| if err := json.NewDecoder(r.Body).Decode(cfg); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| auditlog.Log(r, "alert-update", cfg) |
| if err := f.alertStore.Save(r.Context(), cfg); err != nil { |
| httputils.ReportError(w, err, "Failed to save alerts.Config.", http.StatusInternalServerError) |
| } |
| err := json.NewEncoder(w).Encode(AlertUpdateResponse{ |
| IDAsString: cfg.IDAsString, |
| }) |
| if err != nil { |
| sklog.Errorf("Failed to write JSON response: %s", err) |
| } |
| } |
| |
| func (f *Frontend) alertDeleteHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| if login.LoggedInAs(r) == "" { |
| httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to delete alerts.", http.StatusInternalServerError) |
| return |
| } |
| |
| sid := mux.Vars(r)["id"] |
| id, err := strconv.ParseInt(sid, 10, 64) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to parse alert id.", http.StatusInternalServerError) |
| } |
| auditlog.Log(r, "alert-delete", sid) |
| if err := f.alertStore.Delete(r.Context(), int(id)); err != nil { |
| httputils.ReportError(w, err, "Failed to delete the alerts.Config.", http.StatusInternalServerError) |
| return |
| } |
| } |
| |
| // TryBugRequest is a request to try a bug template URI. |
| type TryBugRequest struct { |
| BugURITemplate string `json:"bug_uri_template"` |
| } |
| |
| // TryBugResponse is response to a TryBugRequest. |
| type TryBugResponse struct { |
| URL string `json:"url"` |
| } |
| |
| func alertBugTryHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| if login.LoggedInAs(r) == "" { |
| httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to test alerts.", http.StatusInternalServerError) |
| return |
| } |
| |
| req := &TryBugRequest{} |
| if err := json.NewDecoder(r.Body).Decode(req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| auditlog.Log(r, "alert-bug-try", req) |
| resp := &TryBugResponse{ |
| URL: bug.ExampleExpand(req.BugURITemplate), |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| sklog.Errorf("Failed to encode response: %s", err) |
| } |
| } |
| |
| func (f *Frontend) alertNotifyTryHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| if login.LoggedInAs(r) == "" { |
| httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to try alerts.", http.StatusInternalServerError) |
| return |
| } |
| |
| req := &alerts.Alert{} |
| if err := json.NewDecoder(r.Body).Decode(req); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| auditlog.Log(r, "alert-notify-try", req) |
| if err := f.notifier.ExampleSend(req); err != nil { |
| httputils.ReportError(w, err, fmt.Sprintf("Failed to send email: %s", err), http.StatusInternalServerError) |
| } |
| } |
| |
| func (f *Frontend) makeDistHandler() func(http.ResponseWriter, *http.Request) { |
| fileServer := http.StripPrefix("/dist", http.FileServer(f.distFileSystem)) |
| return func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Add("Cache-Control", "max-age=300") |
| fileServer.ServeHTTP(w, r) |
| } |
| } |
| |
| func oldMainHandler(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/e/", http.StatusMovedPermanently) |
| } |
| |
| func oldClustersHandler(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/c/", http.StatusMovedPermanently) |
| } |
| |
| func oldAlertsHandler(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/t/", http.StatusMovedPermanently) |
| } |
| |
| var internalOnlyExceptions = []string{ |
| "/oauth2callback/", |
| "/_/reg/count", |
| } |
| |
| // internalOnlyHandler wraps the handler with a handler that only allows |
| // authenticated access, with the exception of the endpoints listed in |
| // internalOnlyExceptions. |
| func internalOnlyHandler(h http.Handler) http.Handler { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| if util.In(r.URL.Path, internalOnlyExceptions) || login.LoggedInAs(r) != "" { |
| h.ServeHTTP(w, r) |
| } else { |
| http.Redirect(w, r, login.LoginURL(w, r), http.StatusTemporaryRedirect) |
| } |
| }) |
| } |
| |
| // Serve content on the configured endpoints.Serve. |
| // |
| // This method does not return. |
| func (f *Frontend) Serve() { |
| // Start the internal server on the internal port if requested. |
| if f.flags.InternalPort != "" { |
| // Add the profiling endpoints to the internal router. |
| internalRouter := mux.NewRouter() |
| |
| // Register pprof handlers |
| internalRouter.HandleFunc("/debug/pprof/", pprof.Index) |
| internalRouter.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) |
| internalRouter.HandleFunc("/debug/pprof/symbol", pprof.Symbol) |
| internalRouter.HandleFunc("/debug/pprof/profile", pprof.Profile) |
| internalRouter.HandleFunc("/debug/pprof/trace", pprof.Trace) |
| internalRouter.HandleFunc("/debug/pprof/{profile}", pprof.Index) |
| |
| go func() { |
| sklog.Infof("Internal server on %q", f.flags.InternalPort) |
| sklog.Info(http.ListenAndServe(f.flags.InternalPort, internalRouter)) |
| }() |
| } |
| |
| // Resources are served directly. |
| router := mux.NewRouter() |
| |
| router.PathPrefix("/dist/").HandlerFunc(f.makeDistHandler()) |
| |
| // Redirects for the old Perf URLs. |
| router.HandleFunc("/", oldMainHandler) |
| router.HandleFunc("/clusters/", oldClustersHandler) |
| router.HandleFunc("/alerts/", oldAlertsHandler) |
| |
| // New endpoints that use ptracestore will go here. |
| router.HandleFunc("/e/", f.templateHandler("newindex.html")) |
| router.HandleFunc("/c/", f.templateHandler("clusters2.html")) |
| router.HandleFunc("/t/", f.templateHandler("triage.html")) |
| router.HandleFunc("/a/", f.templateHandler("alerts.html")) |
| router.HandleFunc("/d/", f.templateHandler("dryRunAlert.html")) |
| router.HandleFunc("/r/", f.templateHandler("trybot.html")) |
| router.HandleFunc("/g/{dest:[ect]}/{hash:[a-zA-Z0-9]+}", f.gotoHandler) |
| router.HandleFunc("/help/", f.helpHandler) |
| router.HandleFunc("/logout/", login.LogoutHandler) |
| router.HandleFunc("/loginstatus/", login.StatusHandler) |
| router.HandleFunc("/oauth2callback/", login.OAuth2CallbackHandler) |
| |
| // JSON handlers. |
| |
| // Common endpoint for all long-running requests. |
| router.HandleFunc("/_/status/{id:[a-zA-Z0-9-]+}", f.progressTracker.Handler).Methods("GET") |
| |
| router.HandleFunc("/_/initpage/", f.initpageHandler) |
| router.HandleFunc("/_/cidRange/", f.cidRangeHandler).Methods("POST") |
| router.HandleFunc("/_/count/", f.countHandler).Methods("POST") |
| router.HandleFunc("/_/cid/", f.cidHandler).Methods("POST") |
| router.HandleFunc("/_/keys/", f.keysHandler).Methods("POST") |
| |
| router.HandleFunc("/_/frame/start", f.frameStartHandler).Methods("POST") |
| router.HandleFunc("/_/cluster/start", f.clusterStartHandler).Methods("POST") |
| router.HandleFunc("/_/trybot/load/", f.trybotLoadHandler).Methods("POST") |
| router.HandleFunc("/_/dryrun/start", f.dryrunRequests.StartHandler).Methods("POST") |
| |
| router.HandleFunc("/_/reg/", f.regressionRangeHandler).Methods("POST") |
| router.HandleFunc("/_/reg/count", f.regressionCountHandler).Methods("GET") |
| router.HandleFunc("/_/reg/current", f.regressionCurrentHandler).Methods("GET") |
| router.HandleFunc("/_/triage/", f.triageHandler).Methods("POST") |
| router.HandleFunc("/_/alerts/", f.alertsHandler) |
| router.HandleFunc("/_/details/", f.detailsHandler).Methods("POST") |
| router.HandleFunc("/_/shift/", f.shiftHandler).Methods("POST") |
| router.HandleFunc("/_/alert/list/{show}", f.alertListHandler).Methods("GET") |
| router.HandleFunc("/_/alert/new", alertNewHandler).Methods("GET") |
| router.HandleFunc("/_/alert/update", f.alertUpdateHandler).Methods("POST") |
| router.HandleFunc("/_/alert/delete/{id:[0-9]+}", f.alertDeleteHandler).Methods("POST") |
| router.HandleFunc("/_/alert/bug/try", alertBugTryHandler).Methods("POST") |
| router.HandleFunc("/_/alert/notify/try", f.alertNotifyTryHandler).Methods("POST") |
| |
| var h http.Handler = router |
| if f.flags.InternalOnly { |
| h = internalOnlyHandler(h) |
| } |
| h = httputils.LoggingGzipRequestResponse(h) |
| if !f.flags.Local { |
| h = httputils.HealthzAndHTTPS(h) |
| } |
| http.Handle("/", h) |
| |
| sklog.Info("Ready to serve.") |
| |
| // We create our own server here instead of using http.ListenAndServe, so |
| // that we don't expose the /debug/pprof endpoints to the open web. |
| server := &http.Server{ |
| Addr: f.flags.Port, |
| Handler: h, |
| } |
| sklog.Fatal(server.ListenAndServe()) |
| } |