feat(db): implement PostgreSQL monitoring and maintenance features
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 33s
CI / go (push) Successful in 2m11s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m27s
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 33s
CI / go (push) Successful in 2m11s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 3m27s
Added PostgreSQL monitoring and maintenance capabilities to the API, including new endpoints for instance-level metrics, maintenance operations, and job scheduling. Updated the HTTP API to support PostgreSQL monitoring routes and integrated a background scheduler for metrics collection. Enhanced the CLI with database commands for maintenance tasks. Updated documentation to reflect these changes.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package pgmonitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// CleanupPolicy names safe retention policies.
|
||||
type CleanupPolicy string
|
||||
|
||||
const (
|
||||
PolicyJobAuditRetention CleanupPolicy = "job_audit_retention"
|
||||
PolicyASNCacheRetention CleanupPolicy = "asn_cache_retention"
|
||||
)
|
||||
|
||||
// CleanupRequest for POST /postgres/cleanup.
|
||||
type CleanupRequest struct {
|
||||
Policy string `json:"policy"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// RunCleanup executes a named retention policy.
|
||||
func RunCleanup(ctx context.Context, pool *pgxpool.Pool, policy string, dryRun bool, limit int) (map[string]any, error) {
|
||||
if pool == nil {
|
||||
return nil, fmt.Errorf("pgmonitor: postgres not configured")
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 10000
|
||||
}
|
||||
if limit > 100000 {
|
||||
limit = 100000
|
||||
}
|
||||
detail := map[string]any{"policy": policy, "dry_run": dryRun, "limit": limit}
|
||||
switch CleanupPolicy(policy) {
|
||||
case PolicyJobAuditRetention:
|
||||
cutoff := time.Now().UTC().Add(-90 * 24 * time.Hour)
|
||||
if dryRun {
|
||||
var n int64
|
||||
err := pool.QueryRow(ctx, `
|
||||
SELECT count(*) FROM job_audit
|
||||
WHERE created_at < $1 AND status IN ('succeeded', 'failed', 'cancelled')`, cutoff).Scan(&n)
|
||||
detail["would_delete"] = n
|
||||
return detail, err
|
||||
}
|
||||
tag, err := pool.Exec(ctx, `
|
||||
DELETE FROM job_audit
|
||||
WHERE id IN (
|
||||
SELECT id FROM job_audit
|
||||
WHERE created_at < $1 AND status IN ('succeeded', 'failed', 'cancelled')
|
||||
LIMIT $2
|
||||
)`, cutoff, limit)
|
||||
if err != nil {
|
||||
return detail, err
|
||||
}
|
||||
detail["deleted"] = tag.RowsAffected()
|
||||
return detail, nil
|
||||
case PolicyASNCacheRetention:
|
||||
cutoff := time.Now().UTC().Add(-7 * 24 * time.Hour)
|
||||
if dryRun {
|
||||
var n int64
|
||||
err := pool.QueryRow(ctx, `SELECT count(*) FROM asn_prefix_cache WHERE fetched_at < $1`, cutoff).Scan(&n)
|
||||
detail["would_delete"] = n
|
||||
return detail, err
|
||||
}
|
||||
tag, err := pool.Exec(ctx, `
|
||||
DELETE FROM asn_prefix_cache WHERE fetched_at < $1`, cutoff)
|
||||
if err != nil {
|
||||
return detail, err
|
||||
}
|
||||
detail["deleted"] = tag.RowsAffected()
|
||||
return detail, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("pgmonitor: unknown cleanup policy %q", policy)
|
||||
}
|
||||
}
|
||||
|
||||
// InsertMaintenanceAudit records an audit row at job start.
|
||||
func InsertMaintenanceAudit(ctx context.Context, pool *pgxpool.Pool, tenantID, actorPrefix, kind, table string, dryRun bool) (string, error) {
|
||||
id := uuid.New().String()
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO postgres_maintenance_audit
|
||||
(id, tenant_id, actor_prefix, kind, target_table, dry_run, status, created_at)
|
||||
VALUES ($1, NULLIF($2,''), NULLIF($3,''), $4, NULLIF($5,''), $6, 'running', now())`,
|
||||
id, tenantID, actorPrefix, kind, table, dryRun)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// FinishMaintenanceAudit updates terminal state.
|
||||
func FinishMaintenanceAudit(ctx context.Context, pool *pgxpool.Pool, id, status string, detail map[string]any, errMsg *string) error {
|
||||
var detailJSON []byte
|
||||
if detail != nil {
|
||||
detailJSON, _ = json.Marshal(detail)
|
||||
}
|
||||
_, err := pool.Exec(ctx, `
|
||||
UPDATE postgres_maintenance_audit
|
||||
SET status = $2, detail_json = $3::jsonb, error_message = $4,
|
||||
finished_at = now(), started_at = COALESCE(started_at, now())
|
||||
WHERE id = $1`,
|
||||
id, status, string(detailJSON), errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListMaintenanceLogs returns paginated audit rows.
|
||||
func ListMaintenanceLogs(ctx context.Context, pool *pgxpool.Pool, cursor string, limit int) ([]MaintenanceLogRow, string, bool, error) {
|
||||
limit = clampLimit(limit, 20, 100)
|
||||
args := []any{limit + 1}
|
||||
q := `
|
||||
SELECT id, COALESCE(tenant_id,''), COALESCE(actor_prefix,''), kind,
|
||||
COALESCE(target_table,''), dry_run, status,
|
||||
detail_json, COALESCE(error_message,''), created_at, started_at, finished_at
|
||||
FROM postgres_maintenance_audit`
|
||||
if cursor != "" {
|
||||
q += ` WHERE created_at < (SELECT created_at FROM postgres_maintenance_audit WHERE id = $2)`
|
||||
args = append(args, cursor)
|
||||
}
|
||||
q += ` ORDER BY created_at DESC LIMIT $1`
|
||||
|
||||
rows, err := pool.Query(ctx, q, args...)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []MaintenanceLogRow
|
||||
for rows.Next() {
|
||||
var r MaintenanceLogRow
|
||||
var detailRaw []byte
|
||||
var started, finished *time.Time
|
||||
if err := rows.Scan(&r.ID, &r.TenantID, &r.ActorPrefix, &r.Kind, &r.TargetTable,
|
||||
&r.DryRun, &r.Status, &detailRaw, &r.Error, &r.CreatedAt, &started, &finished); err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
r.StartedAt = started
|
||||
r.FinishedAt = finished
|
||||
if len(detailRaw) > 0 {
|
||||
_ = json.Unmarshal(detailRaw, &r.Detail)
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
hasMore := len(out) > limit
|
||||
if hasMore {
|
||||
out = out[:limit]
|
||||
}
|
||||
next := ""
|
||||
if hasMore && len(out) > 0 {
|
||||
next = out[len(out)-1].ID
|
||||
}
|
||||
return out, next, hasMore, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user