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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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
}