Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/capacity/check_test.go

4637 lines
198 KiB
Go

package capacity
import (
"fmt"
"math"
"strconv"
"strings"
"testing"
"time"
)
func TestEvaluateReportsOKWhenCriticalBacklogsAreZero(t *testing.T) {
report := Evaluate(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_active_connections{protocol="JT808"} 50000
vehicle_gateway_connection_rejections_total{protocol="JT808",reason="max_connections"} 0
vehicle_gateway_identity_snapshot_ready 1
vehicle_gateway_identity_snapshot_last_success_unix_seconds 99999999999
vehicle_async_sink_queue_depth{sink="nats"} 0`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_nats_consumer_ack_pending{consumer="vehicle-fast-writer",stream="VEHICLE_INGEST"} 0
vehicle_fast_writer_nats_consumer_pending{consumer="vehicle-fast-writer",stream="VEHICLE_INGEST"} 0
vehicle_fast_writer_tdengine_enabled 0
vehicle_fast_writer_batch_pending_messages 0`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_batch_pending_messages 0
vehicle_history_batch_pending_rows 0
vehicle_history_retry_pending_messages 0
vehicle_history_kafka_lag{topic="vehicle.raw.go.jt808.v1",partition="0"} 0`,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if report.Totals.ActiveConnections != 50000 {
t.Fatalf("active connections = %d, want 50000", report.Totals.ActiveConnections)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedForPendingAndRejects(t *testing.T) {
report := Evaluate(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_active_connections{protocol="JT808"} 120000
vehicle_gateway_connection_rejections_total{protocol="JT808",reason="max_connections"} 3
vehicle_async_sink_queue_depth{sink="nats"} 25000`,
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_nats_consumer_ack_pending{consumer="vehicle-kafka-bridge",stream="VEHICLE_INGEST"} 101
vehicle_bridge_nats_consumer_pending{consumer="vehicle-kafka-bridge",stream="VEHICLE_INGEST"} 12001
vehicle_bridge_batch_pending_messages 1001`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_nats_consumer_ack_pending{consumer="vehicle-fast-writer",stream="VEHICLE_INGEST"} 101
vehicle_fast_writer_nats_consumer_pending{consumer="vehicle-fast-writer",stream="VEHICLE_INGEST"} 12001
vehicle_fast_writer_tdengine_enabled 1
vehicle_fast_writer_batch_pending_messages 1009`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_batch_pending_messages 1006
vehicle_history_batch_pending_rows 5007
vehicle_history_retry_pending_messages 2
vehicle_history_kafka_lag{topic="vehicle.raw.go.jt808.v1",partition="0"} 1042`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_batch_pending_messages 1008
vehicle_stat_retry_pending_messages 3`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_batch_pending_messages 1009
vehicle_realtime_retry_pending_messages 4
vehicle_realtime_async_queue_depth{protocol="JT808",store="mysql"} 10001`,
"identity": `vehicle_service_info{service="vehicle-identity-writer"} 1
vehicle_kafka_consumer_info{service="vehicle-identity-writer",group="go-identity-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_identity_writer_retry_pending_messages 5`,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway connection rejections",
"async sink queue depth",
"bridge ack pending",
"bridge consumer pending",
"bridge batch pending",
"fast writer ack pending",
"fast writer consumer pending",
"fast writer batch pending",
"history batch pending",
"history rows pending",
"stat batch pending",
"realtime batch pending",
"realtime async queue depth",
"history retry pending 2 exceeds 0",
"realtime retry pending 4 exceeds 0",
"stat retry pending 3 exceeds 0",
"identity retry pending 5 exceeds 0",
"kafka lag 1042 exceeds 1000",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if report.Totals.BridgeAckPending != 101 {
t.Fatalf("bridge ack pending = %v, want 101", report.Totals.BridgeAckPending)
}
if report.Totals.BridgeConsumerPending != 12001 {
t.Fatalf("bridge consumer pending = %v, want 12001", report.Totals.BridgeConsumerPending)
}
if report.Totals.FastWriterAckPending != 101 {
t.Fatalf("fast writer ack pending = %v, want 101", report.Totals.FastWriterAckPending)
}
if report.Totals.FastWriterBatchPending != 1009 {
t.Fatalf("fast writer batch pending = %v, want 1009", report.Totals.FastWriterBatchPending)
}
if report.Totals.HistoryBatchPending != 1006 {
t.Fatalf("history batch pending = %v, want 1006", report.Totals.HistoryBatchPending)
}
if report.Totals.HistoryRowsPending != 5007 {
t.Fatalf("history rows pending = %v, want 5007", report.Totals.HistoryRowsPending)
}
if report.Totals.RealtimeBatchPending != 1009 {
t.Fatalf("realtime batch pending = %v, want 1009", report.Totals.RealtimeBatchPending)
}
if report.Totals.RealtimeAsyncQueuePending != 10001 {
t.Fatalf("realtime async queue pending = %v, want 10001", report.Totals.RealtimeAsyncQueuePending)
}
if report.Totals.HistoryRetryPending != 2 || report.Totals.RealtimeRetryPending != 4 || report.Totals.StatRetryPending != 3 || report.Totals.IdentityRetryPending != 5 {
t.Fatalf("retry pending totals = history:%v realtime:%v stat:%v identity:%v", report.Totals.HistoryRetryPending, report.Totals.RealtimeRetryPending, report.Totals.StatRetryPending, report.Totals.IdentityRetryPending)
}
}
func TestEvaluateCanDisableConsumerRetryPendingLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_retry_pending_messages 9`,
}, Thresholds{ConsumerRetryPendingMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateDoesNotDegradeForTransientBatchPending(t *testing.T) {
report := Evaluate(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_batch_pending_messages 470`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_batch_pending_messages 470`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_batch_pending_messages 470`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_batch_pending_messages 470
vehicle_history_batch_pending_rows 470`,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
if report.Totals.FastWriterBatchPending != 470 {
t.Fatalf("fast writer batch pending = %v, want 470", report.Totals.FastWriterBatchPending)
}
if report.Totals.HistoryBatchPending != 470 {
t.Fatalf("history batch pending = %v, want 470", report.Totals.HistoryBatchPending)
}
if report.Totals.HistoryRowsPending != 470 {
t.Fatalf("history rows pending = %v, want 470", report.Totals.HistoryRowsPending)
}
if report.Totals.RealtimeBatchPending != 470 {
t.Fatalf("realtime batch pending = %v, want 470", report.Totals.RealtimeBatchPending)
}
}
func TestEvaluateReportsFastWriterTDengineStageEnabled(t *testing.T) {
report := Evaluate(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_tdengine_enabled 1`,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "fast writer tdengine enabled 1 exceeds 0") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableFastWriterTDengineStageEnabledLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_tdengine_enabled 1`,
}, Thresholds{FastWriterTDengineEnabledMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRecentlyStartedProcess(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_process_uptime_seconds 12`,
}, Thresholds{ProcessUptimeMinSec: 60})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway process uptime 12s below 60s") {
t.Fatalf("uptime finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsMissingProcessUptimeWhenRequired(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1`,
}, Thresholds{ProcessUptimeMinSec: 60})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway process uptime metric missing") {
t.Fatalf("missing uptime finding not found in:\n%s", joined)
}
}
func TestEvaluateCanDisableProcessUptimeLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_process_uptime_seconds 1`,
}, Thresholds{ProcessUptimeMinSec: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsProcessResourceLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_process_heap_alloc_bytes 2048
vehicle_process_heap_sys_bytes 4096
vehicle_process_goroutines 301`,
}, Thresholds{
ProcessHeapAllocMaxBytes: 1024,
ProcessHeapSysMaxBytes: 2048,
ProcessGoroutinesMax: 300,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway process heap alloc 2048 bytes exceeds 1024",
"gateway process heap sys 4096 bytes exceeds 2048",
"gateway process goroutines 301 exceeds 300",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsMissingProcessResourceMetricsWhenRequired(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1`,
}, Thresholds{
ProcessHeapAllocMaxBytes: 1024,
ProcessHeapSysMaxBytes: 2048,
ProcessGoroutinesMax: 300,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway process heap alloc metric missing",
"gateway process heap sys metric missing",
"gateway process goroutines metric missing",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableProcessResourceLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_process_heap_alloc_bytes 2048
vehicle_process_heap_sys_bytes 4096
vehicle_process_goroutines 301`,
}, Thresholds{
ProcessHeapAllocMaxBytes: 0,
ProcessHeapSysMaxBytes: 0,
ProcessGoroutinesMax: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateUsesConfigurableAsyncSinkQueueDepthLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_depth{sink="nats"} 1500`,
}, Thresholds{AsyncSinkQueueDepthMax: 1000})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "async sink queue depth 1500 exceeds 1000") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableAsyncSinkQueueDepthLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_depth{sink="nats"} 999999`,
}, Thresholds{AsyncSinkQueueDepthMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsSlowAsyncSinkQueueWaitRecentP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_wait_recent_p99_ms{sink="nats",queue="raw",kind="raw"} 125
vehicle_async_sink_queue_wait_recent_samples{sink="nats",queue="raw",kind="raw"} 512`,
}, Thresholds{
AsyncSinkQueueWaitRecentP99MS: 100,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"async sink queue wait recent p99 125ms exceeds 100ms",
"kind=raw,queue=raw,sink=nats",
"samples 512",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableAsyncSinkQueueWaitRecentP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_wait_recent_p99_ms{sink="nats",queue="raw",kind="raw"} 999
vehicle_async_sink_queue_wait_recent_samples{sink="nats",queue="raw",kind="raw"} 512`,
}, Thresholds{
AsyncSinkQueueWaitRecentP99MS: 0,
MinHistogramSamples: 100,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
}
func TestEvaluateReportsAsyncSinkEnqueueAndPublishErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_enqueue_total{sink="nats",kind="raw",status="queued"} 100
vehicle_async_sink_enqueue_total{sink="nats",kind="fields",status="timeout"} 2
vehicle_async_sink_enqueue_total{sink="kafka",kind="raw",status="closed"} 1
vehicle_async_sink_publish_total{sink="nats",kind="raw",status="error"} 3
vehicle_async_sink_publish_total{sink="nats",kind="fields",status="ok"} 100`,
}, Thresholds{AsyncSinkEnqueueErrorMax: 0, AsyncSinkPublishErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway async sink enqueue timeout 2 exceeds 0 for sink nats kind fields",
"gateway async sink enqueue closed 1 exceeds 0 for sink kafka kind raw",
"gateway async sink publish errors 3 exceeds 0 for sink nats kind raw",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if strings.Contains(joined, "queued") || strings.Contains(joined, "publish errors 100") {
t.Fatalf("successful enqueue/publish samples should not be reported:\n%s", joined)
}
}
func TestEvaluateCanDisableAsyncSinkErrorLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_enqueue_total{sink="nats",kind="fields",status="timeout"} 2
vehicle_async_sink_publish_total{sink="nats",kind="raw",status="error"} 3`,
}, Thresholds{AsyncSinkEnqueueErrorMax: -1, AsyncSinkPublishErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRuntimeConfigDrift(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 1000
vehicle_bridge_config{setting="workers"} 1`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 100
vehicle_fast_writer_config{setting="workers"} 2`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 1
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 1
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_config{setting="workers"} 1`,
"identity": `vehicle_service_info{service="vehicle-identity-writer"} 1
vehicle_identity_writer_config{setting="workers"} 1
vehicle_kafka_consumer_info{service="vehicle-identity-writer",group="go-identity-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{
BridgeFetchWaitMaxMS: 50,
BridgeWorkersMin: 2,
FastWriterFetchWaitMaxMS: 50,
FastWriterWorkersMin: 4,
HistoryWorkersMin: 3,
RealtimeWorkersMin: 3,
StatWorkersMin: 3,
IdentityWorkersMin: 3,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge fetch wait 1000ms exceeds 50ms",
"bridge workers 1 below 2",
"fast writer fetch wait 100ms exceeds 50ms",
"fast writer workers 2 below 4",
"history workers 1 below 3",
"realtime workers 1 below 3",
"stat workers 1 below 3",
"identity workers 1 below 3",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateAcceptsRuntimeConfigWithinLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_jt808_registration_gateway_writes_enabled 0`,
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1`,
"identity": `vehicle_service_info{service="vehicle-identity-writer"} 1
vehicle_identity_writer_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-identity-writer",group="go-identity-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{
BridgeFetchWaitMaxMS: 50,
BridgeWorkersMin: 2,
FastWriterFetchWaitMaxMS: 50,
FastWriterWorkersMin: 4,
HistoryWorkersMin: 3,
RealtimeWorkersMin: 3,
StatWorkersMin: 3,
IdentityWorkersMin: 3,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsMissingRuntimeConfigMetrics(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="bridge-test"} 1`,
"fast-writer": `vehicle_service_info{service="fast-writer-test"} 1`,
"history": `vehicle_service_info{service="history-test"} 1
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
"realtime": `vehicle_service_info{service="realtime-test"} 1
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1`,
"stat": `vehicle_service_info{service="stat-test"} 1
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1`,
"identity": `vehicle_service_info{service="identity-test"} 1
vehicle_kafka_consumer_info{service="vehicle-identity-writer",group="go-identity-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{
BridgeFetchWaitMaxMS: 50,
BridgeWorkersMin: 2,
FastWriterFetchWaitMaxMS: 50,
FastWriterWorkersMin: 4,
HistoryWorkersMin: 3,
RealtimeWorkersMin: 3,
StatWorkersMin: 3,
IdentityWorkersMin: 3,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge fetch wait config metric missing (vehicle_bridge_config setting=fetch_wait_ms)",
"bridge workers config metric missing (vehicle_bridge_config setting=workers)",
"fast writer fetch wait config metric missing (vehicle_fast_writer_config setting=fetch_wait_ms)",
"fast writer workers config metric missing (vehicle_fast_writer_config setting=workers)",
"history workers config metric missing (vehicle_history_config setting=workers)",
"realtime workers config metric missing (vehicle_realtime_config setting=workers)",
"stat workers config metric missing (vehicle_stat_config setting=workers)",
"identity workers config metric missing (vehicle_identity_writer_config setting=workers)",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableRuntimeConfigMetricRequirements(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1`,
}, Thresholds{
BridgeFetchWaitMaxMS: 0,
BridgeWorkersMin: 0,
FastWriterFetchWaitMaxMS: 0,
FastWriterWorkersMin: 0,
HistoryWorkersMin: 0,
RealtimeWorkersMin: 0,
StatWorkersMin: 0,
IdentityWorkersMin: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsAsyncSinkQueueUsageOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_depth{sink="nats",queue="raw"} 90
vehicle_async_sink_queue_capacity{sink="nats",queue="raw"} 100
vehicle_async_sink_queue_depth{sink="nats",queue="derived"} 10
vehicle_async_sink_queue_capacity{sink="nats",queue="derived"} 100`,
}, Thresholds{AsyncSinkQueueMaxUsageRatio: 0.80})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway async sink queue usage 0.90 exceeds 0.80",
"queue=raw",
"sink=nats",
"depth 90",
"capacity 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if strings.Contains(joined, "queue=derived") {
t.Fatalf("derived queue should not be reported:\n%s", joined)
}
}
func TestEvaluateAllowsAsyncSinkQueueUsageUnderLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_depth{sink="kafka",queue="raw"} 79
vehicle_async_sink_queue_capacity{sink="kafka",queue="raw"} 100`,
}, Thresholds{AsyncSinkQueueMaxUsageRatio: 0.80})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableAsyncSinkQueueUsageLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_async_sink_queue_depth{sink="nats",queue="derived"} 100
vehicle_async_sink_queue_capacity{sink="nats",queue="derived"} 100`,
}, Thresholds{AsyncSinkQueueMaxUsageRatio: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDurableSpoolBacklogAndErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_durable_spool_backlog_files{name="nats"} 10001
vehicle_durable_spool_oldest_age_seconds{name="nats"} 601
vehicle_durable_spool_records_total{kind="raw",name="nats",status="error"} 2
vehicle_durable_spool_replay_total{name="nats",status="publish_error"} 1`,
}, Thresholds{
DurableSpoolBacklogMax: 10000,
DurableSpoolOldestAgeMaxSec: 300,
DurableSpoolErrorMax: 0,
DurableSpoolReplayErrorMax: 0,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"durable spool backlog 10001 exceeds 10000 for name nats",
"durable spool oldest age 601s exceeds 300s for name nats",
"durable spool record errors 2 exceeds 0 for name nats kind raw",
"durable spool replay publish_error 1 exceeds 0 for name nats",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableDurableSpoolLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_durable_spool_backlog_files{name="kafka"} 999999
vehicle_durable_spool_oldest_age_seconds{name="kafka"} 999999
vehicle_durable_spool_records_total{kind="fields",name="kafka",status="error"} 99
vehicle_durable_spool_replay_total{name="kafka",status="delete_error"} 99`,
}, Thresholds{
DurableSpoolBacklogMax: 0,
DurableSpoolOldestAgeMaxSec: 0,
DurableSpoolErrorMax: -1,
DurableSpoolReplayErrorMax: -1,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDurableOutboxInflightAndErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_durable_outbox_inflight{name="nats-outbox"} 10001
vehicle_durable_outbox_publish_total{kind="raw",name="nats-outbox",status="submitted"} 50000
vehicle_durable_outbox_publish_total{kind="raw",name="nats-outbox",status="acked"} 49998
vehicle_durable_outbox_publish_total{kind="raw",name="nats-outbox",status="ack_error"} 2
vehicle_durable_outbox_publish_total{kind="all",name="nats-outbox",status="close_timeout"} 1`,
}, Thresholds{
DurableOutboxInflightMax: 10000,
DurableOutboxErrorMax: 0,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"durable outbox inflight 10001 exceeds 10000 for name nats-outbox",
"durable outbox ack_error 2 exceeds 0 for name nats-outbox kind raw",
"durable outbox close_timeout 1 exceeds 0 for name nats-outbox kind all",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if strings.Contains(joined, "submitted") || strings.Contains(joined, "acked") {
t.Fatalf("successful outbox statuses must not create findings:\n%s", joined)
}
}
func TestEvaluateCanDisableDurableOutboxLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_durable_outbox_inflight{name="nats-outbox"} 999999
vehicle_durable_outbox_publish_total{kind="raw",name="nats-outbox",status="submit_error"} 99`,
}, Thresholds{
DurableOutboxInflightMax: 0,
DurableOutboxErrorMax: -1,
})
if report.Status != StatusOK || len(report.Findings) != 0 {
t.Fatalf("report = %#v, want ok without findings", report)
}
}
func TestEvaluateCanDisableStatBatchPendingLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_batch_pending_messages 999999`,
}, Thresholds{StatBatchPendingMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableRealtimeBatchPendingLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_batch_pending_messages 999999`,
}, Thresholds{RealtimeBatchPendingMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsBridgeRouteErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_messages_total{status="route_error",subject="vehicle.unconfigured.v1"} 1`,
}, Thresholds{BridgeRouteErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "bridge route errors 1 exceeds 0 for subject vehicle.unconfigured.v1") {
t.Fatalf("route error finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableBridgeRouteErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_messages_total{status="route_error",subject="vehicle.unconfigured.v1"} 999`,
}, Thresholds{BridgeRouteErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsBridgeKafkaWriteErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="error",topic="vehicle.fields.go.jt808.v1"} 2`,
}, Thresholds{BridgeKafkaWriteErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "bridge kafka write errors 2 exceeds 0 for topic vehicle.fields.go.jt808.v1") {
t.Fatalf("kafka write error finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableBridgeKafkaWriteErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="error",topic="vehicle.raw.go.gb32960.v1"} 99`,
}, Thresholds{BridgeKafkaWriteErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsBridgeUnackedMessageRatio(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_messages_total{status="received",subject="vehicle.raw.go.jt808.v1"} 1000
vehicle_bridge_nats_acks_total{status="ok",subject="vehicle.raw.go.jt808.v1"} 700
vehicle_bridge_nats_acks_total{status="dropped_route_error",subject="vehicle.raw.go.jt808.v1"} 50`,
}, Thresholds{
BridgeMessageMinReceived: 100,
BridgeMessageMaxUnackedRatio: 0.20,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge unacked message ratio 0.25 exceeds 0.20 for subject vehicle.raw.go.jt808.v1",
"received 1000",
"ack ok 700",
"dropped route error 50",
"unacked 250",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateAllowsBridgeCompletedAndLowSampleMessages(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_messages_total{status="received",subject="vehicle.raw.go.gb32960.v1"} 100
vehicle_bridge_nats_acks_total{status="ok",subject="vehicle.raw.go.gb32960.v1"} 80
vehicle_bridge_nats_acks_total{status="dropped_route_error",subject="vehicle.raw.go.gb32960.v1"} 20
vehicle_bridge_messages_total{status="received",subject="vehicle.raw.go.yutong-mqtt.v1"} 99`,
}, Thresholds{
BridgeMessageMinReceived: 100,
BridgeMessageMaxUnackedRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsKafkaConsumerUncommittedMessageRatio(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_kafka_messages_total{status="received",topic="vehicle.fields.go.jt808.v1"} 1000
vehicle_stat_kafka_commits_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 700
vehicle_stat_kafka_commits_total{status="error",topic="vehicle.fields.go.jt808.v1"} 10`,
}, Thresholds{
KafkaConsumerMessageMinReceived: 100,
KafkaConsumerMaxUncommittedRatio: 0.20,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat kafka consumer uncommitted message ratio 0.30 exceeds 0.20 for topic vehicle.fields.go.jt808.v1",
"received 1000",
"commit ok 700",
"commit errors 10",
"uncommitted 300",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsKafkaConsumerCommitErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_realtime_kafka_messages_total{status="received",topic="vehicle.raw.go.gb32960.v1"} 3
vehicle_realtime_kafka_commits_total{status="error",topic="vehicle.raw.go.gb32960.v1"} 1`,
}, Thresholds{
KafkaConsumerCommitErrorMax: 0,
KafkaConsumerMessageMinReceived: 100,
KafkaConsumerMaxUncommittedRatio: 0.20,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime kafka consumer commit errors 1 exceeds 0 for topic vehicle.raw.go.gb32960.v1") {
t.Fatalf("commit error finding missing in:\n%s", joined)
}
if strings.Contains(joined, "uncommitted message ratio") {
t.Fatalf("low-sample uncommitted ratio should not be enforced:\n%s", joined)
}
}
func TestEvaluateCanDisableKafkaConsumerCommitErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_realtime_kafka_messages_total{status="received",topic="vehicle.raw.go.gb32960.v1"} 3
vehicle_realtime_kafka_commits_total{status="error",topic="vehicle.raw.go.gb32960.v1"} 1`,
}, Thresholds{
KafkaConsumerCommitErrorMax: -1,
KafkaConsumerMessageMinReceived: 100,
KafkaConsumerMaxUncommittedRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateAllowsKafkaConsumerCompletedAndLowSampleMessages(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_history_kafka_messages_total{status="received",topic="vehicle.raw.go.gb32960.v1"} 100
vehicle_history_kafka_commits_total{status="ok",topic="vehicle.raw.go.gb32960.v1"} 80
vehicle_history_kafka_messages_total{status="received",topic="vehicle.raw.go.yutong-mqtt.v1"} 99`,
}, Thresholds{
KafkaConsumerMessageMinReceived: 100,
KafkaConsumerMaxUncommittedRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsFastWriterInvalidJSON(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_messages_total{status="invalid_json",subject="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{FastWriterInvalidJSONMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "fast writer invalid json 1 exceeds 0 for subject vehicle.raw.go.jt808.v1") {
t.Fatalf("invalid json finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableFastWriterInvalidJSONLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_messages_total{status="invalid_json",subject="vehicle.raw.go.jt808.v1"} 99`,
}, Thresholds{FastWriterInvalidJSONMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsFastWriterBatchFallbackErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_batch_fallback_total{stage="redis",status="error"} 2`,
}, Thresholds{FastWriterFallbackErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "fast writer batch fallback errors 2 exceeds 0 for stage redis") {
t.Fatalf("fallback finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableFastWriterBatchFallbackErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_batch_fallback_total{stage="tdengine",status="error"} 99`,
}, Thresholds{FastWriterFallbackErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsFastWriterDecoupledUpdateErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_decoupled_updates_total{reason="tdengine_transient",status="ok"} 5
vehicle_fast_writer_decoupled_updates_total{reason="tdengine_transient",status="error"} 1`,
}, Thresholds{FastWriterDecoupledUpdateErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "fast writer decoupled update errors 1 exceeds 0 for reason tdengine_transient") {
t.Fatalf("decoupled update finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableFastWriterDecoupledUpdateErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_decoupled_updates_total{reason="tdengine_transient",status="error"} 99`,
}, Thresholds{FastWriterDecoupledUpdateErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsHistoryInvalidJSON(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_history_kafka_messages_total{status="invalid_json",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{HistoryInvalidJSONMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "history invalid json 1 exceeds 0 for topic vehicle.raw.go.jt808.v1") {
t.Fatalf("invalid json finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableHistoryInvalidJSONLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_history_kafka_messages_total{status="invalid_json",topic="vehicle.raw.go.jt808.v1"} 99`,
}, Thresholds{HistoryInvalidJSONMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsHistoryLocationWriteErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_location_writes_total{status="error",topic="vehicle.raw.go.jt808.v1"} 2`,
}, Thresholds{HistoryLocationErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history location writes error 2 exceeds 0",
"vehicle.raw.go.jt808.v1",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableHistoryLocationWriteErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_location_writes_total{status="error",topic="vehicle.raw.go.jt808.v1"} 99`,
}, Thresholds{HistoryLocationErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsHistoryLocationAccountingMismatch(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_location_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 70
vehicle_history_location_writes_total{status="skipped_non_realtime",topic="vehicle.raw.go.jt808.v1"} 10`,
}, Thresholds{HistoryLocationAccountingMaxRatio: 0.02})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history location accounting mismatch ratio 0.20 exceeds 0.02",
"history writes ok 100, location statuses 80, diff 20",
"vehicle.raw.go.jt808.v1",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateAcceptsHistoryLocationAccountingWhenStatusesMatchWrites(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_location_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 70
vehicle_history_location_writes_total{status="skipped_non_realtime",topic="vehicle.raw.go.jt808.v1"} 20
vehicle_history_location_writes_total{status="skipped_missing_coordinates",topic="vehicle.raw.go.jt808.v1"} 10`,
}, Thresholds{HistoryLocationAccountingMaxRatio: 0.02})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableHistoryLocationAccountingLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_location_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{HistoryLocationAccountingMaxRatio: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsHistoryBatchFallbackErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_batch_fallback_total{status="error"} 2`,
}, Thresholds{HistoryFallbackErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "history batch fallback errors 2 exceeds 0") {
t.Fatalf("fallback finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableHistoryBatchFallbackErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_batch_fallback_total{status="error"} 99`,
}, Thresholds{HistoryFallbackErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsWriteRetriesOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_write_retries_total{operation="batch",status="retry"} 11
vehicle_history_write_retries_total{operation="batch",status="exhausted"} 1`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_write_retries_total{operation="single",status="retry"} 12
vehicle_stat_write_retries_total{operation="single",status="exhausted"} 1`,
}, Thresholds{
HistoryRetryMax: 10,
HistoryRetryExhaustedMax: 0,
StatRetryMax: 10,
StatRetryExhaustedMax: 0,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history write retries 11 exceeds 10 for operation batch",
"history write retry exhausted 1 exceeds 0 for operation batch",
"stat write retries 12 exceeds 10 for operation single",
"stat write retry exhausted 1 exceeds 0 for operation single",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableWriteRetryLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_write_retries_total{operation="batch",status="retry"} 99
vehicle_history_write_retries_total{operation="batch",status="exhausted"} 99`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_write_retries_total{operation="single",status="retry"} 99
vehicle_stat_write_retries_total{operation="single",status="exhausted"} 99`,
}, Thresholds{
HistoryRetryMax: -1,
HistoryRetryExhaustedMax: -1,
StatRetryMax: -1,
StatRetryExhaustedMax: -1,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimeInvalidJSON(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_realtime_kafka_messages_total{status="invalid_json",topic="vehicle.raw.go.gb32960.v1"} 1`,
}, Thresholds{RealtimeInvalidJSONMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime invalid json 1 exceeds 0 for topic vehicle.raw.go.gb32960.v1") {
t.Fatalf("invalid json finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableRealtimeInvalidJSONLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_realtime_kafka_messages_total{status="invalid_json",topic="vehicle.raw.go.gb32960.v1"} 99`,
}, Thresholds{RealtimeInvalidJSONMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsStatInvalidJSON(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.yutong-mqtt.v1"} 1
vehicle_stat_kafka_messages_total{status="invalid_json",topic="vehicle.fields.go.jt808.v1"} 1`,
}, Thresholds{StatInvalidJSONMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "stat invalid json 1 exceeds 0 for topic vehicle.fields.go.jt808.v1") {
t.Fatalf("invalid json finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableStatInvalidJSONLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.yutong-mqtt.v1"} 1
vehicle_stat_kafka_messages_total{status="invalid_json",topic="vehicle.fields.go.jt808.v1"} 99`,
}, Thresholds{StatInvalidJSONMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsStatProtocolTopicMismatches(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_kafka_messages_total{status="protocol_topic_mismatch",topic="vehicle.fields.go.gb32960.v1"} 1`,
}, Thresholds{StatProtocolTopicMismatchMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "stat protocol topic mismatches 1 exceeds 0 for topic vehicle.fields.go.gb32960.v1") {
t.Fatalf("protocol topic mismatch finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsMessageContractMismatches(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_kafka_messages_total{status="event_kind_mismatch",topic="vehicle.raw.go.jt808.v1"} 1`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_realtime_kafka_messages_total{status="protocol_topic_mismatch",topic="vehicle.raw.go.gb32960.v1"} 1`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_messages_total{status="event_kind_mismatch",subject="vehicle.raw.go.yutong-mqtt.v1"} 1`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_kafka_messages_total{status="event_kind_mismatch",topic="vehicle.fields.go.jt808.v1"} 1`,
}, Thresholds{MessageContractMismatchMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history message contract event_kind_mismatch 1 exceeds 0 for topic vehicle.raw.go.jt808.v1",
"realtime message contract protocol_topic_mismatch 1 exceeds 0 for topic vehicle.raw.go.gb32960.v1",
"fast writer message contract event_kind_mismatch 1 exceeds 0 for subject vehicle.raw.go.yutong-mqtt.v1",
"stat message contract event_kind_mismatch 1 exceeds 0 for topic vehicle.fields.go.jt808.v1",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableMessageContractMismatchLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_kafka_messages_total{status="event_kind_mismatch",topic="vehicle.raw.go.jt808.v1"} 99`,
}, Thresholds{MessageContractMismatchMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableStatProtocolTopicMismatchLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_kafka_messages_total{status="protocol_topic_mismatch",topic="vehicle.fields.go.gb32960.v1"} 1`,
}, Thresholds{StatProtocolTopicMismatchMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsStatMetricProtocolTopicMismatches(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.jt808.v1"} 12
vehicle_stat_sources_total{protocol="YUTONG_MQTT",status="written",topic="vehicle.fields.go.gb32960.v1"} 7
vehicle_stat_projections_total{protocol="JT808",status="written",topic="vehicle.fields.go.yutong-mqtt.v1"} 5`,
}, Thresholds{StatProtocolTopicMismatchMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat metric protocol topic mismatches 12 exceeds 0 for vehicle_stat_samples_total topic vehicle.fields.go.jt808.v1: expected JT808, got GB32960 (status found)",
"stat metric protocol topic mismatches 7 exceeds 0 for vehicle_stat_sources_total topic vehicle.fields.go.gb32960.v1: expected GB32960, got YUTONG_MQTT (status written)",
"stat metric protocol topic mismatches 5 exceeds 0 for vehicle_stat_projections_total topic vehicle.fields.go.yutong-mqtt.v1: expected YUTONG_MQTT, got JT808 (status written)",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsStatMetricProtocolLabelMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_samples_total{status="found",topic="vehicle.fields.go.jt808.v1"} 12`,
}, Thresholds{StatProtocolTopicMismatchMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "stat metric protocol label missing 12 exceeds 0 for vehicle_stat_samples_total topic vehicle.fields.go.jt808.v1 (status found)") {
t.Fatalf("protocol label missing finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsDegradedWhenServiceInfoMetricIsMissing(t *testing.T) {
report := Evaluate(map[string]string{
"realtime": ``,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime service info metric missing") {
t.Fatalf("service info finding missing in:\n%s", joined)
}
}
func TestEvaluateAggregatesLastActivityTimestampsByMax(t *testing.T) {
report := Evaluate(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_last_publish_unix_seconds{kind="raw",protocol="GB32960",status="ok"} 100
vehicle_gateway_last_publish_unix_seconds{kind="fields",protocol="GB32960",status="ok"} 120
vehicle_gateway_publish_total{kind="raw",protocol="GB32960",status="ok"} 3
vehicle_gateway_publish_total{kind="fields",protocol="GB32960",status="ok"} 4`,
})
if got := report.Services[0].Metrics["vehicle_gateway_last_publish_unix_seconds"]; got != 120 {
t.Fatalf("last publish aggregate = %v, want max timestamp 120", got)
}
if got := report.Services[0].Metrics["vehicle_gateway_last_publish_age_seconds"]; got <= 0 {
t.Fatalf("last publish age = %v, want positive age", got)
}
if got := report.Services[0].Metrics["vehicle_gateway_publish_total"]; got != 7 {
t.Fatalf("publish total aggregate = %v, want sum 7", got)
}
}
func TestEvaluateReportsDegradedWhenObservedLastActivityIsStale(t *testing.T) {
stale := time.Now().UTC().Add(-10 * time.Minute).Unix()
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_last_publish_unix_seconds{kind="raw",protocol="JT808",status="ok"} ` + strconv.FormatInt(stale, 10),
}, Thresholds{LastActivityStaleSec: 300})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway last publish stale",
"exceeds 300s",
"kind=raw,protocol=JT808,status=ok",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeForFreshOrErrorLastActivity(t *testing.T) {
fresh := time.Now().UTC().Unix()
staleError := time.Now().UTC().Add(-10 * time.Minute).Unix()
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_last_publish_unix_seconds{kind="raw",protocol="JT808",status="ok"} ` + strconv.FormatInt(fresh, 10) + `
vehicle_gateway_last_publish_unix_seconds{kind="raw",protocol="JT808",status="error"} ` + strconv.FormatInt(staleError, 10),
}, Thresholds{LastActivityStaleSec: 300})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenFastWriterLastActivityIsStale(t *testing.T) {
stale := time.Now().UTC().Add(-10 * time.Minute).Unix()
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_last_message_unix_seconds{status="received",subject="vehicle.raw.go.jt808.v1"} ` + strconv.FormatInt(stale, 10) + `
vehicle_fast_writer_last_stage_unix_seconds{stage="redis",status="ok",subject="vehicle.raw.go.jt808.v1"} ` + strconv.FormatInt(stale, 10),
}, Thresholds{LastActivityStaleSec: 300})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer last message stale",
"fast writer last stage stale",
"subject=vehicle.raw.go.jt808.v1",
"stage=redis",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateSkipsRollupLastActivitySubjects(t *testing.T) {
stale := time.Now().UTC().Add(-10 * time.Minute).Unix()
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_last_stage_unix_seconds{stage="redis",status="ok",subject="mixed"} ` + strconv.FormatInt(stale, 10) + `
vehicle_fast_writer_last_stage_unix_seconds{stage="redis",status="ok",subject="unknown"} ` + strconv.FormatInt(stale, 10),
}, Thresholds{LastActivityStaleSec: 300})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimeStoreLastActivityStale(t *testing.T) {
stale := time.Now().UTC().Add(-10 * time.Minute).Unix()
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_realtime_last_store_update_unix_seconds{protocol="JT808",status="ok",store="mysql"} ` + strconv.FormatInt(stale, 10),
}, Thresholds{LastActivityStaleSec: 300})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"realtime last store update stale",
"exceeds 300s",
"protocol=JT808,status=ok,store=mysql",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenKafkaConsumerInfoIsMissing(t *testing.T) {
report := Evaluate(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1`,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "stat kafka consumer info metric missing") {
t.Fatalf("kafka consumer info finding missing in:\n%s", joined)
}
}
func TestEvaluateDoesNotDegradeForSmallKafkaLag(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_kafka_lag{topic="vehicle.fields.go.jt808.v1",partition="0"} 8`,
}, Thresholds{KafkaLagMax: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
if report.Totals.KafkaLag != 8 {
t.Fatalf("kafka lag total = %v, want 8", report.Totals.KafkaLag)
}
}
func TestEvaluateReportsDegradedWhenRealtimeRedisProjectorIsEnabled(t *testing.T) {
report := Evaluate(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_redis_projector_enabled 1
vehicle_realtime_kafka_lag{topic="vehicle.raw.go.jt808.v1",partition="0"} 0`,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime redis projector enabled") {
t.Fatalf("realtime redis projector finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsDegradedWhenGatewayRawHasNoFieldsPublish(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 120`,
}, Thresholds{GatewayFieldMinRaw: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway fields missing for protocol JT808",
"raw ok 120",
"realtime candidates 120",
"fields ok 0",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotRequireFieldsForNonRealtimeGatewayRaw(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="GB32960",status="ok"} 120
vehicle_gateway_fields_total{protocol="GB32960",status="skipped_non_realtime"} 120`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMaxMissingRatio: 0.2})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateAcceptsFieldsDelegatedToBridge(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 120
vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="delegated"} 120
vehicle_gateway_fields_total{protocol="JT808",status="delegated_to_bridge"} 120
vehicle_gateway_fields_count{protocol="JT808",status="delegated_to_bridge"} 31`,
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 120
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 120`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMaxMissingRatio: 0.2, GatewayFieldMinCount: 1, GatewayBridgeMinPublish: 100})
if report.Status != StatusOK || len(report.Findings) != 0 {
t.Fatalf("report = %#v, want delegated fields flow ok", report)
}
if report.GatewayFields == nil || len(report.GatewayFields.Protocols) != 1 {
t.Fatalf("gateway fields diagnostics = %#v", report.GatewayFields)
}
row := report.GatewayFields.Protocols[0]
if row.FieldsOK != 120 || row.FieldsPublished != 0 || row.FieldsDelegated != 120 {
t.Fatalf("delegated fields diagnostics = %#v", row)
}
}
func TestEvaluateReportsDegradedWhenGatewayFieldsMissingRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="YUTONG_MQTT",status="ok"} 200
vehicle_gateway_publish_total{kind="fields",protocol="YUTONG_MQTT",status="ok"} 130
vehicle_gateway_fields_total{protocol="YUTONG_MQTT",status="published"} 130
vehicle_gateway_fields_total{protocol="YUTONG_MQTT",status="skipped_non_realtime"} 20
vehicle_gateway_fields_total{protocol="YUTONG_MQTT",status="skipped_missing_fields"} 50`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMaxMissingRatio: 0.2})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway fields missing ratio 0.28 exceeds 0.20 for protocol YUTONG_MQTT",
"realtime candidates 180",
"skipped missing fields 50",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenGatewayPublishesSomeFields(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 120
vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="ok"} 1`,
}, Thresholds{GatewayFieldMinRaw: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateIncludesGatewayFieldsDiagnostics(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 200
vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="ok"} 170
vehicle_gateway_fields_total{protocol="JT808",status="published"} 170
vehicle_gateway_fields_total{protocol="JT808",status="skipped_non_realtime"} 20
vehicle_gateway_fields_total{protocol="JT808",status="skipped_missing_fields"} 10
vehicle_gateway_fields_count{protocol="JT808",status="published"} 31`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMaxMissingRatio: 0.2, GatewayFieldMinCount: 1})
if report.GatewayFields == nil {
t.Fatal("gateway fields diagnostics = nil")
}
if got, want := len(report.GatewayFields.Protocols), 1; got != want {
t.Fatalf("gateway fields protocols = %d, want %d: %#v", got, want, report.GatewayFields.Protocols)
}
row := report.GatewayFields.Protocols[0]
if row.Protocol != "JT808" || row.Status != "ok" || row.Reason != "" {
t.Fatalf("gateway fields row identity/status = %#v", row)
}
if row.RawOK != 200 || row.RealtimeCandidates != 180 || row.FieldsOK != 170 || row.FieldsPublished != 170 {
t.Fatalf("gateway fields counters = %#v", row)
}
if row.FieldsSkippedNonRealtime != 20 || row.FieldsSkippedMissing != 10 || row.FieldsPublishError != 0 {
t.Fatalf("gateway fields skip counters = %#v", row)
}
if math.Abs(row.MissingRatio-(10.0/180.0)) > 0.0001 {
t.Fatalf("missing ratio = %v, want %v", row.MissingRatio, 10.0/180.0)
}
if row.LatestFieldCount == nil || *row.LatestFieldCount != 31 {
t.Fatalf("latest field count = %#v, want 31", row.LatestFieldCount)
}
}
func TestEvaluateIncludesProtocolFlowDiagnostics(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 200
vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="ok"} 170
vehicle_gateway_fields_total{protocol="JT808",status="published"} 170
vehicle_gateway_fields_total{protocol="JT808",status="skipped_non_realtime"} 20
vehicle_gateway_fields_count{protocol="JT808",status="published"} 31
vehicle_gateway_identity_skips_total{protocol="JT808",reason="non_vehicle_frame"} 7
vehicle_gateway_identity_total{protocol="JT808",status="resolved"} 160
vehicle_gateway_identity_total{protocol="JT808",status="unresolved"} 40`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 170
vehicle_stat_samples_total{protocol="JT808",status="found",topic="vehicle.fields.go.jt808.v1"} 160
vehicle_stat_samples_total{protocol="JT808",status="written",topic="vehicle.fields.go.jt808.v1"} 140
vehicle_stat_samples_total{protocol="JT808",status="skipped_missing_mileage",topic="vehicle.fields.go.jt808.v1"} 10
vehicle_stat_sources_total{protocol="JT808",status="written",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_sources_total{protocol="JT808",status="skipped_throttled",topic="vehicle.fields.go.jt808.v1"} 159
vehicle_stat_projections_total{protocol="JT808",status="written",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_projections_total{protocol="JT808",status="skipped_throttled",topic="vehicle.fields.go.jt808.v1"} 139`,
}, Thresholds{
GatewayFieldMinRaw: 100,
GatewayFieldMaxMissingRatio: 0.2,
GatewayFieldMinCount: 1,
GatewayIdentityMinSamples: 100,
GatewayIdentityMaxUnresolvedRatio: 0.25,
StatSampleMinWrites: 100,
StatSampleMaxActionableSkipRatio: 0.20,
StatSourceMinSamples: 100,
StatSourceMaxMissingRatio: 0.60,
StatProjectionMinSampleWrites: 100,
})
if report.ProtocolFlow == nil {
t.Fatal("protocol flow diagnostics = nil")
}
if got, want := len(report.ProtocolFlow.Protocols), 1; got != want {
t.Fatalf("protocol flow rows = %d, want %d: %#v", got, want, report.ProtocolFlow.Protocols)
}
row := report.ProtocolFlow.Protocols[0]
if row.Protocol != "JT808" || row.Status != "ok" || row.Reason != "" {
t.Fatalf("protocol flow row identity/status = %#v", row)
}
if row.RawOK != 200 || row.RealtimeCandidates != 180 || row.FieldsPublished != 170 {
t.Fatalf("gateway counters = %#v", row)
}
if row.LatestFieldCount == nil || *row.LatestFieldCount != 31 {
t.Fatalf("latest field count = %#v, want 31", row.LatestFieldCount)
}
if row.IdentityTotal != 200 || row.IdentitySkipped != 7 || row.IdentityResolved != 160 || row.IdentityNonResolved != 40 {
t.Fatalf("identity counters = %#v", row)
}
if math.Abs(row.IdentityNonResolvedRatio-0.2) > 0.0001 {
t.Fatalf("identity ratio = %v, want 0.2", row.IdentityNonResolvedRatio)
}
if row.StatWritesOK != 170 || row.StatMileageCandidateWrites != 170 || row.StatSamplesFound != 160 || row.StatSamplesWritten != 140 {
t.Fatalf("stat sample counters = %#v", row)
}
if row.StatSamplesActionableSkipped != 10 || row.StatSkippedMissingMileage != 10 {
t.Fatalf("stat skip counters = %#v", row)
}
if row.StatSourceWritten != 1 || row.StatSourceThrottled != 159 {
t.Fatalf("stat source counters = %#v", row)
}
if row.StatProjectionWritten != 1 || row.StatProjectionThrottled != 139 {
t.Fatalf("stat projection counters = %#v", row)
}
}
func TestProtocolFlowDiagnosticsHighlightsStatProjectionBreak(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.gb32960.v1"} 120
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.gb32960.v1"} 120
vehicle_stat_samples_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 120
vehicle_stat_sources_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_sources_total{protocol="GB32960",status="skipped_throttled",topic="vehicle.fields.go.gb32960.v1"} 119
vehicle_stat_projections_total{protocol="GB32960",status="attempted",topic="vehicle.fields.go.gb32960.v1"} 120`,
}, Thresholds{
StatSampleMinWrites: 100,
StatSourceMinSamples: 100,
StatProjectionMinSampleWrites: 100,
})
if report.ProtocolFlow == nil || len(report.ProtocolFlow.Protocols) != 1 {
t.Fatalf("protocol flow diagnostics missing: %#v", report.ProtocolFlow)
}
row := report.ProtocolFlow.Protocols[0]
if row.Protocol != "GB32960" || row.Status != "degraded" || row.Reason != "stat_projection_missing" {
t.Fatalf("protocol flow row = %#v", row)
}
if row.StatProjectionAttempted != 120 || row.StatProjectionWritten != 0 {
t.Fatalf("projection counters = %#v", row)
}
}
func TestEvaluateReportsDegradedWhenGatewayPublishedFieldsAreEmpty(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 120
vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="ok"} 120
vehicle_gateway_fields_total{protocol="JT808",status="published"} 120
vehicle_gateway_fields_count{protocol="JT808",status="published"} 0`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMinCount: 1})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway fields count 0 below 1 for protocol JT808",
"published fields events",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if report.GatewayFields == nil || len(report.GatewayFields.Protocols) != 1 {
t.Fatalf("gateway fields diagnostics missing: %#v", report.GatewayFields)
}
row := report.GatewayFields.Protocols[0]
if row.Status != "degraded" || row.Reason != "low_field_count" {
t.Fatalf("gateway fields diagnostic row = %#v", row)
}
}
func TestEvaluateReportsDegradedWhenGatewayFieldsCountMetricIsMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="GB32960",status="ok"} 120
vehicle_gateway_publish_total{kind="fields",protocol="GB32960",status="ok"} 120
vehicle_gateway_fields_total{protocol="GB32960",status="published"} 120`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMinCount: 1})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway fields count metric missing for protocol GB32960 after 120 published fields events") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateSkipsGatewayFieldCountWhenPublishedSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="YUTONG_MQTT",status="ok"} 2
vehicle_gateway_publish_total{kind="fields",protocol="YUTONG_MQTT",status="ok"} 2
vehicle_gateway_fields_total{protocol="YUTONG_MQTT",status="published"} 2
vehicle_gateway_fields_count{protocol="YUTONG_MQTT",status="published"} 0`,
}, Thresholds{GatewayFieldMinRaw: 100, GatewayFieldMinCount: 1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenGatewayRawPublishDoesNotReachBridge(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 120`,
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4`,
}, Thresholds{GatewayBridgeMinPublish: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge writes missing for gateway raw publish protocol JT808",
"gateway ok 120",
"expected topic vehicle.raw.go.jt808.v1",
"bridge writes ok 0",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenGatewayFieldsPublishDoesNotReachBridge(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="fields",protocol="GB32960",status="ok"} 120`,
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4`,
}, Thresholds{GatewayBridgeMinPublish: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge writes missing for gateway fields publish protocol GB32960",
"expected topic vehicle.fields.go.gb32960.v1",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenGatewayPublishReachesBridge(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="YUTONG_MQTT",status="ok"} 120
vehicle_gateway_publish_total{kind="fields",protocol="YUTONG_MQTT",status="ok"} 120`,
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.fields.go.yutong-mqtt.v1"} 1`,
}, Thresholds{GatewayBridgeMinPublish: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsGatewayBridgeFlowWhenSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 99`,
}, Thresholds{GatewayBridgeMinPublish: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenGatewayPublishTopicMappingIsMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="UNKNOWN_PROTO",status="ok"} 120`,
}, Thresholds{GatewayBridgeMinPublish: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway raw publish topic mapping missing for protocol UNKNOWN_PROTO") {
t.Fatalf("mapping finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsDegradedWhenGatewayRawPublishDoesNotReachFastWriter(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="GB32960",status="ok"} 120`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8`,
}, Thresholds{GatewayFastWriterMinRawPublish: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer received messages missing for gateway raw publish protocol GB32960",
"gateway ok 120",
"expected subject vehicle.raw.go.gb32960.v1",
"fast writer received 0",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenGatewayRawPublishReachesFastWriter(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 120`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_messages_total{status="received",subject="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{GatewayFastWriterMinRawPublish: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsFastWriterUnackedMessageRatio(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_messages_total{status="received",subject="vehicle.raw.go.gb32960.v1"} 1000
vehicle_fast_writer_messages_total{status="ok",subject="vehicle.raw.go.gb32960.v1"} 700
vehicle_fast_writer_messages_total{status="invalid_json",subject="vehicle.raw.go.gb32960.v1"} 50`,
}, Thresholds{
FastWriterMessageMinReceived: 100,
FastWriterMessageMaxUnackedRatio: 0.20,
FastWriterInvalidJSONMax: -1,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer unacked message ratio 0.25 exceeds 0.20 for subject vehicle.raw.go.gb32960.v1",
"received 1000",
"ok 700",
"invalid_json 50",
"unacked 250",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateAllowsFastWriterCompletedAndLowSampleMessages(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_messages_total{status="received",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_messages_total{status="ok",subject="vehicle.raw.go.jt808.v1"} 90
vehicle_fast_writer_messages_total{status="invalid_json",subject="vehicle.raw.go.jt808.v1"} 10
vehicle_fast_writer_messages_total{status="received",subject="vehicle.raw.go.yutong-mqtt.v1"} 99`,
}, Thresholds{
FastWriterMessageMinReceived: 100,
FastWriterMessageMaxUnackedRatio: 0.20,
FastWriterInvalidJSONMax: -1,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateDoesNotRequireFastWriterForGatewayFieldsPublish(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="fields",protocol="YUTONG_MQTT",status="ok"} 120`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8`,
}, Thresholds{GatewayFastWriterMinRawPublish: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenFastWriterRedisStaleFieldRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.gb32960.v1",status="seen"} 2000
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.gb32960.v1",status="written"} 1500
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.gb32960.v1",status="skipped_stale"} 500`,
}, Thresholds{
FastWriterRedisFieldMinSeen: 1000,
FastWriterRedisFieldMaxStaleRatio: 0.20,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer redis stale field ratio 0.25 exceeds 0.20 for subject vehicle.raw.go.gb32960.v1",
"seen 2000",
"written 1500",
"skipped_stale 500",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenFastWriterRedisEnvelopeUpdatesAreMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.gb32960.v1",status="seen"} 120
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.gb32960.v1",status="skipped_non_realtime"} 120`,
}, Thresholds{
FastWriterRedisEnvelopeMinSeen: 100,
FastWriterRedisEnvelopeMaxBadRatio: 0.50,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer redis realtime updates missing for subject vehicle.raw.go.gb32960.v1",
"envelopes seen 120",
"updated 0",
"skipped non-realtime 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenFastWriterRedisEnvelopeIdentitySkipsAreHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.jt808.v1",status="seen"} 200
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.jt808.v1",status="updated"} 70
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.jt808.v1",status="skipped_missing_vin"} 130`,
}, Thresholds{
FastWriterRedisEnvelopeMinSeen: 100,
FastWriterRedisEnvelopeMaxBadRatio: 0.50,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer redis envelope skip ratio 0.65 exceeds 0.50 for subject vehicle.raw.go.jt808.v1",
"envelopes seen 200",
"updated 70",
"missing vin 130",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenFastWriterRedisParsedFieldSkipsAreHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.gb32960.v1",status="seen"} 200
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.gb32960.v1",status="updated"} 70
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.gb32960.v1",status="skipped_missing_fields"} 130`,
}, Thresholds{
FastWriterRedisEnvelopeMinSeen: 100,
FastWriterRedisEnvelopeMaxBadRatio: 0.50,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer redis envelope skip ratio 0.65 exceeds 0.50 for subject vehicle.raw.go.gb32960.v1",
"missing parsed fields 130",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenFastWriterRedisEnvelopeSkipsAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.yutong-mqtt.v1",status="seen"} 200
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.yutong-mqtt.v1",status="updated"} 180
vehicle_fast_writer_redis_envelopes_total{subject="vehicle.raw.go.yutong-mqtt.v1",status="skipped_missing_vin"} 20`,
}, Thresholds{
FastWriterRedisEnvelopeMinSeen: 100,
FastWriterRedisEnvelopeMaxBadRatio: 0.50,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateDoesNotDegradeWhenFastWriterRedisStaleFieldRatioIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.jt808.v1",status="seen"} 2000
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.jt808.v1",status="written"} 1950
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.jt808.v1",status="skipped_stale"} 50`,
}, Thresholds{
FastWriterRedisFieldMinSeen: 1000,
FastWriterRedisFieldMaxStaleRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsFastWriterRedisStaleFieldRatioWhenSamplesAreLowOrRollup(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.yutong-mqtt.v1",status="seen"} 999
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.yutong-mqtt.v1",status="written"} 1
vehicle_fast_writer_redis_fields_total{subject="vehicle.raw.go.yutong-mqtt.v1",status="skipped_stale"} 998
vehicle_fast_writer_redis_fields_total{subject="mixed",status="seen"} 2000
vehicle_fast_writer_redis_fields_total{subject="mixed",status="written"} 1
vehicle_fast_writer_redis_fields_total{subject="mixed",status="skipped_stale"} 1999`,
}, Thresholds{
FastWriterRedisFieldMinSeen: 1000,
FastWriterRedisFieldMaxStaleRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsGatewayFastWriterFlowWhenSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 99`,
}, Thresholds{GatewayFastWriterMinRawPublish: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenHistoryParsedFieldsAreMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_history_parsed_fields_total{protocol="GB32960",status="present",topic="vehicle.raw.go.gb32960.v1"} 90
vehicle_history_parsed_fields_total{protocol="GB32960",status="missing",topic="vehicle.raw.go.gb32960.v1"} 10`,
}, Thresholds{
HistoryParsedFieldMinFrames: 100,
HistoryParsedFieldMaxMissingRatio: 0.05,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history parsed fields missing ratio 0.10 exceeds 0.05 for topic vehicle.raw.go.gb32960.v1 protocol GB32960",
"total 100",
"present 90",
"missing 10",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenHistoryParsedFieldMissingRatioIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_parsed_fields_total{protocol="JT808",status="present",topic="vehicle.raw.go.jt808.v1"} 98
vehicle_history_parsed_fields_total{protocol="JT808",status="missing",topic="vehicle.raw.go.jt808.v1"} 2`,
}, Thresholds{
HistoryParsedFieldMinFrames: 100,
HistoryParsedFieldMaxMissingRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsHistoryParsedFieldCheckWhenSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_history_parsed_fields_total{protocol="YUTONG_MQTT",status="present",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_history_parsed_fields_total{protocol="YUTONG_MQTT",status="missing",topic="vehicle.raw.go.yutong-mqtt.v1"} 98`,
}, Thresholds{
HistoryParsedFieldMinFrames: 100,
HistoryParsedFieldMaxMissingRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenGatewayParseBadRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_frames_total{protocol="JT808",status="OK"} 90
vehicle_gateway_frames_total{protocol="JT808",status="PARTIAL"} 5
vehicle_gateway_frames_total{protocol="JT808",status="BAD_FRAME"} 10`,
}, Thresholds{
GatewayParseMinFrames: 100,
GatewayParseMaxBadRatio: 0.05,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway parse bad ratio 0.14 exceeds 0.05 for protocol JT808",
"total 105",
"ok 90",
"bad 15",
"partial 5",
"bad_frame 10",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenGatewayParseBadRatioIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_frames_total{protocol="GB32960",status="OK"} 99
vehicle_gateway_frames_total{protocol="GB32960",status="BAD_FRAME"} 1`,
}, Thresholds{
GatewayParseMinFrames: 100,
GatewayParseMaxBadRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsGatewayParseQualityWhenSampleCountIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_frames_total{protocol="YUTONG_MQTT",status="OK"} 1
vehicle_gateway_frames_total{protocol="YUTONG_MQTT",status="BAD_FRAME"} 9`,
}, Thresholds{
GatewayParseMinFrames: 100,
GatewayParseMaxBadRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenGatewayResponseErrorRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_response_total{message_id="0x0100",protocol="JT808",status="ok"} 90
vehicle_gateway_response_total{message_id="0x0100",protocol="JT808",status="build_error"} 5
vehicle_gateway_response_total{message_id="0x0200",protocol="JT808",status="write_error"} 10
vehicle_gateway_response_total{message_id="0x0001",protocol="JT808",status="skipped"} 1000`,
}, Thresholds{
GatewayResponseMinSamples: 100,
GatewayResponseMaxErrorRatio: 0.05,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway response error ratio 0.14 exceeds 0.05 for protocol JT808",
"total 105",
"ok 90",
"errors 15",
"build_error 5",
"write_error 10",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenGatewayResponseErrorRatioIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_response_total{message_id="0x05",protocol="GB32960",status="ok"} 99
vehicle_gateway_response_total{message_id="0x05",protocol="GB32960",status="write_error"} 1`,
}, Thresholds{
GatewayResponseMinSamples: 100,
GatewayResponseMaxErrorRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsGatewayResponseQualityWhenSampleCountIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_response_total{message_id="0x0200",protocol="JT808",status="write_error"} 9
vehicle_gateway_response_total{message_id="0x0001",protocol="JT808",status="skipped"} 1000`,
}, Thresholds{
GatewayResponseMinSamples: 100,
GatewayResponseMaxErrorRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenGatewayIdentityUnresolvedRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="JT808",status="resolved"} 60
vehicle_gateway_identity_total{protocol="JT808",status="unresolved"} 30
vehicle_gateway_identity_total{protocol="JT808",status="timeout"} 10`,
}, Thresholds{
GatewayIdentityMinSamples: 100,
GatewayIdentityMaxUnresolvedRatio: 0.20,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway identity unresolved ratio 0.40 exceeds 0.20 for protocol JT808",
"total 100",
"unresolved 40",
"resolved 60",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateIncludesIdentityResolutionDiagnostics(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="JT808",status="resolved"} 60
vehicle_gateway_identity_total{protocol="JT808",status="unresolved"} 30
vehicle_gateway_identity_total{protocol="JT808",status="timeout"} 7
vehicle_gateway_identity_total{protocol="JT808",status="error"} 3
vehicle_gateway_identity_issues_total{protocol="JT808",status="unresolved",message_id="0x0200",reason="no_binding"} 30
vehicle_gateway_identity_issues_total{protocol="JT808",status="timeout",message_id="0x0200",reason="timeout"} 7
vehicle_gateway_identity_issues_total{protocol="JT808",status="error",message_id="0x0100",reason="resolver_error"} 3
vehicle_gateway_identity_cache_total{protocol="JT808",cache_status="hit"} 80
vehicle_gateway_identity_cache_total{protocol="JT808",cache_status="stale"} 5
vehicle_gateway_identity_cache_entries{cache="lookup"} 123
vehicle_gateway_identity_cache_entries{cache="registration"} 45
vehicle_gateway_identity_skips_total{protocol="GB32960",reason="non_vehicle_frame"} 12
vehicle_gateway_identity_total{protocol="GB32960",status="resolved"} 100`,
}, Thresholds{})
if report.IdentityResolution == nil {
t.Fatal("identity resolution diagnostics = nil")
}
got := report.IdentityResolution
if got.Total != 200 || got.ResolvedCount != 160 || got.NonResolvedCount != 40 {
t.Fatalf("identity totals = %#v", got)
}
if got.SkippedCount != 12 {
t.Fatalf("identity skipped = %d, want 12", got.SkippedCount)
}
if got.UnresolvedCount != 30 || got.TimeoutCount != 7 || got.ErrorCount != 3 {
t.Fatalf("identity error split = %#v", got)
}
if len(got.Issues) != 3 {
t.Fatalf("identity issues = %#v, want 3 rows", got.Issues)
}
if got.Issues[0] != (IdentityIssueCount{Protocol: "JT808", Status: "error", MessageID: "0x0100", Reason: "resolver_error", Count: 3}) {
t.Fatalf("first identity issue = %#v", got.Issues[0])
}
if got.Issues[1] != (IdentityIssueCount{Protocol: "JT808", Status: "timeout", MessageID: "0x0200", Reason: "timeout", Count: 7}) {
t.Fatalf("second identity issue = %#v", got.Issues[1])
}
if got.Issues[2] != (IdentityIssueCount{Protocol: "JT808", Status: "unresolved", MessageID: "0x0200", Reason: "no_binding", Count: 30}) {
t.Fatalf("third identity issue = %#v", got.Issues[2])
}
if got.CacheEntries["lookup"] != 123 || got.CacheEntries["registration"] != 45 {
t.Fatalf("cache entries = %#v", got.CacheEntries)
}
if len(got.Protocols) != 2 || got.Protocols[0].Protocol != "GB32960" || got.Protocols[1].Protocol != "JT808" {
t.Fatalf("protocol rows = %#v", got.Protocols)
}
if got.Protocols[0].SkippedCount != 12 || got.Protocols[0].NonResolvedCount != 0 {
t.Fatalf("gb32960 skipped row = %#v", got.Protocols[0])
}
jt808 := got.Protocols[1]
if jt808.Total != 100 || jt808.NonResolvedCount != 40 || jt808.NonResolvedRatio != 0.40 {
t.Fatalf("jt808 row = %#v", jt808)
}
if jt808.StaleCacheCount != 5 || jt808.StaleCacheRatio != 0.05 || jt808.CacheStatusCounts["stale"] != 5 {
t.Fatalf("jt808 cache row = %#v", jt808)
}
}
func TestEvaluateDoesNotDegradeWhenGatewayIdentityUnresolvedRatioIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="GB32960",status="resolved"} 98
vehicle_gateway_identity_total{protocol="GB32960",status="unresolved"} 2`,
}, Thresholds{
GatewayIdentityMinSamples: 100,
GatewayIdentityMaxUnresolvedRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsGatewayIdentityQualityWhenSampleCountIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="YUTONG_MQTT",status="resolved"} 2
vehicle_gateway_identity_total{protocol="YUTONG_MQTT",status="unresolved"} 8`,
}, Thresholds{
GatewayIdentityMinSamples: 100,
GatewayIdentityMaxUnresolvedRatio: 0.20,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsGatewayIdentityCacheEntriesOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="lookup"} 300001`,
}, Thresholds{GatewayIdentityCacheEntriesMax: 300000})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway identity cache entries 300001 exceeds 300000 for cache lookup") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsGatewayIdentitySnapshotNotReady(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_snapshot_ready 0`,
}, Thresholds{
GatewayIdentitySnapshotRequired: 1,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway identity snapshot ready 0 below 1") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsGatewayIdentitySnapshotTooOld(t *testing.T) {
lastSuccess := time.Now().Add(-10 * time.Minute).Unix()
report := EvaluateWithThresholds(map[string]string{
"gateway": fmt.Sprintf(`vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_snapshot_ready 1
vehicle_gateway_identity_snapshot_last_success_unix_seconds %d`, lastSuccess),
}, Thresholds{
GatewayIdentitySnapshotRequired: 1,
GatewayIdentitySnapshotMaxAgeSec: 180,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway identity snapshot age") || !strings.Contains(joined, "exceeds 180s") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateAllowsFreshGatewayIdentitySnapshot(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": fmt.Sprintf(`vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_snapshot_ready 1
vehicle_gateway_identity_snapshot_last_success_unix_seconds %d`, time.Now().Unix()),
}, Thresholds{
GatewayIdentitySnapshotRequired: 1,
GatewayIdentitySnapshotMaxAgeSec: 180,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
}
func TestEvaluateReportsGatewayIdentityLocationTouchFailures(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="location_touch_failure"} 2`,
}, Thresholds{GatewayIdentityLocationTouchFailureMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway identity location touch failure cache 2 exceeds 0") {
t.Fatalf("finding missing in:\n%s", joined)
}
if !strings.Contains(joined, "jt808 registration writes are failing or in protective backoff") {
t.Fatalf("finding should explain registration write backoff in:\n%s", joined)
}
}
func TestEvaluateReportsGatewayRegistrationWriteQueueDepth(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="lookup"} 500000
vehicle_gateway_identity_cache_entries{cache="registration_write_queue"} 12001
vehicle_gateway_identity_cache_max_entries{cache="registration_write_queue"} 100000`,
}, Thresholds{
GatewayIdentityRegistrationWriteQueueDepthMax: 10000,
GatewayIdentityRegistrationWriteQueueMaxUsageRatio: 0.80,
GatewayIdentityCacheEntriesMax: 0,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
if report.Totals.JT808RegistrationWriteQueuePending != 12001 {
t.Fatalf("registration write queue pending = %v, want 12001", report.Totals.JT808RegistrationWriteQueuePending)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway jt808 registration write queue depth 12001 exceeds 10000") {
t.Fatalf("depth finding missing in:\n%s", joined)
}
}
func TestEvaluateReportsGatewayRegistrationWriteQueueUsage(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="registration_write_queue"} 90
vehicle_gateway_identity_cache_max_entries{cache="registration_write_queue"} 100`,
}, Thresholds{
GatewayIdentityRegistrationWriteQueueDepthMax: 0,
GatewayIdentityRegistrationWriteQueueMaxUsageRatio: 0.80,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway jt808 registration write queue usage 0.90 exceeds 0.80",
"depth 90",
"capacity 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableGatewayRegistrationWriteQueueLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="registration_write_queue"} 100
vehicle_gateway_identity_cache_max_entries{cache="registration_write_queue"} 100`,
}, Thresholds{
GatewayIdentityRegistrationWriteQueueDepthMax: 0,
GatewayIdentityRegistrationWriteQueueMaxUsageRatio: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if report.Totals.JT808RegistrationWriteQueuePending != 100 {
t.Fatalf("registration write queue pending = %v, want 100", report.Totals.JT808RegistrationWriteQueuePending)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsGatewayRegistrationWriteErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_jt808_registration_write_total{mode="async_enqueue",status="ok"} 100
vehicle_gateway_jt808_registration_write_total{mode="async_background",status="error"} 2
vehicle_gateway_jt808_registration_write_total{mode="sync",status="error"} 1`,
}, Thresholds{
GatewayIdentityRegistrationWriteErrorMax: 0,
GatewayIdentityRegistrationWriteQueueDepthMax: 0,
GatewayIdentityRegistrationWriteQueueMaxUsageRatio: 0,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
if report.Totals.JT808RegistrationWriteErrors != 3 {
t.Fatalf("registration write errors = %v, want 3", report.Totals.JT808RegistrationWriteErrors)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway jt808 registration write error count 2 exceeds 0 for mode async_background",
"gateway jt808 registration write error count 1 exceeds 0 for mode sync",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if strings.Contains(joined, "async_enqueue") {
t.Fatalf("ok status should not produce finding:\n%s", joined)
}
}
func TestEvaluateCanDisableGatewayRegistrationWriteErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_jt808_registration_write_total{mode="async_background",status="error"} 2`,
}, Thresholds{
GatewayIdentityRegistrationWriteErrorMax: -1,
GatewayIdentityRegistrationWriteQueueDepthMax: 0,
GatewayIdentityRegistrationWriteQueueMaxUsageRatio: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if report.Totals.JT808RegistrationWriteErrors != 2 {
t.Fatalf("registration write errors = %v, want 2", report.Totals.JT808RegistrationWriteErrors)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestGatewayIdentityCacheLimitIgnoresRegistrationWriteQueue(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="registration_write_queue"} 500000`,
}, Thresholds{
GatewayIdentityCacheEntriesMax: 300000,
GatewayIdentityRegistrationWriteQueueDepthMax: 0,
GatewayIdentityRegistrationWriteQueueMaxUsageRatio: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableGatewayIdentityLocationTouchFailureLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="location_touch_failure"} 99`,
}, Thresholds{GatewayIdentityLocationTouchFailureMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateAllowsGatewayIdentityCacheEntriesUnderLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="registration"} 299999`,
}, Thresholds{GatewayIdentityCacheEntriesMax: 300000})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableGatewayIdentityCacheEntryLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_cache_entries{cache="lookup"} 9999999`,
}, Thresholds{GatewayIdentityCacheEntriesMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsGatewayIdentityStaleCacheRatioOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="JT808",status="resolved"} 90
vehicle_gateway_identity_total{protocol="JT808",status="unresolved"} 10
vehicle_gateway_identity_cache_total{protocol="JT808",cache_status="stale"} 9`,
}, Thresholds{
GatewayIdentityStaleMinSamples: 100,
GatewayIdentityMaxStaleCacheRatio: 0.05,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "gateway identity stale cache ratio 0.09 exceeds 0.05 for protocol JT808") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateAllowsGatewayIdentityStaleCacheRatioUnderLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="GB32960",status="resolved"} 100
vehicle_gateway_identity_cache_total{protocol="GB32960",cache_status="stale"} 3`,
}, Thresholds{
GatewayIdentityStaleMinSamples: 100,
GatewayIdentityMaxStaleCacheRatio: 0.05,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableGatewayIdentityStaleCacheRatio(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_total{protocol="JT808",status="resolved"} 100
vehicle_gateway_identity_cache_total{protocol="JT808",cache_status="stale"} 100`,
}, Thresholds{
GatewayIdentityStaleMinSamples: 0,
GatewayIdentityMaxStaleCacheRatio: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenRequiredConsumerTopicIsMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1`,
}, Thresholds{
RequiredConsumerTopics: map[string][]string{
"history": {
"vehicle.raw.go.gb32960.v1",
"vehicle.raw.go.jt808.v1",
},
},
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "history required kafka consumer topic missing: vehicle.raw.go.jt808.v1") {
t.Fatalf("required topic finding missing in:\n%s", joined)
}
}
func TestEvaluateDoesNotDegradeWhenRequiredConsumerTopicsArePresent(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1`,
}, Thresholds{
RequiredConsumerTopics: map[string][]string{
"stat": {
"vehicle.fields.go.gb32960.v1",
"vehicle.fields.go.jt808.v1",
},
},
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestDefaultRequiredConsumerTopicsIncludesIdentityWriter(t *testing.T) {
required := DefaultRequiredConsumerTopics()
if got := strings.Join(required["identity"], ","); got != "vehicle.raw.go.jt808.v1" {
t.Fatalf("identity required topics = %q, want JT808 raw", got)
}
}
func TestEvaluateRejectsDuplicateJT808RegistrationWriters(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_jt808_registration_gateway_writes_enabled 1`,
"identity": `vehicle_service_info{service="vehicle-identity-writer"} 1
vehicle_kafka_consumer_info{service="vehicle-identity-writer",group="go-identity-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
if joined := strings.Join(report.Findings, "\n"); !strings.Contains(joined, "duplicate writers") {
t.Fatalf("duplicate writer finding missing in:\n%s", joined)
}
}
func TestEvaluateAcceptsDelegatedJT808RegistrationWriter(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_jt808_registration_gateway_writes_enabled 0`,
"identity": `vehicle_service_info{service="vehicle-identity-writer"} 1
vehicle_kafka_consumer_info{service="vehicle-identity-writer",group="go-identity-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_identity_writer_batch_pending_messages 0
vehicle_identity_writer_batch_pending_facts 0
vehicle_identity_writer_kafka_lag{topic="vehicle.raw.go.jt808.v1",partition="0"} 0`,
}, Thresholds{})
if report.Status != StatusOK || len(report.Findings) != 0 {
t.Fatalf("report = %#v, want ok without findings", report)
}
}
func TestEvaluateReportsDegradedWhenRawFanoutDoesNotReachHistoryOrRealtime(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 120`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1`,
}, Thresholds{RawFanoutMinBridge: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history messages missing for vehicle.raw.go.jt808.v1",
"realtime messages missing for vehicle.raw.go.jt808.v1",
"bridge raw writes ok 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenRawFanoutIsConsumedButNotWritten(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.raw.go.gb32960.v1"} 120`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_history_kafka_messages_total{status="received",topic="vehicle.raw.go.gb32960.v1"} 120`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_realtime_kafka_messages_total{status="received",topic="vehicle.raw.go.gb32960.v1"} 120`,
}, Thresholds{RawFanoutMinBridge: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history writes missing for vehicle.raw.go.gb32960.v1",
"realtime updates missing for vehicle.raw.go.gb32960.v1",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenRawFanoutIsComplete(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.raw.go.yutong-mqtt.v1"} 120`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_history_kafka_messages_total{status="received",topic="vehicle.raw.go.yutong-mqtt.v1"} 120
vehicle_history_writes_total{status="ok",topic="vehicle.raw.go.yutong-mqtt.v1"} 120`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_realtime_kafka_messages_total{status="received",topic="vehicle.raw.go.yutong-mqtt.v1"} 120
vehicle_realtime_updates_total{status="ok",topic="vehicle.raw.go.yutong-mqtt.v1"} 120`,
}, Thresholds{RawFanoutMinBridge: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsRawFanoutWhenBridgeSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.raw.go.jt808.v1"} 99`,
}, Thresholds{RawFanoutMinBridge: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenBridgeFieldsTopicIsNotConsumedByStat(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 120`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1`,
}, Thresholds{StatTopicMinBridge: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat messages missing for vehicle.fields.go.jt808.v1",
"bridge writes ok 120",
"stat received 0",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenBridgeFieldsTopicIsNotConfiguredByStat(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_writes_total{status="ok",topic="vehicle.fields.go.gb32960.v1"} 120`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1`,
}, Thresholds{StatTopicMinBridge: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat consumer topic missing for vehicle.fields.go.gb32960.v1",
"bridge writes ok 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenStatWritesHaveNoExtractedSamples(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 120
vehicle_stat_samples_total{protocol="JT808",status="skipped_missing_fields",topic="vehicle.fields.go.jt808.v1"} 120`,
}, Thresholds{StatSampleMinWrites: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat samples missing for vehicle.fields.go.jt808.v1",
"writes ok 120",
"missing fields 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenStatWritesAreOnlyNonMileageFrames(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.yutong-mqtt.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.yutong-mqtt.v1"} 120
vehicle_stat_samples_total{protocol="YUTONG_MQTT",status="skipped_non_mileage_frame",topic="vehicle.fields.go.yutong-mqtt.v1"} 120`,
}, Thresholds{StatSampleMinWrites: 100, StatSampleMaxActionableSkipRatio: 0.20})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
if report.ProtocolFlow == nil || len(report.ProtocolFlow.Protocols) != 1 {
t.Fatalf("protocol flow diagnostics missing: %#v", report.ProtocolFlow)
}
row := report.ProtocolFlow.Protocols[0]
if row.Protocol != "YUTONG_MQTT" || row.Status != "ok" || row.StatMileageCandidateWrites != 0 || row.StatSkippedNonMileageFrame != 120 || row.StatSamplesActionableSkipped != 0 {
t.Fatalf("protocol flow row = %#v", row)
}
}
func TestEvaluateDoesNotDegradeWhenStatWritesOnlyMissSourceMileage(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.yutong-mqtt.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.yutong-mqtt.v1"} 120
vehicle_stat_samples_total{protocol="YUTONG_MQTT",status="skipped_missing_mileage",topic="vehicle.fields.go.yutong-mqtt.v1"} 120`,
}, Thresholds{StatSampleMinWrites: 100, StatSampleMaxActionableSkipRatio: 0.20})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
if report.ProtocolFlow == nil || len(report.ProtocolFlow.Protocols) != 1 {
t.Fatalf("protocol flow diagnostics missing: %#v", report.ProtocolFlow)
}
row := report.ProtocolFlow.Protocols[0]
if row.Protocol != "YUTONG_MQTT" || row.Status != "ok" || row.StatMileageCandidateWrites != 120 || row.StatSkippedMissingMileage != 120 || row.StatSamplesActionableSkipped != 120 {
t.Fatalf("protocol flow row = %#v", row)
}
}
func TestEvaluateUsesMileageCandidatesForStatActionableSkipRatio(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.gb32960.v1"} 1000
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.gb32960.v1"} 50
vehicle_stat_samples_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 40
vehicle_stat_samples_total{protocol="GB32960",status="skipped_non_mileage_frame",topic="vehicle.fields.go.gb32960.v1"} 900
vehicle_stat_samples_total{protocol="GB32960",status="skipped_missing_mileage",topic="vehicle.fields.go.gb32960.v1"} 50`,
}, Thresholds{
StatSampleMinWrites: 100,
StatSampleMaxActionableSkipRatio: 0.40,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat actionable skip ratio 0.50 exceeds 0.40 for vehicle.fields.go.gb32960.v1",
"writes ok 1000",
"mileage candidates 100",
"non-mileage frames 900",
"missing mileage 50",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if report.ProtocolFlow == nil || len(report.ProtocolFlow.Protocols) != 1 {
t.Fatalf("protocol flow diagnostics missing: %#v", report.ProtocolFlow)
}
row := report.ProtocolFlow.Protocols[0]
if row.StatMileageCandidateWrites != 100 || row.StatSamplesActionableSkipped != 50 || row.Status != "degraded" || row.Reason != "stat_actionable_skip_ratio" {
t.Fatalf("protocol flow row = %#v", row)
}
}
func TestEvaluateDoesNotDegradeWhenStatSamplesAreOnlyDuplicateMileage(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.yutong-mqtt.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.yutong-mqtt.v1"} 120
vehicle_stat_samples_total{protocol="YUTONG_MQTT",status="found",topic="vehicle.fields.go.yutong-mqtt.v1"} 120
vehicle_stat_samples_total{protocol="YUTONG_MQTT",status="skipped_same_mileage",topic="vehicle.fields.go.yutong-mqtt.v1"} 120`,
}, Thresholds{StatSampleMinWrites: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenStatActionableSkipRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.gb32960.v1"} 200
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.gb32960.v1"} 80
vehicle_stat_samples_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 70
vehicle_stat_samples_total{protocol="GB32960",status="skipped_missing_mileage",topic="vehicle.fields.go.gb32960.v1"} 100
vehicle_stat_samples_total{protocol="GB32960",status="skipped_non_positive_mileage",topic="vehicle.fields.go.gb32960.v1"} 20`,
}, Thresholds{
StatSampleMinWrites: 100,
StatSampleMaxActionableSkipRatio: 0.50,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat actionable skip ratio 0.60 exceeds 0.50 for vehicle.fields.go.gb32960.v1",
"writes ok 200",
"missing mileage 100",
"non-positive mileage 20",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateTreatsMissingTimeAsActionableStatSkip(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 200
vehicle_stat_samples_total{protocol="JT808",status="found",topic="vehicle.fields.go.jt808.v1"} 80
vehicle_stat_samples_total{protocol="JT808",status="written",topic="vehicle.fields.go.jt808.v1"} 70
vehicle_stat_samples_total{protocol="JT808",status="skipped_missing_time",topic="vehicle.fields.go.jt808.v1"} 120`,
}, Thresholds{
StatSampleMinWrites: 100,
StatSampleMaxActionableSkipRatio: 0.50,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat actionable skip ratio 0.60 exceeds 0.50 for vehicle.fields.go.jt808.v1",
"missing time 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsFutureEventTimeAdjustments(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.gb32960.v1"} 200
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.gb32960.v1"} 190
vehicle_stat_samples_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 180
vehicle_stat_samples_total{protocol="GB32960",status="event_time_future_adjusted",topic="vehicle.fields.go.gb32960.v1"} 3`,
}, Thresholds{
StatSampleMinWrites: 100,
StatSampleFutureAdjustmentMax: 0,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "stat future event time adjustments 3 exceeds 0 for vehicle.fields.go.gb32960.v1") {
t.Fatalf("future adjustment finding missing in:\n%s", joined)
}
if report.ProtocolFlow == nil || len(report.ProtocolFlow.Protocols) != 1 {
t.Fatalf("protocol flow diagnostics missing: %#v", report.ProtocolFlow)
}
row := report.ProtocolFlow.Protocols[0]
if row.Protocol != "GB32960" || row.StatEventTimeFutureAdjusted != 3 || row.Status != "degraded" || row.Reason != "stat_future_event_time_adjusted" {
t.Fatalf("protocol flow row = %#v", row)
}
}
func TestEvaluateDoesNotDegradeWhenStatActionableSkipRatioIsLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.jt808.v1"} 200
vehicle_stat_samples_total{protocol="JT808",status="found",topic="vehicle.fields.go.jt808.v1"} 180
vehicle_stat_samples_total{protocol="JT808",status="written",topic="vehicle.fields.go.jt808.v1"} 150
vehicle_stat_samples_total{protocol="JT808",status="skipped_missing_mileage",topic="vehicle.fields.go.jt808.v1"} 20`,
}, Thresholds{
StatSampleMinWrites: 100,
StatSampleMaxActionableSkipRatio: 0.50,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenStatSamplesHaveNoSource(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_writes_total{status="ok",topic="vehicle.fields.go.gb32960.v1"} 120
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.gb32960.v1"} 120
vehicle_stat_samples_total{protocol="GB32960",status="skipped_missing_source",topic="vehicle.fields.go.gb32960.v1"} 120`,
}, Thresholds{StatSampleMinWrites: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat samples not written for vehicle.fields.go.gb32960.v1",
"found 120",
"skipped missing source 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenStatSourceTrackingIsMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_stat_samples_total{protocol="JT808",status="found",topic="vehicle.fields.go.jt808.v1"} 120`,
}, Thresholds{StatSourceMinSamples: 100, StatSourceMaxMissingRatio: 0.60})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat source tracking missing for vehicle.fields.go.jt808.v1",
"samples found 120",
"source written 0",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsDegradedWhenStatSourceMissingEndpointRatioIsHigh(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_stat_samples_total{protocol="YUTONG_MQTT",status="found",topic="vehicle.fields.go.yutong-mqtt.v1"} 200
vehicle_stat_sources_total{protocol="YUTONG_MQTT",status="written",topic="vehicle.fields.go.yutong-mqtt.v1"} 10
vehicle_stat_sources_total{protocol="YUTONG_MQTT",status="skipped_throttled",topic="vehicle.fields.go.yutong-mqtt.v1"} 50
vehicle_stat_sources_total{protocol="YUTONG_MQTT",status="skipped_missing_endpoint",topic="vehicle.fields.go.yutong-mqtt.v1"} 140`,
}, Thresholds{StatSourceMinSamples: 100, StatSourceMaxMissingRatio: 0.60})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat source missing endpoint ratio 0.70 exceeds 0.60 for vehicle.fields.go.yutong-mqtt.v1",
"samples found 200",
"missing endpoint 140",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenStatSourceTrackingIsHealthy(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_samples_total{protocol="GB32960",status="found",topic="vehicle.fields.go.gb32960.v1"} 200
vehicle_stat_sources_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_sources_total{protocol="GB32960",status="skipped_throttled",topic="vehicle.fields.go.gb32960.v1"} 199`,
}, Thresholds{StatSourceMinSamples: 100, StatSourceMaxMissingRatio: 0.60})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedWhenStatFinalProjectionIsMissing(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_samples_total{protocol="JT808",status="written",topic="vehicle.fields.go.jt808.v1"} 120
vehicle_stat_projections_total{protocol="JT808",status="attempted",topic="vehicle.fields.go.jt808.v1"} 120
vehicle_stat_projections_total{protocol="JT808",status="skipped_throttled",topic="vehicle.fields.go.jt808.v1"} 30`,
}, Thresholds{StatProjectionMinSampleWrites: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat final projection missing for vehicle.fields.go.jt808.v1",
"samples written 120",
"projections attempted 120",
"written 0",
"skipped throttled 30",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateDoesNotDegradeWhenStatFinalProjectionExists(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_samples_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 120
vehicle_stat_projections_total{protocol="GB32960",status="written",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_projections_total{protocol="GB32960",status="skipped_throttled",topic="vehicle.fields.go.gb32960.v1"} 119`,
}, Thresholds{StatProjectionMinSampleWrites: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateSkipsStatFinalProjectionCheckWhenSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.yutong-mqtt.v1"} 1
vehicle_stat_samples_total{protocol="YUTONG_MQTT",status="written",topic="vehicle.fields.go.yutong-mqtt.v1"} 99
vehicle_stat_projections_total{protocol="YUTONG_MQTT",status="attempted",topic="vehicle.fields.go.yutong-mqtt.v1"} 99`,
}, Thresholds{StatProjectionMinSampleWrites: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsStatCacheEntriesOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_cache_entries{cache="last_total_mileage"} 1200
vehicle_stat_cache_entries{cache="source_seen"} 12`,
}, Thresholds{StatCacheEntriesMax: 1000})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat cache entries 1200 exceeds 1000 for cache last_total_mileage",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateAllowsStatCacheEntriesUnderLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_cache_entries{cache="last_total_mileage"} 999
vehicle_stat_cache_entries{cache="source_seen"} 12`,
}, Thresholds{StatCacheEntriesMax: 1000})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableStatCacheEntryLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_cache_entries{cache="baseline"} 9999999`,
}, Thresholds{StatCacheEntriesMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateAllowsRealtimeThrottleWhenSomeWritesPass(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_throttle_total{protocol="JT808",status="passed",store="mysql"} 20
vehicle_realtime_throttle_total{protocol="JT808",status="skipped_interval",store="mysql"} 180`,
}, Thresholds{RealtimeThrottleMinSamples: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimeThrottleWhenWritesNeverPass(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_realtime_throttle_total{protocol="GB32960",status="skipped_interval",store="mysql"} 120`,
}, Thresholds{RealtimeThrottleMinSamples: 100})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"realtime throttle passing updates missing for store=mysql,protocol=GB32960",
"total 120",
"passed 0",
"skipped interval 120",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateSkipsRealtimeThrottleWhenSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.yutong-mqtt.v1"} 1
vehicle_realtime_throttle_total{protocol="YUTONG_MQTT",status="skipped_interval",store="mysql"} 99`,
}, Thresholds{RealtimeThrottleMinSamples: 100})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimeThrottleCacheEntriesOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_throttle_cache_entries{store="mysql"} 1200`,
}, Thresholds{RealtimeThrottleCacheEntriesMax: 1000})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime throttle cache entries 1200 exceeds 1000 for store mysql") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateAllowsRealtimeThrottleCacheEntriesUnderLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_throttle_cache_entries{store="mysql"} 999`,
}, Thresholds{RealtimeThrottleCacheEntriesMax: 1000})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableRealtimeThrottleCacheEntryLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_throttle_cache_entries{store="mysql"} 9999999`,
}, Thresholds{RealtimeThrottleCacheEntriesMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimeStoreUpdateErrors(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_store_updates_total{protocol="JT808",status="error",store="mysql"} 2`,
}, Thresholds{RealtimeStoreErrorMax: 0})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime store update errors 2 exceeds 0 for store mysql protocol JT808") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateCanDisableRealtimeStoreUpdateErrorLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_store_updates_total{protocol="JT808",status="error",store="mysql"} 99`,
}, Thresholds{RealtimeStoreErrorMax: -1})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimeAsyncQueueDroppedAndUsage(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_async_queue_total{protocol="JT808",status="dropped",store="mysql"} 1
vehicle_realtime_async_queue_total{protocol="JT808",status="closed",store="mysql"} 1
vehicle_realtime_async_queue_depth{protocol="JT808",store="mysql"} 90
vehicle_realtime_async_queue_capacity{store="mysql"} 100`,
}, Thresholds{
RealtimeAsyncQueueDroppedMax: 0,
RealtimeAsyncQueueMaxUsageRatio: 0.80,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"realtime async queue dropped 1 exceeds 0 for store mysql protocol JT808",
"realtime async queue closed 1 exceeds 0 for store mysql protocol JT808",
"realtime async queue usage 0.90 exceeds 0.80",
"protocol=JT808,store=mysql",
"depth 90",
"capacity 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateCanDisableRealtimeAsyncQueueLimits(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_async_queue_total{protocol="JT808",status="dropped",store="mysql"} 99
vehicle_realtime_async_queue_depth{protocol="JT808",store="mysql"} 999
vehicle_realtime_async_queue_capacity{store="mysql"} 100`,
}, Thresholds{
RealtimeAsyncQueueDepthMax: 0,
RealtimeAsyncQueueDroppedMax: -1,
RealtimeAsyncQueueMaxUsageRatio: 0,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsRealtimePlateCacheEntriesOverLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_plate_cache_entries 250000`,
}, Thresholds{RealtimePlateCacheEntriesMax: 200000})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
if !strings.Contains(joined, "realtime plate cache entries 250000 exceeds 200000") {
t.Fatalf("finding missing in:\n%s", joined)
}
}
func TestEvaluateAllowsRealtimePlateCacheEntriesUnderLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_plate_cache_entries 199999`,
}, Thresholds{RealtimePlateCacheEntriesMax: 200000})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateCanDisableRealtimePlateCacheEntryLimit(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_plate_cache_entries 9999999`,
}, Thresholds{RealtimePlateCacheEntriesMax: 0})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Findings) != 0 {
t.Fatalf("findings = %#v, want none", report.Findings)
}
}
func TestEvaluateReportsDegradedForSlowGatewayIdentityP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_identity_duration_ms_histogram_bucket{le="1",protocol="JT808",status="resolved"} 10
vehicle_gateway_identity_duration_ms_histogram_bucket{le="5",protocol="JT808",status="resolved"} 80
vehicle_gateway_identity_duration_ms_histogram_bucket{le="100",protocol="JT808",status="resolved"} 98
vehicle_gateway_identity_duration_ms_histogram_bucket{le="250",protocol="JT808",status="resolved"} 100
vehicle_gateway_identity_duration_ms_histogram_bucket{le="+Inf",protocol="JT808",status="resolved"} 100
vehicle_gateway_identity_duration_ms_histogram_count{protocol="JT808",status="resolved"} 100
vehicle_gateway_identity_duration_ms_histogram_sum{protocol="JT808",status="resolved"} 350`,
}, Thresholds{
GatewayIdentityP99MS: 100,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway identity p99 250ms exceeds 100ms",
"protocol=JT808,status=resolved",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateSkipsLatencyFindingsWhenHistogramSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_frame_duration_ms_histogram_bucket{le="250",protocol="GB32960",status="OK"} 1
vehicle_gateway_frame_duration_ms_histogram_bucket{le="+Inf",protocol="GB32960",status="OK"} 1
vehicle_gateway_frame_duration_ms_histogram_count{protocol="GB32960",status="OK"} 1`,
}, Thresholds{
GatewayFrameP99MS: 100,
MinHistogramSamples: 10,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
}
func TestEvaluateReportsSlowFastWriterRedisE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_e2e_duration_ms_histogram_bucket{le="100",subject="vehicle.raw.go.jt808.v1"} 90
vehicle_fast_writer_redis_e2e_duration_ms_histogram_bucket{le="1000",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_redis_e2e_duration_ms_histogram_bucket{le="+Inf",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_redis_e2e_duration_ms_histogram_count{subject="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
FastWriterRedisE2EP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer redis e2e p99 1000ms exceeds 300ms",
"subject=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsSlowFastWriterRedisRecentE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_redis_e2e_recent_p99_ms{subject="vehicle.raw.go.jt808.v1"} 180
vehicle_fast_writer_redis_e2e_recent_samples{subject="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
FastWriterRedisRecentE2EP99MS: 100,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"fast writer redis e2e recent p99 180ms exceeds 100ms",
"subject=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsSlowGatewayResponseRecentP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"gateway": `vehicle_service_info{service="vehicle-gateway"} 1
vehicle_gateway_response_e2e_recent_p99_ms{protocol="JT808"} 180
vehicle_gateway_response_e2e_recent_samples{protocol="JT808"} 100`,
}, Thresholds{
GatewayResponseRecentP99MS: 100,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"gateway response e2e recent p99 180ms exceeds 100ms",
"protocol=JT808",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
if report.PipelineLatency == nil || len(report.PipelineLatency.Items) != 1 {
t.Fatalf("gateway response pipeline diagnostics missing: %#v", report.PipelineLatency)
}
if got := report.PipelineLatency.Items[0].Stage; got != "gateway_protocol_response_e2e" {
t.Fatalf("pipeline stage = %q", got)
}
}
func TestEvaluateServiceSummaryAggregatesP99GaugesByMax(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_e2e_recent_p99_ms{topic="vehicle.fields.go.gb32960.v1"} 49
vehicle_bridge_kafka_e2e_recent_p99_ms{topic="vehicle.raw.go.jt808.v1"} 106
vehicle_bridge_kafka_e2e_recent_samples{topic="vehicle.fields.go.gb32960.v1"} 512
vehicle_bridge_kafka_e2e_recent_samples{topic="vehicle.raw.go.jt808.v1"} 512`,
}, Thresholds{
BridgeKafkaRecentE2EP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if len(report.Services) != 1 {
t.Fatalf("services = %d, want 1", len(report.Services))
}
if got, want := report.Services[0].Metrics["vehicle_bridge_kafka_e2e_recent_p99_ms"], 106.0; got != want {
t.Fatalf("summary p99 = %v, want max %v instead of label sum", got, want)
}
}
func TestEvaluateServiceSummaryDoesNotCreateP99AgeMetric(t *testing.T) {
parsed := parseMetricSet(`vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_last_kafka_e2e_unix_seconds{topic="vehicle.raw.go.jt808.v1"} 1000
vehicle_bridge_kafka_e2e_recent_p99_ms{topic="vehicle.raw.go.jt808.v1"} 106
vehicle_bridge_kafka_e2e_recent_samples{topic="vehicle.raw.go.jt808.v1"} 512`)
addLastActivityAges(parsed.totals, time.Unix(1010, 0).UTC())
metrics := parsed.totals
if got, want := metrics["vehicle_bridge_last_kafka_e2e_age_seconds"], 10.0; got != want {
t.Fatalf("last activity age = %v, want %v", got, want)
}
if _, ok := metrics["vehicle_bridge_kafka_e2e_recent_p99_ms_age_seconds"]; ok {
t.Fatalf("p99 gauge should not produce an age metric: %#v", metrics)
}
}
func TestEvaluateIncludesPipelineLatencyDiagnostics(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_e2e_recent_p99_ms{topic="vehicle.raw.go.gb32960.v1"} 80
vehicle_bridge_kafka_e2e_recent_samples{topic="vehicle.raw.go.gb32960.v1"} 128`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_tdengine_enabled 0
vehicle_fast_writer_redis_e2e_recent_p99_ms{subject="vehicle.raw.go.gb32960.v1"} 42
vehicle_fast_writer_redis_e2e_recent_samples{subject="vehicle.raw.go.gb32960.v1"} 128`,
"history": `vehicle_service_info{service="history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="history-writer",group="go-history-writer",topic="vehicle.raw.go.gb32960.v1"} 1
vehicle_history_write_e2e_recent_p99_ms{topic="vehicle.raw.go.gb32960.v1"} 1500
vehicle_history_write_e2e_recent_samples{topic="vehicle.raw.go.gb32960.v1"} 128`,
"stat": `vehicle_service_info{service="stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="stat-writer",group="go-stat-writer",topic="vehicle.fields.go.gb32960.v1"} 1
vehicle_stat_write_e2e_recent_p99_ms{topic="vehicle.fields.go.gb32960.v1"} 900
vehicle_stat_write_e2e_recent_samples{topic="vehicle.fields.go.gb32960.v1"} 8`,
}, Thresholds{
BridgeKafkaRecentE2EP99MS: 300,
FastWriterRedisRecentE2EP99MS: 100,
HistoryWriteRecentE2EP99MS: 1000,
StatWriteRecentE2EP99MS: 1000,
MinHistogramSamples: 100,
})
if report.PipelineLatency == nil {
t.Fatal("pipeline latency diagnostics missing")
}
if got, want := len(report.PipelineLatency.Items), 4; got != want {
t.Fatalf("pipeline latency items = %d, want %d", got, want)
}
if got, want := report.PipelineLatency.TotalSamples, int64(392); got != want {
t.Fatalf("total samples = %d, want %d", got, want)
}
if got, want := report.PipelineLatency.MaxP99MS, 1500.0; got != want {
t.Fatalf("max p99 = %v, want %v", got, want)
}
statusByStage := map[string]string{}
for _, item := range report.PipelineLatency.Items {
statusByStage[item.Stage] = item.Status
}
if statusByStage["bridge_kafka_e2e"] != "ok" {
t.Fatalf("bridge status = %q", statusByStage["bridge_kafka_e2e"])
}
if statusByStage["fast_writer_redis_e2e"] != "ok" {
t.Fatalf("fast writer status = %q", statusByStage["fast_writer_redis_e2e"])
}
if statusByStage["history_tdengine_write_e2e"] != "degraded" {
t.Fatalf("history status = %q", statusByStage["history_tdengine_write_e2e"])
}
if statusByStage["stat_mysql_write_e2e"] != "insufficient_samples" {
t.Fatalf("stat status = %q", statusByStage["stat_mysql_write_e2e"])
}
}
func TestEvaluateReportsSlowBridgeKafkaWriteP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_write_duration_ms_histogram_bucket{le="100",status="ok",topic="vehicle.raw.go.jt808.v1"} 90
vehicle_bridge_kafka_write_duration_ms_histogram_bucket{le="1000",status="ok",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_bridge_kafka_write_duration_ms_histogram_bucket{le="+Inf",status="ok",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_bridge_kafka_write_duration_ms_histogram_count{status="ok",topic="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
BridgeKafkaWriteP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge kafka write p99 1000ms exceeds 300ms",
"topic=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsSlowBridgeKafkaE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_e2e_duration_ms_histogram_bucket{le="100",topic="vehicle.raw.go.jt808.v1"} 90
vehicle_bridge_kafka_e2e_duration_ms_histogram_bucket{le="1000",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_bridge_kafka_e2e_duration_ms_histogram_bucket{le="+Inf",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_bridge_kafka_e2e_duration_ms_histogram_count{topic="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
BridgeKafkaE2EP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge kafka e2e p99 1000ms exceeds 300ms",
"topic=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsSlowBridgeKafkaRecentE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_e2e_recent_p99_ms{topic="vehicle.raw.go.jt808.v1"} 450
vehicle_bridge_kafka_e2e_recent_samples{topic="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
BridgeKafkaRecentE2EP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge kafka e2e recent p99 450ms exceeds 300ms",
"topic=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsSlowHistoryWriteE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_history_write_e2e_duration_ms_histogram_bucket{le="100",topic="vehicle.raw.go.jt808.v1"} 90
vehicle_history_write_e2e_duration_ms_histogram_bucket{le="1000",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_write_e2e_duration_ms_histogram_bucket{le="+Inf",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_write_e2e_duration_ms_histogram_count{topic="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
HistoryWriteE2EP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history write e2e p99 1000ms exceeds 300ms",
"topic=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateReportsSlowStatWriteE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_stat_write_e2e_duration_ms_histogram_bucket{le="100",topic="vehicle.fields.go.jt808.v1"} 90
vehicle_stat_write_e2e_duration_ms_histogram_bucket{le="1000",topic="vehicle.fields.go.jt808.v1"} 100
vehicle_stat_write_e2e_duration_ms_histogram_bucket{le="+Inf",topic="vehicle.fields.go.jt808.v1"} 100
vehicle_stat_write_e2e_duration_ms_histogram_count{topic="vehicle.fields.go.jt808.v1"} 100`,
}, Thresholds{
StatWriteE2EP99MS: 300,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat write e2e p99 1000ms exceeds 300ms",
"topic=vehicle.fields.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestDefaultThresholdsDoNotGateCumulativeE2EHistograms(t *testing.T) {
report := Evaluate(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_kafka_e2e_duration_ms_histogram_bucket{le="100",topic="vehicle.raw.go.jt808.v1"} 90
vehicle_bridge_kafka_e2e_duration_ms_histogram_bucket{le="10000",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_bridge_kafka_e2e_duration_ms_histogram_bucket{le="+Inf",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_bridge_kafka_e2e_duration_ms_histogram_count{topic="vehicle.raw.go.jt808.v1"} 100`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_tdengine_enabled 0
vehicle_fast_writer_redis_e2e_duration_ms_histogram_bucket{le="100",subject="vehicle.raw.go.jt808.v1"} 90
vehicle_fast_writer_redis_e2e_duration_ms_histogram_bucket{le="10000",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_redis_e2e_duration_ms_histogram_bucket{le="+Inf",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_redis_e2e_duration_ms_histogram_count{subject="vehicle.raw.go.jt808.v1"} 100`,
"history": `vehicle_service_info{service="history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_write_e2e_duration_ms_histogram_bucket{le="100",topic="vehicle.raw.go.jt808.v1"} 90
vehicle_history_write_e2e_duration_ms_histogram_bucket{le="10000",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_write_e2e_duration_ms_histogram_bucket{le="+Inf",topic="vehicle.raw.go.jt808.v1"} 100
vehicle_history_write_e2e_duration_ms_histogram_count{topic="vehicle.raw.go.jt808.v1"} 100`,
"stat": `vehicle_service_info{service="stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_write_e2e_duration_ms_histogram_bucket{le="100",topic="vehicle.fields.go.jt808.v1"} 90
vehicle_stat_write_e2e_duration_ms_histogram_bucket{le="10000",topic="vehicle.fields.go.jt808.v1"} 100
vehicle_stat_write_e2e_duration_ms_histogram_bucket{le="+Inf",topic="vehicle.fields.go.jt808.v1"} 100
vehicle_stat_write_e2e_duration_ms_histogram_count{topic="vehicle.fields.go.jt808.v1"} 100`,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
if joined := strings.Join(report.Findings, "\n"); strings.Contains(joined, "e2e p99") {
t.Fatalf("cumulative e2e p99 should be observe-only by default:\n%s", joined)
}
}
func TestEvaluateReportsSlowHistoryWriteRecentE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_write_e2e_recent_p99_ms{topic="vehicle.raw.go.jt808.v1"} 2500
vehicle_history_write_e2e_recent_samples{topic="vehicle.raw.go.jt808.v1"} 100`,
}, Thresholds{
HistoryWriteRecentE2EP99MS: 1000,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"history write e2e recent p99 2500ms exceeds 1000ms",
"topic=vehicle.raw.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestEvaluateSkipsHistoryWriteRecentE2EWhenSamplesAreLow(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"history": `vehicle_service_info{service="history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_write_e2e_recent_p99_ms{topic="vehicle.raw.go.jt808.v1"} 2500
vehicle_history_write_e2e_recent_samples{topic="vehicle.raw.go.jt808.v1"} 99`,
}, Thresholds{
HistoryWriteRecentE2EP99MS: 1000,
MinHistogramSamples: 100,
})
if report.Status != StatusOK {
t.Fatalf("status = %s, want ok: %#v", report.Status, report)
}
}
func TestEvaluateReportsSlowStatWriteRecentE2EP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"stat": `vehicle_service_info{service="stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_write_e2e_recent_p99_ms{topic="vehicle.fields.go.jt808.v1"} 1500
vehicle_stat_write_e2e_recent_samples{topic="vehicle.fields.go.jt808.v1"} 100`,
}, Thresholds{
StatWriteRecentE2EP99MS: 1000,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"stat write e2e recent p99 1500ms exceeds 1000ms",
"topic=vehicle.fields.go.jt808.v1",
"samples 100",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}
func TestAnnotateDailyMileageDiagnosticsSplitsPipelineAndSourceDataIssues(t *testing.T) {
items, pipelineIssues, sourceDataIssues := AnnotateDailyMileageDiagnostics([]DailyMileageDiagnosisReasonItem{
{Diagnosis: "OK", Reason: "daily_metric_exists", Count: 234},
{Diagnosis: "NO_SOURCE_SAMPLE", Reason: "realtime_location_has_total_mileage_but_no_stat_sample", Count: 7},
{Diagnosis: "MISSING_DAILY", Reason: "source_samples_exist_but_final_metric_missing", Count: 1},
{Diagnosis: "NO_TOTAL_MILEAGE", Reason: "realtime_total_mileage_time_missing", Count: 2},
{Diagnosis: "NO_TOTAL_MILEAGE", Reason: "realtime_total_mileage_non_positive", Count: 3},
{Diagnosis: "NO_TOTAL_MILEAGE", Reason: "realtime_total_mileage_missing", Count: 4},
})
if pipelineIssues != 10 || sourceDataIssues != 7 {
t.Fatalf("pipeline=%d source=%d", pipelineIssues, sourceDataIssues)
}
severities := make([]string, 0, len(items))
for _, item := range items {
severities = append(severities, item.Severity)
}
got := strings.Join(severities, ",")
want := "ok,pipeline,pipeline,pipeline,source_data,source_data"
if got != want {
t.Fatalf("severities = %q, want %q", got, want)
}
if !strings.Contains(items[4].RecommendedOperatorAction, "总里程小于等于 0") {
t.Fatalf("recommended action = %q", items[4].RecommendedOperatorAction)
}
if !strings.Contains(items[5].RecommendedOperatorAction, "缺少总里程字段") {
t.Fatalf("recommended action = %q", items[5].RecommendedOperatorAction)
}
}
func TestAnnotateDailyMileageDiagnosticsPreservesAPISeverityAndClassifiesInvalidSamples(t *testing.T) {
items, pipelineIssues, sourceDataIssues := AnnotateDailyMileageDiagnostics([]DailyMileageDiagnosisReasonItem{
{Diagnosis: "MISSING_DAILY", Reason: "source_samples_all_invalid", Severity: DailyMileageSeveritySourceData, Count: 25},
{Diagnosis: "MISSING_DAILY", Reason: "source_samples_all_invalid", Count: 17},
{Diagnosis: "NO_SOURCE_SAMPLE", Reason: "realtime_location_has_total_mileage_but_no_stat_sample", Severity: DailyMileageSeverityPipeline, Count: 2},
})
if pipelineIssues != 2 || sourceDataIssues != 42 {
t.Fatalf("pipeline=%d source=%d", pipelineIssues, sourceDataIssues)
}
if items[0].Severity != DailyMileageSeveritySourceData || items[1].Severity != DailyMileageSeveritySourceData {
t.Fatalf("severities = %#v", []string{items[0].Severity, items[1].Severity})
}
}
func TestEvaluateReportsDegradedForSlowDownstreamP99(t *testing.T) {
report := EvaluateWithThresholds(map[string]string{
"bridge": `vehicle_service_info{service="nats-kafka-bridge"} 1
vehicle_bridge_config{setting="fetch_wait_ms"} 20
vehicle_bridge_config{setting="workers"} 4
vehicle_bridge_batch_duration_ms_histogram_bucket{le="1000",status="ok"} 98
vehicle_bridge_batch_duration_ms_histogram_bucket{le="5000",status="ok"} 100
vehicle_bridge_batch_duration_ms_histogram_bucket{le="+Inf",status="ok"} 100
vehicle_bridge_batch_duration_ms_histogram_count{status="ok"} 100`,
"fast-writer": `vehicle_service_info{service="nats-fast-writer"} 1
vehicle_fast_writer_config{setting="fetch_wait_ms"} 20
vehicle_fast_writer_config{setting="workers"} 8
vehicle_fast_writer_stage_duration_ms_histogram_bucket{le="25",stage="redis",status="ok",subject="vehicle.raw.go.jt808.v1"} 90
vehicle_fast_writer_stage_duration_ms_histogram_bucket{le="250",stage="redis",status="ok",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_stage_duration_ms_histogram_bucket{le="+Inf",stage="redis",status="ok",subject="vehicle.raw.go.jt808.v1"} 100
vehicle_fast_writer_stage_duration_ms_histogram_count{stage="redis",status="ok",subject="vehicle.raw.go.jt808.v1"} 100`,
"history": `vehicle_service_info{service="vehicle-history-writer"} 1
vehicle_history_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-history-writer",group="go-history-writer",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_history_batch_flush_duration_ms_histogram_bucket{le="100",status="ok"} 95
vehicle_history_batch_flush_duration_ms_histogram_bucket{le="1000",status="ok"} 100
vehicle_history_batch_flush_duration_ms_histogram_bucket{le="+Inf",status="ok"} 100
vehicle_history_batch_flush_duration_ms_histogram_count{status="ok"} 100`,
"realtime": `vehicle_service_info{service="vehicle-realtime-api"} 1
vehicle_realtime_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-realtime-api",group="go-realtime-api",topic="vehicle.raw.go.jt808.v1"} 1
vehicle_realtime_batch_flush_duration_ms_histogram_bucket{le="50",status="ok"} 50
vehicle_realtime_batch_flush_duration_ms_histogram_bucket{le="250",status="ok"} 100
vehicle_realtime_batch_flush_duration_ms_histogram_bucket{le="+Inf",status="ok"} 100
vehicle_realtime_batch_flush_duration_ms_histogram_count{status="ok"} 100
vehicle_realtime_store_update_duration_ms_histogram_bucket{le="10",protocol="JT808",status="ok",store="mysql"} 50
vehicle_realtime_store_update_duration_ms_histogram_bucket{le="250",protocol="JT808",status="ok",store="mysql"} 100
vehicle_realtime_store_update_duration_ms_histogram_bucket{le="+Inf",protocol="JT808",status="ok",store="mysql"} 100
vehicle_realtime_store_update_duration_ms_histogram_count{protocol="JT808",status="ok",store="mysql"} 100`,
"stat": `vehicle_service_info{service="vehicle-stat-writer"} 1
vehicle_stat_config{setting="workers"} 3
vehicle_kafka_consumer_info{service="vehicle-stat-writer",group="go-stat-writer",topic="vehicle.fields.go.jt808.v1"} 1
vehicle_stat_write_duration_ms_histogram_bucket{le="5",status="ok",topic="vehicle.fields.go.jt808.v1"} 98
vehicle_stat_write_duration_ms_histogram_bucket{le="250",status="ok",topic="vehicle.fields.go.jt808.v1"} 100
vehicle_stat_write_duration_ms_histogram_bucket{le="+Inf",status="ok",topic="vehicle.fields.go.jt808.v1"} 100
vehicle_stat_write_duration_ms_histogram_count{status="ok",topic="vehicle.fields.go.jt808.v1"} 100`,
}, Thresholds{
BridgeBatchP99MS: 1000,
FastWriterStageP99MS: 100,
HistoryFlushP99MS: 500,
RealtimeBatchP99MS: 100,
RealtimeStoreP99MS: 100,
StatWriteP99MS: 100,
MinHistogramSamples: 100,
})
if report.Status != StatusDegraded {
t.Fatalf("status = %s, want degraded: %#v", report.Status, report)
}
joined := strings.Join(report.Findings, "\n")
for _, want := range []string{
"bridge batch p99 5000ms exceeds 1000ms",
"fast writer stage p99 250ms exceeds 100ms",
"history flush p99 1000ms exceeds 500ms",
"realtime batch flush p99 250ms exceeds 100ms",
"realtime store update p99 250ms exceeds 100ms",
"stat write p99 250ms exceeds 100ms",
} {
if !strings.Contains(joined, want) {
t.Fatalf("finding missing %q in:\n%s", want, joined)
}
}
}