mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-04 16:02:03 +00:00
87 lines
2.8 KiB
Go
87 lines
2.8 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/warmbly/warmbly/internal/observability/errs"
|
|
)
|
|
|
|
func CaptureError(err error, query string, params []any, operation string) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
// A cancelled context is the caller going away: a browser navigating off a
|
|
// slow list, a worker shutting down mid-pass, a request that hit its
|
|
// deadline and was already answered. The query did not fail, so reporting
|
|
// it filed the caller's own decision as a database incident and buried the
|
|
// real ones under it. The deadline case is the same event seen from the
|
|
// other side, and the pool reports an in-flight cancellation as a closed
|
|
// connection, which is why that is here too.
|
|
if isCallerGone(err) {
|
|
return
|
|
}
|
|
shape := paramShape(params)
|
|
wrappedErr := fmt.Errorf("%s failed: %w (query: %s, params: %s)", operation, err, query, shape)
|
|
|
|
opts := []errs.Option{
|
|
errs.Tag("db.operation", operation),
|
|
errs.Tag("db.query", query),
|
|
errs.Extra("db.params", shape),
|
|
}
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) {
|
|
opts = append(opts,
|
|
errs.Extra("pg.code", pgErr.Code), // e.g., "23505" for unique violation
|
|
errs.Extra("pg.detail", pgErr.Detail),
|
|
errs.Extra("pg.hint", pgErr.Hint),
|
|
)
|
|
}
|
|
|
|
errs.CaptureException(wrappedErr, opts...)
|
|
}
|
|
|
|
// isCallerGone reports whether an error only means the caller stopped waiting.
|
|
//
|
|
// pgx surfaces a cancellation three ways depending on where it landed: as the
|
|
// context error itself, as a Postgres 57014 (query_canceled) when the server
|
|
// had already started, and as "conn closed" / "failed to deallocate cached
|
|
// statement(s)" when the pool tore the connection down with the statement
|
|
// still on it. None of the three is a fault in the query.
|
|
func isCallerGone(err error) bool {
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return true
|
|
}
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == "57014" {
|
|
return true
|
|
}
|
|
return strings.Contains(err.Error(), "conn closed")
|
|
}
|
|
|
|
// paramShape describes the query's arguments without their values.
|
|
//
|
|
// The values are the row: email addresses, password hashes, tokens, message
|
|
// bodies. The query text next to them already says which column each one is,
|
|
// so reporting them would put the most sensitive data in the system into an
|
|
// error tracker. How many there were and of what type is enough to tell one
|
|
// call site from another, which is all the report is for.
|
|
func paramShape(params []any) string {
|
|
if len(params) == 0 {
|
|
return "none"
|
|
}
|
|
kinds := make([]string, 0, len(params))
|
|
for _, p := range params {
|
|
if p == nil {
|
|
kinds = append(kinds, "nil")
|
|
continue
|
|
}
|
|
kinds = append(kinds, fmt.Sprintf("%T", p))
|
|
}
|
|
return strconv.Itoa(len(params)) + " (" + strings.Join(kinds, ", ") + ")"
|
|
}
|