feat(sense): add reconciliation safety controls [T-012]
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHandlerUsesOnlyFixedLowCardinalityLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
registry := New("ins_test-1", "v-test")
|
||||
registry.ObserveReconcileRun(nil, 1500*time.Millisecond)
|
||||
registry.ObserveReconcileItem("success")
|
||||
registry.ObserveReconcileItem("lease_lost")
|
||||
registry.SetConvergence(16, 2)
|
||||
registry.ObserveOrphanScan(20, 1, 3, nil)
|
||||
registry.ObserveOrphanCleanupBlocked("ratio_exceeded")
|
||||
registry.ObserveOrphanCleanup(1, 1)
|
||||
|
||||
request := httptest.NewRequest("GET", "/metrics", nil)
|
||||
response := httptest.NewRecorder()
|
||||
registry.Handler().ServeHTTP(response, request)
|
||||
body := response.Body.String()
|
||||
for _, expected := range []string{
|
||||
`sense_build_info{instance_id="ins_test-1",version="v-test"} 1`,
|
||||
`sense_reconcile_items_total{result="lease_lost"} 1`,
|
||||
`sense_reconcile_devices{state="unconverged"} 2`,
|
||||
`sense_orphan_paths{classification="owned_stale"} 1`,
|
||||
`sense_orphan_cleanup_blocked_total{reason="ratio_exceeded"} 1`,
|
||||
} {
|
||||
if !strings.Contains(body, expected) {
|
||||
t.Fatalf("metrics output lacks %q:\n%s", expected, body)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{
|
||||
"tenant_id", "site_id", "device_id", "path_name", "rtsp://", "credential",
|
||||
} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("high-cardinality or secret-bearing label appeared: %q", forbidden)
|
||||
}
|
||||
}
|
||||
if response.Header().Get("Cache-Control") != "no-store" {
|
||||
t.Fatal("metrics responses must not be cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryIsSafeForConcurrentObservers(t *testing.T) {
|
||||
t.Parallel()
|
||||
registry := New("ins_concurrent", "dev")
|
||||
var wait sync.WaitGroup
|
||||
for index := 0; index < 32; index++ {
|
||||
wait.Add(1)
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
for count := 0; count < 100; count++ {
|
||||
registry.ObserveReconcileItem("success")
|
||||
registry.SetConvergence(128, count%17)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wait.Wait()
|
||||
response := httptest.NewRecorder()
|
||||
registry.Handler().ServeHTTP(response, httptest.NewRequest("GET", "/metrics", nil))
|
||||
if !strings.Contains(response.Body.String(), `sense_reconcile_items_total{result="success"} 3200`) {
|
||||
t.Fatalf("concurrent counter lost updates: %s", response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateInstanceIDShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
first, err := GenerateInstanceID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := GenerateInstanceID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(first) != 28 || !strings.HasPrefix(first, "ins_") || first == second {
|
||||
t.Fatalf("unexpected instance identifiers: %q %q", first, second)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user