| package api |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "net/url" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/go-chi/chi/v5" |
| "go.opencensus.io/trace" |
| "go.skia.org/infra/go/alogin" |
| "go.skia.org/infra/go/auditlog" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/roles" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "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/chromeperf" |
| "go.skia.org/infra/perf/go/config" |
| "go.skia.org/infra/perf/go/dataframe" |
| perfgit "go.skia.org/infra/perf/go/git" |
| "go.skia.org/infra/perf/go/git/provider" |
| "go.skia.org/infra/perf/go/graphsshortcut" |
| "go.skia.org/infra/perf/go/notifytypes" |
| "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/shortcut" |
| "go.skia.org/infra/perf/go/types" |
| "go.skia.org/infra/perf/go/urlprovider" |
| ) |
| |
| 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" |
| |
| // 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" |
| ) |
| |
| // NewRegressionsApi returns a new instance of regressionsApi. |
| func NewRegressionsApi(loginProvider alogin.Login, configProvider alerts.ConfigProvider, alertStore alerts.Store, regStore regression.Store, perfGit perfgit.Git, anomalyApiClient chromeperf.AnomalyApiClient, urlProvider *urlprovider.URLProvider, graphsShortcutStore graphsshortcut.Store, alertGroupClient chromeperf.AlertGroupApiClient, progressTracker progress.Tracker, shortcutStore shortcut.Store, dfBuilder dataframe.DataFrameBuilder, paramsetRefresher psrefresh.ParamSetRefresher) regressionsApi { |
| return regressionsApi{ |
| loginProvider: loginProvider, |
| configProvider: configProvider, |
| alertStore: alertStore, |
| regStore: regStore, |
| perfGit: perfGit, |
| alertGroupClient: alertGroupClient, |
| anomalyApiClient: anomalyApiClient, |
| urlProvider: urlProvider, |
| graphsShortcutStore: graphsShortcutStore, |
| progressTracker: progressTracker, |
| shortcutStore: shortcutStore, |
| dfBuilder: dfBuilder, |
| paramsetRefresher: paramsetRefresher, |
| } |
| } |
| |
| // regressionsApi provides a struct to handle regressions related api requests. |
| type regressionsApi struct { |
| loginProvider alogin.Login |
| configProvider alerts.ConfigProvider |
| alertStore alerts.Store |
| regStore regression.Store |
| perfGit perfgit.Git |
| anomalyApiClient chromeperf.AnomalyApiClient |
| urlProvider *urlprovider.URLProvider |
| graphsShortcutStore graphsshortcut.Store |
| alertGroupClient chromeperf.AlertGroupApiClient |
| progressTracker progress.Tracker |
| shortcutStore shortcut.Store |
| dfBuilder dataframe.DataFrameBuilder |
| paramsetRefresher psrefresh.ParamSetRefresher |
| } |
| |
| // RegisterHandlers registers the api handlers for their respective routes. |
| func (r regressionsApi) RegisterHandlers(router *chi.Mux) { |
| router.HandleFunc("/_/alerts/", r.alertsHandler) |
| router.Post("/_/reg/", r.regressionRangeHandler) |
| router.Get("/_/reg/count", r.regressionCountHandler) |
| router.Get("/_/regressions", r.regressionsHandler) |
| router.Get("/_/alertgroup", r.alertGroupQueryHandler) |
| router.Get("/_/anomaly", r.anomalyHandler) |
| router.Post("/_/triage/", r.triageHandler) |
| router.Post("/_/cluster/start", r.clusterStartHandler) |
| } |
| |
| // 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 provider.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 (rApi regressionsApi) regressionRangeHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| w.Header().Set("Content-Type", "application/json") |
| |
| 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 := rApi.unixTimestampRangeToCommitNumberRange(ctx, 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 := rApi.regStore.Range(ctx, commitNumberBegin, commitNumberEnd) |
| if err != nil { |
| httputils.ReportError(w, err, "Failed to retrieve clusters.", http.StatusInternalServerError) |
| return |
| } |
| |
| headers, err := rApi.configProvider.GetAllAlertConfigs(ctx, false) |
| 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 := rApi.loginProvider.LoggedInAs(r) |
| filteredHeaders := []*alerts.Alert{} |
| for _, a := range headers { |
| if a.Owner == string(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 []provider.Commit |
| if rr.Subset == SubsetAll { |
| commits, err = rApi.perfGit.CommitSliceFromTimeRange(ctx, 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 = rApi.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([]provider.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) |
| } |
| } |
| |
| // 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 (rApi regressionsApi) regressionCountHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| w.Header().Set("Content-Type", "application/json") |
| |
| category := r.FormValue("cat") |
| count, err := rApi.regressionCount(ctx, 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) |
| } |
| } |
| |
| // alertsHandler returns the regression count for the default alert category. |
| func (rApi regressionsApi) alertsHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| w.Header().Set("Content-Type", "application/json") |
| |
| count, err := rApi.regressionCount(ctx, 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) |
| } |
| } |
| |
| // 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 (rApi regressionsApi) regressionCount(ctx context.Context, category string) (int, error) { |
| configs, err := rApi.configProvider.GetAllAlertConfigs(ctx, false) |
| if err != nil { |
| return 0, err |
| } |
| |
| // Query for Regressions in the range. |
| end := time.Now() |
| |
| begin := end.Add(regressionCountDuration) |
| commitNumberBegin, commitNumberEnd, err := rApi.unixTimestampRangeToCommitNumberRange(ctx, begin.Unix(), end.Unix()) |
| if err != nil { |
| return 0, err |
| } |
| regMap, err := rApi.regStore.Range(ctx, 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 |
| } |
| |
| // unixTimestampRangeToCommitNumberRange converts a range of commits given in |
| // Unit timestamps into a range of types.CommitNumbers. |
| // |
| // Note this could return two equal commitNumbers. |
| func (rApi regressionsApi) unixTimestampRangeToCommitNumberRange(ctx context.Context, begin, end int64) (types.CommitNumber, types.CommitNumber, error) { |
| beginCommitNumber, err := rApi.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 := rApi.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 |
| } |
| |
| // regressionsHandler returns a list of regressions for a given subscription. |
| func (rApi regressionsApi) regressionsHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| ctx, span := trace.StartSpan(ctx, "regressionsQueryRequest") |
| defer span.End() |
| |
| sub_name := r.URL.Query().Get("sub_name") |
| limit, err := strconv.Atoi(r.URL.Query().Get("limit")) |
| if err != nil { |
| httputils.ReportError(w, err, "Limit value is not an integer", http.StatusBadRequest) |
| return |
| } |
| offset, err := strconv.Atoi(r.URL.Query().Get("offset")) |
| if err != nil { |
| httputils.ReportError(w, err, "Offset value is not an integer", http.StatusBadRequest) |
| return |
| } |
| |
| regressionsList, err := rApi.regStore.GetRegressionsBySubName(ctx, sub_name, limit, offset) |
| if err != nil { |
| httputils.ReportError(w, err, "Unable to fetch regressions", http.StatusInternalServerError) |
| return |
| } |
| |
| if err := json.NewEncoder(w).Encode(regressionsList); err != nil { |
| sklog.Errorf("Failed to write or encode output: %s", err) |
| } |
| } |
| |
| // anomalyHandler handles the request for the anomaly api. |
| func (rApi regressionsApi) anomalyHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| |
| if rApi.anomalyApiClient == nil { |
| sklog.Info("Anomaly service is not enabled") |
| httputils.ReportError(w, nil, "Anomaly service is not enabled", http.StatusNotFound) |
| return |
| } |
| key := r.URL.Query().Get("key") |
| ctx, span := trace.StartSpan(ctx, "anomalyGetRequest") |
| defer span.End() |
| queryParams, anomaly, err := rApi.anomalyApiClient.GetAnomalyFromUrlSafeKey(ctx, key) |
| if err != nil { |
| httputils.ReportError(w, err, "Error retrieving anomaly data", http.StatusBadRequest) |
| return |
| } |
| |
| // Generate the explore page url for the given params. |
| queryParams["stat"] = []string{"value"} |
| graphs := []graphsshortcut.GraphConfig{} |
| queryString := rApi.urlProvider.GetQueryStringFromParameters(queryParams) |
| |
| // Let's generate the graph config that represents the graph for the queryString. |
| // This is then inserted as a shortcut and we generate the multigraph url with |
| // the created shortcut. |
| graphs = append(graphs, graphsshortcut.GraphConfig{ |
| Queries: []string{queryString}, |
| Formulas: []string{}, |
| }) |
| shortcutObj := graphsshortcut.GraphsShortcut{ |
| Graphs: graphs, |
| } |
| |
| graphQueryParams := getGraphQueryParamsForAnomalyId([]string{strconv.Itoa(anomaly.Id)}) |
| var redirectUrl string |
| shortcutId, err := rApi.graphsShortcutStore.InsertShortcut(ctx, &shortcutObj) |
| if err != nil { |
| // Something went wrong while inserting shortcut. Let's fall back to the explore page. |
| sklog.Errorf("Error inserting shortcut %s", err) |
| redirectUrl = rApi.urlProvider.Explore(ctx, anomaly.StartRevision, anomaly.EndRevision, queryParams, true, graphQueryParams) |
| } else { |
| redirectUrl = rApi.urlProvider.MultiGraph(ctx, anomaly.StartRevision, anomaly.EndRevision, shortcutId, true, graphQueryParams) |
| } |
| |
| sklog.Infof("Generated url: %s", redirectUrl) |
| http.Redirect(w, r, redirectUrl, http.StatusSeeOther) |
| } |
| |
| // alertGroupQueryHandler redirects the user to the relevant plot for the given alert group id. |
| func (rApi regressionsApi) alertGroupQueryHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| |
| if rApi.alertGroupClient == nil { |
| sklog.Info("Alert Grouping is not enabled") |
| httputils.ReportError(w, nil, "Alert Grouping is not enabled", http.StatusNotFound) |
| return |
| } |
| groupId := r.URL.Query().Get("group_id") |
| sklog.Infof("Group id is %s", groupId) |
| ctx, span := trace.StartSpan(ctx, "alertGroupQueryRequest") |
| defer span.End() |
| alertGroupDetails, err := rApi.alertGroupClient.GetAlertGroupDetails(ctx, groupId) |
| if err != nil { |
| sklog.Errorf("Error in retrieving alert group details: %s", err) |
| } |
| |
| if alertGroupDetails != nil { |
| sklog.Infof("Retrieved %d anomalies for alert group id %s", len(alertGroupDetails.Anomalies), groupId) |
| |
| anomalyIds := []string{} |
| for anomalyId := range alertGroupDetails.Anomalies { |
| anomalyIds = append(anomalyIds, anomalyId) |
| } |
| |
| highlightAnomalyParams := getGraphQueryParamsForAnomalyId(anomalyIds) |
| explore := r.URL.Query().Get("e") |
| var redirectUrl string |
| if explore == "" { |
| queryParamsPerTrace := alertGroupDetails.GetQueryParamsPerTrace(ctx) |
| graphs := []graphsshortcut.GraphConfig{} |
| for _, queryParams := range queryParamsPerTrace { |
| queryString := rApi.urlProvider.GetQueryStringFromParameters(queryParams) |
| graphs = append(graphs, graphsshortcut.GraphConfig{ |
| Queries: []string{queryString}, |
| Formulas: []string{}, |
| }) |
| } |
| |
| shortcutObj := graphsshortcut.GraphsShortcut{ |
| Graphs: graphs, |
| } |
| |
| shortcutId, err := rApi.graphsShortcutStore.InsertShortcut(ctx, &shortcutObj) |
| if err != nil { |
| // Something went wrong while inserting shortcut. |
| sklog.Errorf("Error inserting shortcut %s", err) |
| // Let's redirect the user to the explore page instead. |
| queryParams := alertGroupDetails.GetQueryParams(ctx) |
| redirectUrl = rApi.urlProvider.Explore(ctx, int(alertGroupDetails.StartCommitNumber), int(alertGroupDetails.EndCommitNumber), queryParams, false, highlightAnomalyParams) |
| } else { |
| startCommit := alertGroupDetails.StartCommitNumber |
| endCommit := alertGroupDetails.EndCommitNumber |
| if alertGroupDetails.StartCommitHash != "" && alertGroupDetails.EndCommitHash != "" { |
| commitNum, err := rApi.perfGit.CommitNumberFromGitHash(ctx, alertGroupDetails.StartCommitHash) |
| if err != nil { |
| httputils.ReportError(w, err, fmt.Sprintf("Invalid git hash %s received for commit number %d from chromeperf", alertGroupDetails.StartCommitHash, startCommit), http.StatusInternalServerError) |
| return |
| } else { |
| startCommit = int32(commitNum) |
| } |
| |
| commitNum, err = rApi.perfGit.CommitNumberFromGitHash(ctx, alertGroupDetails.EndCommitHash) |
| if err != nil { |
| httputils.ReportError(w, err, fmt.Sprintf("Invalid git hash %s received for commit number %d from chromeperf", alertGroupDetails.EndCommitHash, endCommit), http.StatusInternalServerError) |
| return |
| } else { |
| endCommit = int32(commitNum) |
| } |
| } |
| redirectUrl = rApi.urlProvider.MultiGraph(ctx, int(startCommit), int(endCommit), shortcutId, false, highlightAnomalyParams) |
| } |
| |
| } else { |
| queryParams := alertGroupDetails.GetQueryParams(ctx) |
| redirectUrl = rApi.urlProvider.Explore(ctx, int(alertGroupDetails.StartCommitNumber), int(alertGroupDetails.EndCommitNumber), queryParams, false, highlightAnomalyParams) |
| } |
| sklog.Infof("Generated url: %s", redirectUrl) |
| http.Redirect(w, r, redirectUrl, http.StatusSeeOther) |
| return |
| } |
| } |
| |
| // 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 (rApi regressionsApi) triageHandler(w http.ResponseWriter, r *http.Request) { |
| ctx, cancel := context.WithTimeout(r.Context(), defaultDatabaseTimeout) |
| defer cancel() |
| w.Header().Set("Content-Type", "application/json") |
| |
| tr := &TriageRequest{} |
| if err := json.NewDecoder(r.Body).Decode(tr); err != nil { |
| httputils.ReportError(w, err, "Failed to decode JSON.", http.StatusInternalServerError) |
| return |
| } |
| if !rApi.isEditor(w, r, "triage", tr) { |
| return |
| } |
| detail, err := rApi.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 = rApi.regStore.TriageLow(ctx, detail.CommitNumber, key, tr.Triage) |
| } else { |
| err = rApi.regStore.TriageHigh(ctx, 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 && config.Config.NotifyConfig.Notifications != notifytypes.MarkdownIssueTracker { |
| cfgs, err := rApi.configProvider.GetAllAlertConfigs(ctx, false) |
| 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) |
| } |
| } |
| |
| func (rApi regressionsApi) isEditor(w http.ResponseWriter, r *http.Request, action string, body interface{}) bool { |
| user := rApi.loginProvider.LoggedInAs(r) |
| if !rApi.loginProvider.HasRole(r, roles.Editor) { |
| httputils.ReportError(w, fmt.Errorf("Not logged in."), "You must be logged in to complete this action.", http.StatusUnauthorized) |
| return false |
| } |
| auditlog.LogWithUser(r, user.String(), action, body) |
| return true |
| } |
| |
| // 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 (rApi regressionsApi) 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.LogWithUser(r, rApi.loginProvider.LoggedInAs(r).String(), "cluster", req) |
| |
| cb := func(ctx context.Context, _ *regression.RegressionDetectionRequest, clusterResponse []*regression.RegressionDetectionResponse, _ string) { |
| // We don't do GroupBy clustering, so there will only be one clusterResponse. |
| req.Progress.Results(clusterResponse[0]) |
| } |
| rApi.progressTracker.Add(req.Progress) |
| |
| go func() { |
| // This intentionally does not use r.Context() because we want it to outlive this request. |
| err := regression.ProcessRegressions(context.Background(), req, cb, rApi.perfGit, rApi.shortcutStore, rApi.dfBuilder, rApi.paramsetRefresher.GetAll(), regression.ExpandBaseAlertByGroupBy, regression.ReturnOnError, config.Config.AnomalyConfig) |
| if err != nil { |
| sklog.Errorf("ProcessRegressions returned: %s", err) |
| req.Progress.Error("Failed to load data.") |
| } else { |
| req.Progress.Finished() |
| } |
| }() |
| |
| if err := req.Progress.JSON(w); err != nil { |
| sklog.Errorf("Failed to encode paramset: %s", err) |
| } |
| } |
| |
| func getGraphQueryParamsForAnomalyId(anomalyIds []string) url.Values { |
| return url.Values{ |
| "highlight_anomalies": anomalyIds, |
| } |
| } |