From f548d0671f6f16e0ff21e0fb100cec5fbdc95226 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 12 Jun 2026 13:23:10 +0700 Subject: [PATCH] feat(api): add /v1/maintenance policies and run endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenAPI, httpapi CRUD/run/dry-run, job maintenance_policy_run и audit с policy_id. Co-authored-by: Cursor --- docs/openapi.yaml | 296 +++++++++++++++++++++++- internal/httpapi/routes.go | 1 + internal/httpapi/routes_maintenance.go | 270 +++++++++++++++++++++ internal/httpapi/server.go | 10 + internal/jobs/maintenance_worker.go | 63 +++++ internal/jobs/worker.go | 3 + internal/maintenance/scheduler.go | 14 +- internal/pgmonitor/maintenance_audit.go | 11 +- 8 files changed, 653 insertions(+), 15 deletions(-) create mode 100644 internal/httpapi/routes_maintenance.go create mode 100644 internal/jobs/maintenance_worker.go diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 73641c1..5096432 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -51,6 +51,8 @@ tags: description: Сессия текущего API-ключа (tenant и роль). - name: Monitoring description: Наблюдаемость PostgreSQL и корреляция (instance-level, viewer+). Maintenance — operator. + - name: Maintenance + description: Политики обслуживания PostgreSQL (instance-scoped). CRUD и запуск — operator. security: - bearerAuth: [] @@ -966,10 +968,98 @@ components: default: false policy: type: string - description: job_audit_retention | asn_cache_retention + description: Deprecated; use maintenance policies API. limit: type: integer + MaintenancePolicy: + type: object + required: [name, table_name, schedule, vacuum_strategy] + properties: + id: + $ref: "#/components/schemas/ResourceId" + name: + type: string + table_name: + type: string + condition: + type: string + default: "true" + retention_period_sec: + type: integer + minimum: 1 + max_rows: + type: integer + minimum: 1 + maximum: 100000 + vacuum_strategy: + type: string + enum: [none, vacuum, analyze, vacuum_analyze, reindex] + schedule: + type: string + description: Cron expression (5-field, UTC). + enabled: + type: boolean + default: true + dry_run_enabled: + type: boolean + default: false + last_run_at: + type: string + format: date-time + last_status: + type: string + last_error: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + MaintenancePolicyPatch: + type: object + properties: + name: + type: string + table_name: + type: string + condition: + type: string + retention_period_sec: + type: integer + max_rows: + type: integer + vacuum_strategy: + type: string + enum: [none, vacuum, analyze, vacuum_analyze, reindex] + schedule: + type: string + enabled: + type: boolean + dry_run_enabled: + type: boolean + + MaintenanceRunBody: + type: object + required: [policy_id] + properties: + policy_id: + $ref: "#/components/schemas/ResourceId" + + MaintenancePolicyList: + type: object + properties: + items: + type: array + items: + $ref: "#/components/schemas/MaintenancePolicy" + next_cursor: + type: string + has_more: + type: boolean + BirdLocalStatus: type: object description: Статус локального BIRD на хосте API (GET /v1/bird/status). @@ -3467,6 +3557,210 @@ paths: default: $ref: "#/components/responses/DefaultProblem" + /v1/maintenance/policies: + get: + tags: [Maintenance] + summary: List maintenance policies + operationId: listMaintenancePolicies + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenancePolicyList" + default: + $ref: "#/components/responses/DefaultProblem" + post: + tags: [Maintenance] + summary: Create maintenance policy + operationId: createMaintenancePolicy + parameters: + - $ref: "#/components/parameters/TenantId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenancePolicy" + responses: + "201": + description: Создано. + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenancePolicy" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/maintenance/policies/{id}: + get: + tags: [Maintenance] + summary: Get maintenance policy + operationId: getMaintenancePolicy + parameters: + - $ref: "#/components/parameters/TenantId" + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenancePolicy" + default: + $ref: "#/components/responses/DefaultProblem" + patch: + tags: [Maintenance] + summary: Update maintenance policy + operationId: patchMaintenancePolicy + parameters: + - $ref: "#/components/parameters/TenantId" + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenancePolicyPatch" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenancePolicy" + default: + $ref: "#/components/responses/DefaultProblem" + delete: + tags: [Maintenance] + summary: Delete maintenance policy + operationId: deleteMaintenancePolicy + parameters: + - $ref: "#/components/parameters/TenantId" + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + responses: + "204": + description: Удалено. + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/maintenance/policies/{id}/hints: + get: + tags: [Maintenance] + summary: PostgreSQL hints for policy table + operationId: getMaintenancePolicyHints + parameters: + - $ref: "#/components/parameters/TenantId" + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/ResourceId" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + additionalProperties: true + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/maintenance/config-audit: + get: + tags: [Maintenance] + summary: Maintenance policy configuration audit log + operationId: listMaintenanceConfigAudit + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/Cursor" + - $ref: "#/components/parameters/Limit" + responses: + "200": + description: Успешно. + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + additionalProperties: true + next_cursor: + type: string + has_more: + type: boolean + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/maintenance/run: + post: + tags: [Maintenance] + summary: Run maintenance policy (async job) + operationId: postMaintenanceRun + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenanceRunBody" + responses: + "202": + description: Задача поставлена. + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + default: + $ref: "#/components/responses/DefaultProblem" + + /v1/maintenance/dry-run: + post: + tags: [Maintenance] + summary: Dry-run maintenance policy (async job) + operationId: postMaintenanceDryRun + parameters: + - $ref: "#/components/parameters/TenantId" + - $ref: "#/components/parameters/IdempotencyKey" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MaintenanceRunBody" + responses: + "202": + description: Задача поставлена. + content: + application/json: + schema: + $ref: "#/components/schemas/AsyncJobAccepted" + default: + $ref: "#/components/responses/DefaultProblem" + /v1/settings: get: tags: [Settings] diff --git a/internal/httpapi/routes.go b/internal/httpapi/routes.go index 2641718..5e2fb59 100644 --- a/internal/httpapi/routes.go +++ b/internal/httpapi/routes.go @@ -78,6 +78,7 @@ func (s *Server) registerV1(m *http.ServeMux) { s.registerCRUDRoutes(m) s.registerPostgresMonitoringRoutes(m) s.registerPostgresMaintenanceRoutes(m) + s.registerMaintenanceRoutes(m) } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { diff --git a/internal/httpapi/routes_maintenance.go b/internal/httpapi/routes_maintenance.go new file mode 100644 index 0000000..8620075 --- /dev/null +++ b/internal/httpapi/routes_maintenance.go @@ -0,0 +1,270 @@ +package httpapi + +import ( + "encoding/json" + "net/http" + "strings" + + "evobgp/internal/jobs" + "evobgp/internal/store" +) + +func (s *Server) registerMaintenanceRoutes(m *http.ServeMux) { + m.HandleFunc("GET /maintenance/policies", s.handleListMaintenancePolicies) + m.HandleFunc("POST /maintenance/policies", s.handleCreateMaintenancePolicy) + m.HandleFunc("GET /maintenance/policies/{id}", s.handleGetMaintenancePolicy) + m.HandleFunc("PATCH /maintenance/policies/{id}", s.handlePatchMaintenancePolicy) + m.HandleFunc("DELETE /maintenance/policies/{id}", s.handleDeleteMaintenancePolicy) + m.HandleFunc("GET /maintenance/policies/{id}/hints", s.handleMaintenancePolicyHints) + m.HandleFunc("GET /maintenance/config-audit", s.handleListMaintenanceConfigAudit) + m.HandleFunc("POST /maintenance/run", s.handleMaintenanceRun) + m.HandleFunc("POST /maintenance/dry-run", s.handleMaintenanceDryRun) +} + +func maintenancePolicyJSON(p *store.MaintenancePolicy) map[string]any { + if p == nil { + return map[string]any{} + } + out := map[string]any{ + "id": p.ID, + "name": p.Name, + "table_name": p.TableName, + "condition": p.Condition, + "vacuum_strategy": p.VacuumStrategy, + "schedule": p.Schedule, + "enabled": p.Enabled, + "dry_run_enabled": p.DryRunEnabled, + } + if p.RetentionPeriodSec != nil { + out["retention_period_sec"] = *p.RetentionPeriodSec + } + if p.MaxRows != nil { + out["max_rows"] = *p.MaxRows + } + if p.LastRunAt != nil { + out["last_run_at"] = p.LastRunAt.UTC().Format("2006-01-02T15:04:05Z") + } + if p.LastStatus != "" { + out["last_status"] = p.LastStatus + } + if p.LastError != "" { + out["last_error"] = p.LastError + } + if !p.CreatedAt.IsZero() { + out["created_at"] = p.CreatedAt.UTC().Format("2006-01-02T15:04:05Z") + } + if !p.UpdatedAt.IsZero() { + out["updated_at"] = p.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z") + } + return out +} + +func (s *Server) handleListMaintenancePolicies(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requireAtLeast(w, a, "viewer") || !s.requirePostgres(w) { + return + } + cursor := r.URL.Query().Get("cursor") + limit := parseLimitQuery(r, 20, 100) + items, next, hasMore, err := s.store.ListMaintenancePolicies(cursor, limit) + if err != nil { + writeInternalError(w, "maintenance_policies_list", err) + return + } + out := make([]map[string]any, 0, len(items)) + for _, p := range items { + out = append(out, maintenancePolicyJSON(p)) + } + writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore}) +} + +func (s *Server) handleGetMaintenancePolicy(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requireAtLeast(w, a, "viewer") || !s.requirePostgres(w) { + return + } + p, err := s.store.GetMaintenancePolicy(r.PathValue("id")) + if err != nil { + writeStoreErr(w, err) + return + } + writeJSON(w, http.StatusOK, maintenancePolicyJSON(p)) +} + +func (s *Server) handleCreateMaintenancePolicy(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) { + return + } + var body store.MaintenancePolicy + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeProblem(w, http.StatusBadRequest, "Bad Request", invalidInputDetail) + return + } + p, err := s.store.CreateMaintenancePolicy(&body) + if err != nil { + writeStoreErr(w, err) + return + } + _ = s.store.AppendMaintenancePolicyConfigAudit(actorPrefix(a), p.ID, "create", nil, maintenancePolicyJSON(p)) + s.reloadMaintenanceConfig(r) + writeJSON(w, http.StatusCreated, maintenancePolicyJSON(p)) +} + +func (s *Server) handlePatchMaintenancePolicy(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) { + return + } + id := r.PathValue("id") + before, err := s.store.GetMaintenancePolicy(id) + if err != nil { + writeStoreErr(w, err) + return + } + var patch store.MaintenancePolicyPatch + if err := json.NewDecoder(r.Body).Decode(&patch); err != nil { + writeProblem(w, http.StatusBadRequest, "Bad Request", invalidInputDetail) + return + } + updated, err := s.store.UpdateMaintenancePolicy(id, &patch) + if err != nil { + writeStoreErr(w, err) + return + } + _ = s.store.AppendMaintenancePolicyConfigAudit(actorPrefix(a), id, "update", maintenancePolicyJSON(before), maintenancePolicyJSON(updated)) + s.reloadMaintenanceConfig(r) + writeJSON(w, http.StatusOK, maintenancePolicyJSON(updated)) +} + +func (s *Server) handleDeleteMaintenancePolicy(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) { + return + } + id := r.PathValue("id") + before, err := s.store.GetMaintenancePolicy(id) + if err != nil { + writeStoreErr(w, err) + return + } + if err := s.store.DeleteMaintenancePolicy(id); err != nil { + writeStoreErr(w, err) + return + } + _ = s.store.AppendMaintenancePolicyConfigAudit(actorPrefix(a), id, "delete", maintenancePolicyJSON(before), nil) + s.reloadMaintenanceConfig(r) + w.WriteHeader(http.StatusNoContent) +} + +func (s *Server) handleMaintenancePolicyHints(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requireAtLeast(w, a, "viewer") || !s.requirePostgres(w) { + return + } + if s.maintStats == nil { + writeProblem(w, http.StatusServiceUnavailable, "Unavailable", "postgresql backend required") + return + } + p, err := s.store.GetMaintenancePolicy(r.PathValue("id")) + if err != nil { + writeStoreErr(w, err) + return + } + hints, err := s.maintStats.Hints(r.Context(), p.TableName) + if err != nil { + writeInternalError(w, "maintenance_policy_hints", err) + return + } + writeJSON(w, http.StatusOK, hints) +} + +func (s *Server) handleListMaintenanceConfigAudit(w http.ResponseWriter, r *http.Request) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requireAtLeast(w, a, "viewer") || !s.requirePostgres(w) { + return + } + cursor := r.URL.Query().Get("cursor") + limit := parseLimitQuery(r, 20, 100) + items, next, hasMore, err := s.store.ListMaintenancePolicyConfigAudit(cursor, limit) + if err != nil { + writeInternalError(w, "maintenance_config_audit", err) + return + } + out := make([]map[string]any, 0, len(items)) + for _, row := range items { + out = append(out, map[string]any{ + "id": row.ID, + "policy_id": row.PolicyID, + "actor_prefix": row.ActorPrefix, + "action": row.Action, + "before": row.Before, + "after": row.After, + "created_at": row.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"), + }) + } + writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore}) +} + +type maintenanceRunBody struct { + PolicyID string `json:"policy_id"` +} + +func (s *Server) handleMaintenanceRun(w http.ResponseWriter, r *http.Request) { + s.enqueueMaintenancePolicy(w, r, false) +} + +func (s *Server) handleMaintenanceDryRun(w http.ResponseWriter, r *http.Request) { + s.enqueueMaintenancePolicy(w, r, true) +} + +func (s *Server) enqueueMaintenancePolicy(w http.ResponseWriter, r *http.Request, dryRun bool) { + a, ok := authFromContext(r.Context()) + if !ok || !s.requirePostgres(w) || !s.requireOperatorStrict(w, a) { + return + } + var body maintenanceRunBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeProblem(w, http.StatusBadRequest, "Bad Request", invalidInputDetail) + return + } + policyID := strings.TrimSpace(body.PolicyID) + if policyID == "" { + writeProblem(w, http.StatusBadRequest, "Bad Request", "policy_id is required") + return + } + if _, err := s.store.GetMaintenancePolicy(policyID); err != nil { + writeStoreErr(w, err) + return + } + kind := "maintenance_policy_run" + if !s.checkPgMaintRateLimit(a.TenantID, kind+":"+policyID) { + writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "wait before repeating this maintenance operation") + return + } + idem := strings.TrimSpace(r.Header.Get("Idempotency-Key")) + var idemPtr *string + if idem != "" { + idemPtr = &idem + } + title := "Maintenance policy run" + if dryRun { + title = "Maintenance policy dry-run" + } + j, _, err := s.jobs.Enqueue(a.TenantID, jobs.KindMaintenancePolicyRun, idemPtr, nil, map[string]any{ + "policy_id": policyID, "dry_run": dryRun, "actor_prefix": actorPrefix(a), "job_title": title, + }) + if err != nil { + writeInternalError(w, "maintenance_policy_enqueue", err) + return + } + w.Header().Set("Location", "/v1/jobs/"+j.ID) + snap := j.Snapshot() + writeJSON(w, http.StatusAccepted, map[string]any{"job_id": snap["job_id"], "status": snap["status"]}) +} + +func (s *Server) reloadMaintenanceConfig(r *http.Request) { + if s.maintConfig != nil { + _ = s.maintConfig.Reload(r.Context()) + } +} diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index f163150..e30c3b7 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -10,6 +10,7 @@ import ( "strings" "evobgp/internal/jobs" + "evobgp/internal/maintenance" "evobgp/internal/pgmonitor" "evobgp/internal/store" @@ -21,6 +22,8 @@ type Server struct { store store.Backend pgPool *pgxpool.Pool pgMonitor *pgmonitor.Service + maintConfig *maintenance.ConfigProvider + maintStats *maintenance.DBStatsProvider jobs *jobs.Registry bundlePriv ed25519.PrivateKey keyResolver *apiKeyResolver @@ -66,13 +69,20 @@ func New(opts Options) (*Server, error) { return nil, err } var pgMon *pgmonitor.Service + var maintCfg *maintenance.ConfigProvider + var maintStats *maintenance.DBStatsProvider if pool != nil { pgMon = pgmonitor.NewService(pool) + maintCfg = maintenance.NewConfigProvider(backend) + _ = maintCfg.Reload(context.Background()) + maintStats = maintenance.NewDBStatsProvider(pgMon) } s := &Server{ store: backend, pgPool: pool, pgMonitor: pgMon, + maintConfig: maintCfg, + maintStats: maintStats, jobs: reg, bundlePriv: priv, keyResolver: resolver, diff --git a/internal/jobs/maintenance_worker.go b/internal/jobs/maintenance_worker.go new file mode 100644 index 0000000..ae8aa1a --- /dev/null +++ b/internal/jobs/maintenance_worker.go @@ -0,0 +1,63 @@ +package jobs + +import ( + "errors" + "strings" + + "evobgp/internal/maintenance" + "evobgp/internal/pgmonitor" + "evobgp/internal/store" +) + +func (w *Worker) maintenanceExecutor() *maintenance.PolicyExecutor { + if w == nil { + return nil + } + return &maintenance.PolicyExecutor{Store: w.Store, Pool: w.PgPool} +} + +func (w *Worker) runMaintenancePolicy(j *Job) { + if w == nil || w.PgPool == nil { + j.Fail("postgresql not configured") + return + } + policyID, _ := j.Meta["policy_id"].(string) + policyID = strings.TrimSpace(policyID) + if policyID == "" { + j.Fail("missing policy_id in job meta") + return + } + dryRun, _ := j.Meta["dry_run"].(bool) + actor, _ := j.Meta["actor_prefix"].(string) + ctx, cancel := j.workContext() + defer cancel() + + pol, err := w.Store.GetMaintenancePolicy(policyID) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + j.Fail("maintenance policy not found") + return + } + j.Fail(err.Error()) + return + } + + auditID, _ := pgmonitor.InsertMaintenanceAuditWithPolicy(ctx, w.PgPool, j.TenantID, actor, "maintenance_policy_run", pol.TableName, policyID, dryRun) + exec := w.maintenanceExecutor() + detail, err := exec.Execute(ctx, pol, dryRun) + var errMsg *string + status := StatusSucceeded + if err != nil { + s := err.Error() + errMsg = &s + status = StatusFailed + _ = w.Store.TouchMaintenancePolicyRun(policyID, status, s) + j.Fail(s) + } else { + j.mergeMeta(map[string]any{"maintenance": detail, "audit_id": auditID, "policy_id": policyID}) + j.Succeed() + } + if auditID != "" { + _ = pgmonitor.FinishMaintenanceAudit(ctx, w.PgPool, auditID, status, detail, errMsg) + } +} diff --git a/internal/jobs/worker.go b/internal/jobs/worker.go index fcf0790..a06f5c3 100644 --- a/internal/jobs/worker.go +++ b/internal/jobs/worker.go @@ -61,6 +61,7 @@ const ( KindPostgresAnalyze = "postgres_analyze" KindPostgresReindex = "postgres_reindex" KindPostgresCleanup = "postgres_cleanup" + KindMaintenancePolicyRun = "maintenance_policy_run" ) // Worker executes queued jobs against store.Backend (memory or SQL). @@ -188,6 +189,8 @@ func (w *Worker) Process(j *Job) { w.runPostgresMaint(j, "reindex") case KindPostgresCleanup: w.runPostgresCleanup(j) + case KindMaintenancePolicyRun: + w.runMaintenancePolicy(j) default: j.Fail("unknown job kind") } diff --git a/internal/maintenance/scheduler.go b/internal/maintenance/scheduler.go index 4e4d34d..01f3947 100644 --- a/internal/maintenance/scheduler.go +++ b/internal/maintenance/scheduler.go @@ -8,14 +8,12 @@ import ( "sync" "time" - "evobgp/internal/jobs" - "github.com/robfig/cron/v3" ) // StartScheduler enqueues maintenance_policy_run jobs when cron schedules match. -func StartScheduler(ctx context.Context, provider *ConfigProvider, reg *jobs.Registry, tick time.Duration) { - if provider == nil || reg == nil { +func StartScheduler(ctx context.Context, provider *ConfigProvider, enqueue func(policyID string, dryRun bool, idempotencyKey string), tick time.Duration) { + if provider == nil || enqueue == nil { return } if tick <= 0 { @@ -76,14 +74,8 @@ func StartScheduler(ctx context.Context, provider *ConfigProvider, reg *jobs.Reg continue } lastFired[p.ID] = next - dry := p.DryRunEnabled idem := fmt.Sprintf("maint-%s-%d", p.ID, slot) - key := idem - _, _, _ = reg.Enqueue("", "maintenance_policy_run", &key, nil, map[string]any{ - "policy_id": p.ID, - "dry_run": dry, - "trigger": "scheduler", - }) + enqueue(p.ID, p.DryRunEnabled, idem) } mu.Unlock() } diff --git a/internal/pgmonitor/maintenance_audit.go b/internal/pgmonitor/maintenance_audit.go index 252c3c6..0f32175 100644 --- a/internal/pgmonitor/maintenance_audit.go +++ b/internal/pgmonitor/maintenance_audit.go @@ -82,12 +82,17 @@ func RunCleanup(ctx context.Context, pool *pgxpool.Pool, policy string, dryRun b // 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) { + return InsertMaintenanceAuditWithPolicy(ctx, pool, tenantID, actorPrefix, kind, table, "", dryRun) +} + +// InsertMaintenanceAuditWithPolicy records an audit row linked to maintenance_policy. +func InsertMaintenanceAuditWithPolicy(ctx context.Context, pool *pgxpool.Pool, tenantID, actorPrefix, kind, table, policyID 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) + (id, tenant_id, actor_prefix, kind, target_table, policy_id, dry_run, status, created_at) + VALUES ($1, NULLIF($2,''), NULLIF($3,''), $4, NULLIF($5,''), NULLIF($6,''), $7, 'running', now())`, + id, tenantID, actorPrefix, kind, table, policyID, dryRun) return id, err }