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.
155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
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()
|
|
}
|