mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-11-12 00:48:08 +00:00
* feat(db): five pgx pools with fixed session settings and Prometheus tracing * feat(guards): per-family observe/enforce guards with client slots, deadlines and a shadow budget * fix(guards): attribute only guard deadlines and classify like the handlers * feat(db): route API statements by sqlc name; class C always unnamed * feat(admin): statement statistics by sqlc name; create pg_stat_statements where allowed * fix(db): skip pg_stat_statements when no schema is creatable * feat(background): index health against post-rebuild baselines, Postgres activity, job and ingest metrics * feat: separate API, ingest and maintenance stores; guards on the public API; metrics listener * fix(db): keep ingest writes waiting, size the ingest pool to the host, tidy * test(db): give the statements probe a query id of its own
112 lines
3.4 KiB
Go
112 lines
3.4 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package db
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"slices"
|
|
|
|
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
// unnamedStatements labels the statements that carry no sqlc name.
|
|
const unnamedStatements = "(unnamed)"
|
|
|
|
type statementsQuerier interface {
|
|
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
|
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
|
|
}
|
|
|
|
// DBStatements sums pg_stat_statements for the current database by sqlc statement name.
|
|
// It needs a dedicated pool, so call it on the maintenance store.
|
|
func (s *Store) DBStatements(ctx context.Context) ([]api.DBStatement, error) {
|
|
if s.pool == nil {
|
|
return nil, errors.New("statement statistics need a dedicated pool")
|
|
}
|
|
return dbStatements(ctx, s.pool)
|
|
}
|
|
|
|
func dbStatements(ctx context.Context, q statementsQuerier) ([]api.DBStatement, error) {
|
|
// The view lives in the extension's schema, which need not be on this session's search_path.
|
|
var schema string
|
|
err := q.QueryRow(ctx, "SELECT extnamespace::regnamespace::text FROM pg_extension WHERE extname = 'pg_stat_statements'").Scan(&schema)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, api.ErrStatementsUnavailable
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("find pg_stat_statements: %w", err)
|
|
}
|
|
// regnamespace::text is already quoted where the name needs it.
|
|
rows, err := q.Query(ctx, `-- name: ListDBStatements :many
|
|
SELECT query, calls, total_exec_time, total_plan_time, shared_blks_hit, shared_blks_read, rows, wal_bytes::bigint
|
|
FROM `+schema+`.pg_stat_statements
|
|
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())`)
|
|
if err != nil {
|
|
return nil, statementsError(err)
|
|
}
|
|
defer rows.Close()
|
|
byName := map[string]*api.DBStatement{}
|
|
for rows.Next() {
|
|
var (
|
|
query string
|
|
execMs, planMs float64
|
|
r api.DBStatement
|
|
)
|
|
if err := rows.Scan(&query, &r.Calls, &execMs, &planMs, &r.SharedHit, &r.SharedRead, &r.Rows, &r.WALBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
name := sqlcName(query)
|
|
if name == "" {
|
|
name = unnamedStatements
|
|
}
|
|
sum := byName[name]
|
|
if sum == nil {
|
|
sum = &api.DBStatement{Name: name}
|
|
byName[name] = sum
|
|
}
|
|
sum.Calls += r.Calls
|
|
sum.ExecMs += execMs
|
|
sum.PlanMs += planMs
|
|
sum.SharedHit += r.SharedHit
|
|
sum.SharedRead += r.SharedRead
|
|
sum.Rows += r.Rows
|
|
sum.WALBytes += r.WALBytes
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, statementsError(err)
|
|
}
|
|
items := make([]api.DBStatement, 0, len(byName))
|
|
for _, sum := range byName {
|
|
sum.ExecMs = roundMicros(sum.ExecMs)
|
|
sum.PlanMs = roundMicros(sum.PlanMs)
|
|
items = append(items, *sum)
|
|
}
|
|
slices.SortFunc(items, func(a, b api.DBStatement) int {
|
|
if c := cmp.Compare(b.ExecMs, a.ExecMs); c != 0 {
|
|
return c
|
|
}
|
|
return cmp.Compare(a.Name, b.Name)
|
|
})
|
|
return items, nil
|
|
}
|
|
|
|
// roundMicros rounds milliseconds to whole microseconds.
|
|
func roundMicros(ms float64) float64 { return math.Round(ms*1000) / 1000 }
|
|
|
|
// statementsError maps a missing view (42P01) and a library that was not preloaded (55000)
|
|
// to api.ErrStatementsUnavailable.
|
|
func statementsError(err error) error {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && (pgErr.Code == "42P01" || pgErr.Code == "55000") {
|
|
return api.ErrStatementsUnavailable
|
|
}
|
|
return fmt.Errorf("read pg_stat_statements: %w", err)
|
|
}
|