pkg/server/meta/accident.go (322 lines of code) (raw):

package meta import ( "context" "encoding/json" "fmt" "io" "log" "log/slog" "net/http" "strconv" "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" ) type accidentResponse struct { ID int64 `json:"id"` Date string `json:"date"` AffectedTest string `json:"affectedTest"` Reason string `json:"reason"` BuildNumber string `json:"buildNumber"` Kind string `json:"kind"` ExternalId string `json:"externalId,omitempty"` Stacktrace string `json:"stacktrace"` UserName string `json:"userName"` } type accidentRequestParams struct { Tests []string `json:"tests"` Interval string `json:"interval"` } type AccidentInsertParams struct { Date string `json:"date"` Test string `json:"affected_test"` Reason string `json:"reason"` BuildNumber string `json:"build_number"` Kind string `json:"kind,omitempty"` ExternalId string `json:"externalId,omitempty"` Stacktrace string `json:"stacktrace"` UserName string `json:"user_name,omitempty"` } type accidentIdParams struct { Id int64 `json:"id"` } func CreateGetAccidentsAroundDateRequestHandler(metaDb *pgxpool.Pool) http.HandlerFunc { type date struct { Date string `json:"date"` } return func(writer http.ResponseWriter, request *http.Request) { body := request.Body all, err := io.ReadAll(body) if err != nil { slog.Error("Cannot read body", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } defer body.Close() var params date if err = json.Unmarshal(all, &params); err != nil { slog.Error("Cannot unmarshal parameters", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } sql := "SELECT id, date, affected_test, reason, build_number, kind, stacktrace, user_name FROM accidents WHERE (LOWER(kind)='regression' or LOWER(kind)='improvement' or LOWER(kind)='investigation') AND date BETWEEN '" + params.Date + "'::date - INTERVAL '1 days' AND '" + params.Date + "'::date + INTERVAL '1 days'" rows, err := metaDb.Query(request.Context(), sql) if err != nil { slog.Error("unable to execute the query", "query", sql, "error", err) writer.WriteHeader(http.StatusInternalServerError) return } defer rows.Close() accidents, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (accidentResponse, error) { var id int64 var date pgtype.Date var affectedTest, reason, buildNumber, kind, stacktrace, userName string err := row.Scan(&id, &date, &affectedTest, &reason, &buildNumber, &kind, &stacktrace, &userName) return accidentResponse{ ID: id, Date: date.Time.String(), AffectedTest: affectedTest, Reason: reason, BuildNumber: buildNumber, Kind: kind, Stacktrace: stacktrace, UserName: userName, }, err }) if err != nil { slog.Error("unable to collect rows", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } jsonBytes, err := json.Marshal(accidents) if err != nil { slog.Error("unable to marshal accidents", "accidents", accidents, "error", err) writer.WriteHeader(http.StatusInternalServerError) return } _, err = writer.Write(jsonBytes) if err != nil { slog.Error("unable to write response", "error", err) writer.WriteHeader(http.StatusInternalServerError) } } } func CreateGetManyAccidentsRequestHandler(metaDb *pgxpool.Pool) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { body := request.Body all, err := io.ReadAll(body) if err != nil { slog.Error("Cannot read body", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } defer body.Close() var params accidentRequestParams if err = json.Unmarshal(all, &params); err != nil { slog.Error("Cannot unmarshal parameters", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } sql := "SELECT id, date, affected_test, reason, build_number, kind, externalId, stacktrace, user_name FROM accidents WHERE date >= CURRENT_DATE - INTERVAL '" + params.Interval + "'" if params.Tests != nil { sql += " and affected_test in (" + stringArrayToSQL(params.Tests) + ") or affected_test = ''" } rows, err := metaDb.Query(request.Context(), sql) if err != nil { slog.Error("unable to execute the query", "query", sql, "error", err) writer.WriteHeader(http.StatusInternalServerError) return } defer rows.Close() if _, err := writer.Write([]byte("[")); err != nil { slog.Error("Failed to write JSON array start", "error", err) return } firstItem := true for rows.Next() { accident, err := getAccidentFromRow(rows) if err != nil { slog.Error("unable to scan row", "error", err) // We've already started sending the response, so we can't change the status code // Best we can do is log the error and stop return } // Add comma separator between items (but not before the first item) if !firstItem { if _, err := writer.Write([]byte(",")); err != nil { slog.Error("Failed to write comma separator", "error", err) return } } else { firstItem = false } // Marshal and write this individual item itemBytes, err := json.Marshal(accident) if err != nil { slog.Error("unable to marshal accident", "error", err) return } if _, err := writer.Write(itemBytes); err != nil { slog.Error("Failed to write item", "error", err) return } // Flush the response writer if it supports flushing if flusher, ok := writer.(http.Flusher); ok { flusher.Flush() } } // Check for errors from iterating over rows if err := rows.Err(); err != nil { slog.Error("Error iterating over rows", "error", err) return } // Close the JSON array if _, err := writer.Write([]byte("]")); err != nil { slog.Error("Failed to write JSON array end", "error", err) return } } } func CreatePostAccidentRequestHandler(metaDb *pgxpool.Pool) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { body := request.Body all, err := io.ReadAll(body) if err != nil { slog.Error("cannot read body", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } defer body.Close() var params AccidentInsertParams if err = json.Unmarshal(all, &params); err != nil { slog.Error("cannot unmarshal parameters", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } var kind string if params.Kind == "" { kind = "regression" } else { kind = params.Kind } var id int idRow := metaDb.QueryRow(request.Context(), "INSERT INTO accidents (date, affected_test, reason, build_number, kind, externalId, stacktrace, user_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id", params.Date, params.Test, params.Reason, params.BuildNumber, kind, params.ExternalId, params.Stacktrace, params.UserName) if err = idRow.Scan(&id); err != nil { if strings.Contains(err.Error(), "unique constraint") { http.Error(writer, "Conflict: Accident already exists", http.StatusConflict) } else { slog.Error("cannot execute insert accidents query", "error", err, "date", params.Date, "affected_test", params.Test, "reason", params.Reason, "build_number", params.BuildNumber, "kind", kind, "externalId", params.ExternalId) writer.WriteHeader(http.StatusInternalServerError) } return } _, err = writer.Write([]byte(strconv.Itoa(id))) if err != nil { slog.Error("cannot write response", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } } } func CreateDeleteAccidentRequestHandler(metaDb *pgxpool.Pool) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { body := request.Body all, err := io.ReadAll(body) if err != nil { slog.Error("cannot read body", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } defer body.Close() var params accidentIdParams if err = json.Unmarshal(all, &params); err != nil { slog.Error("cannot unmarshal parameters", "body", all, "error", err) writer.WriteHeader(http.StatusInternalServerError) return } _, err = metaDb.Exec(request.Context(), "DELETE FROM accidents WHERE id=$1", params.Id) if err != nil { slog.Error("cannot execute query", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } writer.WriteHeader(http.StatusOK) } } func CreateGetAccidentByIdHandler(metaDb *pgxpool.Pool) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { id := request.URL.Query().Get("id") accidentById, err := getAccidentById(request.Context(), metaDb, id) if err != nil { slog.Error("cannot get accident by id", "error", err, "id", id) writer.WriteHeader(http.StatusInternalServerError) return } jsonBytes, err := json.Marshal(accidentById) if err != nil { slog.Error("cannot marshal accident", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } _, err = writer.Write(jsonBytes) if err != nil { slog.Error("cannot write response", "error", err) writer.WriteHeader(http.StatusInternalServerError) return } } } func stringArrayToSQL(input []string) string { var str strings.Builder str.WriteRune('\'') for i, s := range input { // Escape any single quotes in the string escapedStr := strings.ReplaceAll(s, "'", "''") str.WriteString(escapedStr) // Add a separator if it's not the last element if i < len(input)-1 { str.WriteString("','") } } str.WriteRune('\'') return str.String() } func getAccidentFromRow(row pgx.CollectableRow) (accidentResponse, error) { var id int64 var date pgtype.Date var affected_test, reason, build_number, kind, externalId, stacktrace, user_name string err := row.Scan(&id, &date, &affected_test, &reason, &build_number, &kind, &externalId, &stacktrace, &user_name) return accidentResponse{ ID: id, Date: date.Time.String(), AffectedTest: affected_test, Reason: reason, BuildNumber: build_number, Kind: kind, ExternalId: externalId, Stacktrace: stacktrace, UserName: user_name, }, err } func getAccidentById(ctx context.Context, metaDb *pgxpool.Pool, accidentId string) (*accidentResponse, error) { sql := "SELECT id, date, affected_test, reason, build_number, kind, externalId, stacktrace, user_name FROM accidents WHERE id=$1" rows, err := metaDb.Query(ctx, sql, accidentId) if err != nil { log.Println("unable to execute the query", "query", sql, "error", err) return nil, err } defer rows.Close() accidents, err := pgx.CollectRows(rows, getAccidentFromRow) if err != nil { log.Println("unable to collect rows", "error", err) return nil, err } if len(accidents) == 0 { return nil, fmt.Errorf("no accident found with id: %s", accidentId) } return &accidents[0], nil } func updateAccidentReason(ctx context.Context, metaDb *pgxpool.Pool, accident *accidentResponse) error { sql := `UPDATE accidents SET reason = $2 WHERE id = $1` _, err := metaDb.Exec(ctx, sql, accident.ID, accident.Reason, ) if err != nil { log.Println("unable to update the query", "query", sql, "error", err) return err } return nil }