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).
54 lines
1.8 KiB
Go
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()
|
|
}
|