Telemetry up-gating hardening (code portion of #32): - Context field whitelist: telemetry `context` is filtered to a small set of non-content diagnostic keys (route/retryable/phase/exit_code/duration_ms/ attempt) before persistence. Unknown keys — including potentially identifying ones (email, full file path, prompt, raw IP) — are dropped, so a client regression cannot land arbitrary JSON in the store. Empty/unparseable/no-allowed-key context is dropped to "". - Per-field size cap: stack_top and context are truncated to 8KiB after redaction (backstop against unbounded blobs within batch limits). - Retention: daily master-only task deletes telemetry rows older than HEICODE_TELEMETRY_RETENTION_DAYS (default 30; <=0 disables). HEICODE_TELEMETRY_RETENTION_INTERVAL_HOURS (default 24) sets cadence. model.DeleteTelemetryEventsBefore(cutoff) + controller.StartTelemetryRetentionTask() wired into main.go under IsMasterNode. - GET /api/heicode/config telemetry block now surfaces retention_days for client/admin transparency. Tests: whitelist drop/keep, size cap, redaction-within-allowed-key. go build/vet clean; controller telemetry tests pass. Affects: Manager only (telemetry ingest + retention). No billing/consume-log change (telemetry still never bills). Privacy-doc disclosure + production enable-checklist portions of #32 tracked in heicodeDocs sync (#34) / desktop client API docs (#35). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
2.5 KiB
Go
78 lines
2.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/model"
|
|
)
|
|
|
|
// Telemetry retention (#32). Account-linkable client error-telemetry must not be
|
|
// kept indefinitely: a daily task deletes rows older than the retention window.
|
|
// Disabled when telemetry ingest is off (default) or retention <= 0.
|
|
//
|
|
// - HEICODE_TELEMETRY_RETENTION_DAYS (default 30): rows received earlier than
|
|
// now-RETENTION are deleted. <=0 disables the purge (keep-forever — only for
|
|
// explicit operator opt-out; not recommended for production).
|
|
// - HEICODE_TELEMETRY_RETENTION_INTERVAL_HOURS (default 24): sweep cadence.
|
|
//
|
|
// Master-only (wired from main.go under IsMasterNode) so multiple nodes don't
|
|
// all sweep the shared LOG_DB.
|
|
|
|
var telemetryRetentionTaskOnce sync.Once
|
|
|
|
func telemetryRetentionDays() int {
|
|
return common.GetEnvOrDefault("HEICODE_TELEMETRY_RETENTION_DAYS", 30)
|
|
}
|
|
|
|
// StartTelemetryRetentionTask launches the daily telemetry retention sweep.
|
|
func StartTelemetryRetentionTask() {
|
|
telemetryRetentionTaskOnce.Do(func() {
|
|
// Only meaningful once ingest is enabled; if the endpoint is off there is
|
|
// nothing being written, but we still allow the sweep to drain any rows
|
|
// captured during a prior enabled window. Gate on retention days instead.
|
|
days := telemetryRetentionDays()
|
|
if days <= 0 {
|
|
common.SysLog("telemetry retention task disabled (HEICODE_TELEMETRY_RETENTION_DAYS<=0; rows kept indefinitely)")
|
|
return
|
|
}
|
|
intervalHours := common.GetEnvOrDefault("HEICODE_TELEMETRY_RETENTION_INTERVAL_HOURS", 24)
|
|
if intervalHours < 1 {
|
|
intervalHours = 24
|
|
}
|
|
go func() {
|
|
time.Sleep(5 * time.Minute) // avoid startup churn
|
|
runTelemetryRetentionOnce()
|
|
ticker := time.NewTicker(time.Duration(intervalHours) * time.Hour)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
runTelemetryRetentionOnce()
|
|
}
|
|
}()
|
|
common.SysLog(fmt.Sprintf("telemetry retention task started: retention=%dd interval=%dh", days, intervalHours))
|
|
})
|
|
}
|
|
|
|
func runTelemetryRetentionOnce() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
common.SysLog(fmt.Sprintf("telemetry retention task panic recovered: %v", r))
|
|
}
|
|
}()
|
|
days := telemetryRetentionDays()
|
|
if days <= 0 {
|
|
return
|
|
}
|
|
cutoff := time.Now().Unix() - int64(days)*86400
|
|
deleted, err := model.DeleteTelemetryEventsBefore(cutoff)
|
|
if err != nil {
|
|
common.SysLog("telemetry retention task: " + err.Error())
|
|
return
|
|
}
|
|
if deleted > 0 {
|
|
common.SysLog(fmt.Sprintf("telemetry retention task: deleted %d events older than %d days", deleted, days))
|
|
}
|
|
}
|