iop/apps/edge/internal/service/single_request_metrics_test.go

141 lines
4.6 KiB
Go

package service
import (
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func metricValue(t *testing.T, registry *prometheus.Registry, name string, want map[string]string) float64 {
t.Helper()
families, err := registry.Gather()
if err != nil {
t.Fatalf("gather metrics: %v", err)
}
for _, family := range families {
if family.GetName() != name {
continue
}
for _, metric := range family.Metric {
if metricHasLabels(metric, want) {
return metric.GetCounter().GetValue()
}
}
}
return 0
}
func metricHasLabels(metric *dto.Metric, want map[string]string) bool {
if len(metric.Label) != len(want) {
return false
}
for _, label := range metric.Label {
if want[label.GetName()] != label.GetValue() {
return false
}
}
return true
}
func TestSingleRequestMetrics(t *testing.T) {
if first, second := defaultSingleRequestCollectorSet(), defaultSingleRequestCollectorSet(); first != second {
t.Fatal("default collector set was registered more than once")
}
registry := prometheus.NewRegistry()
core, logs := observer.New(zap.InfoLevel)
collector := newSingleRequestObservability(registry, zap.New(core))
clock := newSingleRequestManualClock(time.Unix(0, 0))
accumulator := newSingleRequestTimingAccumulator(clock, collector)
accumulator.onRequest()
accumulator.onStageEnter(singleRequestStagePlan)
clock.Advance(2 * time.Second)
accumulator.onStageExit(singleRequestStagePlan, singleRequestOutcomeSuccess, "")
accumulator.onTerminal(singleRequestOutcomeSuccess, "", true)
accumulator.onTerminal(singleRequestOutcomeError, singleRequestErrorClassProvider, false)
terminalLabels := map[string]string{
"event_class": "terminal", "stage": "none", "operation": "terminal", "outcome": "success", "error_class": "none",
}
if got := metricValue(t, registry, singleRequestLifecycleMetric, terminalLabels); got != 1 {
t.Fatalf("terminal metric = %v, want 1", got)
}
stageLabels := map[string]string{
"event_class": "stage", "stage": "plan", "operation": "plan", "outcome": "success", "error_class": "none",
}
if got := metricValue(t, registry, singleRequestLifecycleMetric, stageLabels); got != 1 {
t.Fatalf("stage metric = %v, want 1", got)
}
families, err := registry.Gather()
if err != nil {
t.Fatalf("gather metrics: %v", err)
}
for _, family := range families {
for _, metric := range family.Metric {
for _, label := range metric.Label {
if label.GetName() == "correlation" || strings.Contains(label.GetValue(), "sr-") {
t.Fatalf("unbounded correlation label: %s=%q", label.GetName(), label.GetValue())
}
}
}
}
allowed := map[string]bool{
"correlation": true, "event_class": true, "stage": true, "operation": true,
"outcome": true, "error_class": true, "duration_ms": true, "tool_count": true, "has_result": true,
}
entries := logs.All()
if len(entries) != 3 {
t.Fatalf("observation logs = %d, want 3", len(entries))
}
for _, entry := range entries {
if entry.Message != singleRequestObservationLogKey {
t.Fatalf("log message = %q", entry.Message)
}
if len(entry.Context) != len(allowed) {
t.Fatalf("log field count = %d, want %d", len(entry.Context), len(allowed))
}
for _, field := range entry.Context {
if !allowed[field.Key] {
t.Fatalf("unexpected log key %q", field.Key)
}
}
}
}
func TestSingleRequestMetricsRejectSecretSentinelAndIsolatesObserverFailures(t *testing.T) {
registry := prometheus.NewRegistry()
core, logs := observer.New(zap.InfoLevel)
collector := newSingleRequestObservability(registry, zap.New(core))
if err := collector.Emit(singleRequestDTO{
EventClass: singleRequestEventClassTerminal, Operation: singleRequestOperationTerminal,
Outcome: singleRequestOutcomeError, ErrorClass: singleRequestErrorClassProvider,
Correlation: "SECRET_PATH_COMMAND_BEARER", DurationMS: 1,
}); err != nil {
t.Fatalf("Emit: %v", err)
}
for _, entry := range logs.All() {
if strings.Contains(strings.ToLower(fmt.Sprint(entry.ContextMap()["correlation"])), "secret") {
t.Fatalf("secret sentinel leaked: %+v", entry)
}
}
failing := singleRequestObserverFunc(func(singleRequestDTO) error { return errors.New("observer failure") })
accumulator := newSingleRequestTimingAccumulator(nil, failing)
accumulator.onTerminal(singleRequestOutcomeSuccess, "", true)
if got := accumulator.observer.failureCount(); got != 1 {
t.Fatalf("isolated failures = %d, want 1", got)
}
}
type singleRequestObserverFunc func(singleRequestDTO) error
func (fn singleRequestObserverFunc) Emit(dto singleRequestDTO) error { return fn(dto) }