Files
cairnobs/api/logretention/agent_floor.go
T
jcoffey-dev 087c52a64f Scope log retention deletion to selected hosts, not the whole table
api/logretention no longer deletes wholesale by age alone: a new
GET /logs/retention/hosts lists every host with matching records (plus
any configured retention floor), and preview/delete now require an
explicit, non-empty host list -- there is no "omitted host means every
host" shortcut server-side. Store's count/delete statements are
host-scoped (host IN (...)); Handler.partitionHosts checks the floor
per host instead of one global max, so a floor on one host never
blocks acting on other hosts requested in the same call. A request
that ends up fully or partially blocked still returns 200 with
blocked_hosts explaining why, rather than rejecting the whole call.

Settings' Log retention section is a host picker now: checkboxes with
per-host counts and a "protected Nd" badge where a floor applies,
"select all/none", and a confirm panel that names exactly which hosts
will be affected and which were skipped and why.

Verified live against real ClickHouse/Postgres and in-browser: three
hosts seeded, one protected by a 90-day floor -- a scoped delete
correctly removed the two open hosts' records, left the protected
host's untouched, and the response/UI both named it as skipped. Also
fixed a real spacing bug in the result message caught during that
browser pass (an adjacent {expr}{#if} with no source whitespace
between them rendered with no space either).
2026-08-21 15:32:05 -07:00

54 lines
1.8 KiB
Go

package logretention
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
// AgentRetentionStore reads the protective retention floors set on
// agents.ConfigOverride.LogRetentionDays -- a separate, Postgres-backed
// concern from Store's ClickHouse access above, so it lives in its own
// file. Deliberately its own narrow query against the same `agents`
// table api/agents.Store manages, rather than importing api/agents for
// a shared type, matching this codebase's "each package owns direct
// SQL access to what it needs" convention (e.g. alerting and api both
// read dashboards-adjacent tables independently rather than sharing a
// store type across a package boundary).
type AgentRetentionStore struct {
pool *pgxpool.Pool
}
func NewAgentRetentionStore(pool *pgxpool.Pool) *AgentRetentionStore {
return &AgentRetentionStore{pool: pool}
}
// RetentionDaysByHost reports the configured log_retention_days for
// every agent that has one set, keyed by host -- a host absent from
// this map has no configured floor at all. Per-host rather than a
// single global maximum: now that deletion is host-scoped
// (Handler.partitionHosts), a floor on one host must never block
// deleting another host's logs that happen to be requested in the same
// call.
func (s *AgentRetentionStore) RetentionDaysByHost(ctx context.Context) (map[string]int, error) {
rows, err := s.pool.Query(ctx, `
SELECT host, (desired_override->>'log_retention_days')::int
FROM agents
WHERE desired_override->>'log_retention_days' IS NOT NULL`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]int{}
for rows.Next() {
var host string
var days int
if err := rows.Scan(&host, &days); err != nil {
return nil, err
}
out[host] = days
}
return out, rows.Err()
}