feat(db): enhance PostgreSQL statistics monitoring and error handling
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 30s
CI / go (push) Failing after 24s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 30s
CI / go (push) Failing after 24s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
Updated the PostgreSQL monitoring service to improve handling of `pg_stat_statements` availability. Introduced a new method to check if the extension is queryable and updated the response structure to include availability status and hints. Enhanced the documentation to clarify the requirements for enabling `pg_stat_statements`. Adjusted related components to reflect these changes, ensuring better user feedback in the monitoring interface.
This commit is contained in:
@@ -106,11 +106,7 @@ func (s *Service) fetchOverview(ctx context.Context) (Overview, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var ext bool
|
||||
_ = s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements')`).Scan(&ext)
|
||||
out.StatementsEnabled = ext
|
||||
out.StatementsEnabled = s.statementsQueryable(ctx)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -185,27 +181,75 @@ func (s *Service) TopQueries(ctx context.Context, limit int) (QueriesResponse, e
|
||||
if snap, ok, err := s.loadSnapshot(ctx, "slow_queries", 15*time.Minute); err == nil && ok {
|
||||
var items []QueryStat
|
||||
if err := decodePayload(snap.Payload, &items); err == nil {
|
||||
return QueriesResponse{CollectedAt: snap.CollectedAt, Source: "snapshot", Items: items}, nil
|
||||
return QueriesResponse{
|
||||
CollectedAt: snap.CollectedAt,
|
||||
Source: "snapshot",
|
||||
Items: items,
|
||||
StatementsAvailable: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !s.statementsEnabled(ctx) {
|
||||
return QueriesResponse{CollectedAt: now, Source: "live", Items: nil}, nil
|
||||
if !s.statementsQueryable(ctx) {
|
||||
return queriesUnavailable(now), nil
|
||||
}
|
||||
items, err := queryTopStatements(ctx, s.pool, limit)
|
||||
if err != nil {
|
||||
if isPgStatStatementsUnavailable(err) {
|
||||
s.markStatementsUnavailable()
|
||||
return queriesUnavailable(now), nil
|
||||
}
|
||||
return QueriesResponse{}, err
|
||||
}
|
||||
return QueriesResponse{CollectedAt: now, Source: "live", Items: items}, nil
|
||||
return QueriesResponse{
|
||||
CollectedAt: now,
|
||||
Source: "live",
|
||||
Items: items,
|
||||
StatementsAvailable: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) statementsEnabled(ctx context.Context) bool {
|
||||
var ok bool
|
||||
_ = s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements')`).Scan(&ok)
|
||||
func queriesUnavailable(at time.Time) QueriesResponse {
|
||||
return QueriesResponse{
|
||||
CollectedAt: at,
|
||||
Source: "unavailable",
|
||||
Items: nil,
|
||||
StatementsAvailable: false,
|
||||
StatementsHint: statementsUnavailableHint,
|
||||
}
|
||||
}
|
||||
|
||||
const statementsUnavailableHint = "pg_stat_statements requires shared_preload_libraries and PostgreSQL restart (see docs/db-diagnostics.md)"
|
||||
|
||||
// statementsQueryable returns true only when pg_stat_statements can be queried (not merely installed).
|
||||
func (s *Service) statementsQueryable(ctx context.Context) bool {
|
||||
if s == nil || s.pool == nil {
|
||||
return false
|
||||
}
|
||||
if v, ok := s.cache.get("stmt_queryable"); ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
ok := probePgStatStatements(ctx, s.pool)
|
||||
s.cache.set("stmt_queryable", ok)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *Service) markStatementsUnavailable() {
|
||||
s.cache.set("stmt_queryable", false)
|
||||
}
|
||||
|
||||
func probePgStatStatements(ctx context.Context, pool *pgxpool.Pool) bool {
|
||||
var dummy int64
|
||||
err := pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(calls), 0)::bigint FROM pg_stat_statements LIMIT 1`).Scan(&dummy)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
return !isPgStatStatementsUnavailable(err)
|
||||
}
|
||||
|
||||
func queryTopStatements(ctx context.Context, pool *pgxpool.Pool, limit int) ([]QueryStat, error) {
|
||||
rows, err := pool.Query(ctx, `
|
||||
SELECT queryid, left(query, 500), calls, total_exec_time, mean_exec_time, rows
|
||||
@@ -214,7 +258,7 @@ func queryTopStatements(ctx context.Context, pool *pgxpool.Pool, limit int) ([]Q
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
if isUndefinedTable(err) {
|
||||
if isPgStatStatementsUnavailable(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("pgmonitor: pg_stat_statements: %w", err)
|
||||
@@ -231,12 +275,24 @@ func queryTopStatements(ctx context.Context, pool *pgxpool.Pool, limit int) ([]Q
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func isUndefinedTable(err error) bool {
|
||||
// isPgStatStatementsUnavailable reports extension missing or not loaded via shared_preload_libraries.
|
||||
func isPgStatStatementsUnavailable(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
return pgErr.Code == "42P01" || pgErr.Code == "42704"
|
||||
switch pgErr.Code {
|
||||
case "42P01", "42704", "55000":
|
||||
return true
|
||||
}
|
||||
msg := strings.ToLower(pgErr.Message)
|
||||
if strings.Contains(msg, "shared_preload_libraries") || strings.Contains(msg, "pg_stat_statements") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return strings.Contains(err.Error(), "pg_stat_statements")
|
||||
low := strings.ToLower(err.Error())
|
||||
return strings.Contains(low, "shared_preload_libraries") || strings.Contains(low, "pg_stat_statements")
|
||||
}
|
||||
|
||||
func isSafeIdent(name string) bool {
|
||||
|
||||
Reference in New Issue
Block a user