78 lines
2.2 KiB
Go
78 lines
2.2 KiB
Go
package store
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// A ServerError is one 500 the API answered, kept so it can be read after the
|
||
|
|
// container that produced it has gone.
|
||
|
|
type ServerError struct {
|
||
|
|
ID string
|
||
|
|
At time.Time
|
||
|
|
Method string
|
||
|
|
Path string
|
||
|
|
Status int
|
||
|
|
What string
|
||
|
|
Message string
|
||
|
|
UserID string
|
||
|
|
UserName string
|
||
|
|
RequestID string
|
||
|
|
}
|
||
|
|
|
||
|
|
// RecordError stores one failure.
|
||
|
|
//
|
||
|
|
// Errors here are dropped rather than returned: this is called from the path that
|
||
|
|
// is already reporting a failure to somebody, and a diagnostic that can itself
|
||
|
|
// fail the request would turn one broken screen into two.
|
||
|
|
func (db *DB) RecordError(ctx context.Context, e ServerError) {
|
||
|
|
var user any
|
||
|
|
if e.UserID != "" {
|
||
|
|
user = e.UserID
|
||
|
|
}
|
||
|
|
_, _ = db.ExecContext(ctx,
|
||
|
|
`INSERT INTO error_log (id, at, method, path, status, what, message, user_id, request_id)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
|
|
NewID(), time.Now().Unix(), e.Method, truncate(e.Path, 500), e.Status,
|
||
|
|
truncate(e.What, 200), truncate(e.Message, 2000), user, truncate(e.RequestID, 100))
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListErrors returns the most recent failures, newest first.
|
||
|
|
func (db *DB) ListErrors(ctx context.Context, limit int) ([]ServerError, error) {
|
||
|
|
rows, err := db.QueryContext(ctx, `
|
||
|
|
SELECT e.id, e.at, e.method, e.path, e.status, e.what, e.message,
|
||
|
|
COALESCE(e.user_id, ''), COALESCE(u.name, ''), e.request_id
|
||
|
|
FROM error_log e
|
||
|
|
LEFT JOIN users u ON u.id = e.user_id
|
||
|
|
ORDER BY e.at DESC
|
||
|
|
LIMIT ?`, limit)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
|
||
|
|
out := []ServerError{}
|
||
|
|
for rows.Next() {
|
||
|
|
var e ServerError
|
||
|
|
var at int64
|
||
|
|
if err := rows.Scan(&e.ID, &at, &e.Method, &e.Path, &e.Status, &e.What,
|
||
|
|
&e.Message, &e.UserID, &e.UserName, &e.RequestID); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
e.At = time.Unix(at, 0).UTC()
|
||
|
|
out = append(out, e)
|
||
|
|
}
|
||
|
|
return out, rows.Err()
|
||
|
|
}
|
||
|
|
|
||
|
|
// PurgeOldErrors is run by the nightly sweep. A diagnostic that grows without
|
||
|
|
// limit is a disk that fills for a reason nobody expected.
|
||
|
|
func (db *DB) PurgeOldErrors(ctx context.Context, keep time.Duration) (int64, error) {
|
||
|
|
res, err := db.ExecContext(ctx,
|
||
|
|
`DELETE FROM error_log WHERE at < ?`, time.Now().Add(-keep).Unix())
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
return res.RowsAffected()
|
||
|
|
}
|