mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
- Add MasterID field with index to LogRecord model for efficient queries - Fix threshold config loading to use fixed ID=1 with FirstOrCreate - Allow traffic spike detection to work without Redis for log-based checks - Add traffic_spike to API documentation for alert type filter - Add comprehensive tests for RPM/RPD/TPM spike detection scenarios
636 lines
17 KiB
Go
636 lines
17 KiB
Go
package cron
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupTestDB(t *testing.T) *gorm.DB {
|
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Alert{}, &model.AlertThresholdConfig{}, &model.Master{}, &model.Key{}, &model.APIKey{}, &model.ProviderGroup{}, &model.LogRecord{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestDefaultAlertThresholdConfig(t *testing.T) {
|
|
cfg := model.DefaultAlertThresholdConfig()
|
|
|
|
if cfg.GlobalQPS != 100 {
|
|
t.Errorf("expected GlobalQPS=100, got %d", cfg.GlobalQPS)
|
|
}
|
|
if cfg.MasterRPM != 20 {
|
|
t.Errorf("expected MasterRPM=20, got %d", cfg.MasterRPM)
|
|
}
|
|
if cfg.MasterRPD != 1000 {
|
|
t.Errorf("expected MasterRPD=1000, got %d", cfg.MasterRPD)
|
|
}
|
|
if cfg.MasterTPM != 10_000_000 {
|
|
t.Errorf("expected MasterTPM=10000000, got %d", cfg.MasterTPM)
|
|
}
|
|
if cfg.MasterTPD != 100_000_000 {
|
|
t.Errorf("expected MasterTPD=100000000, got %d", cfg.MasterTPD)
|
|
}
|
|
if cfg.MinRPMRequests1m != 10 {
|
|
t.Errorf("expected MinRPMRequests1m=10, got %d", cfg.MinRPMRequests1m)
|
|
}
|
|
if cfg.MinTPMTokens1m != 1_000_000 {
|
|
t.Errorf("expected MinTPMTokens1m=1000000, got %d", cfg.MinTPMTokens1m)
|
|
}
|
|
}
|
|
|
|
func TestAlertDetectorLoadThresholdConfigDefault(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
detector := &AlertDetector{db: db}
|
|
cfg := detector.loadThresholdConfig()
|
|
|
|
// Should return defaults when no config in DB
|
|
if cfg.GlobalQPS != 100 {
|
|
t.Errorf("expected default GlobalQPS=100, got %d", cfg.GlobalQPS)
|
|
}
|
|
if cfg.MasterRPM != 20 {
|
|
t.Errorf("expected default MasterRPM=20, got %d", cfg.MasterRPM)
|
|
}
|
|
}
|
|
|
|
func TestAlertDetectorLoadThresholdConfigFromDB(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Insert custom config
|
|
customCfg := model.AlertThresholdConfig{
|
|
GlobalQPS: 500,
|
|
MasterRPM: 100,
|
|
MasterRPD: 5000,
|
|
MasterTPM: 50_000_000,
|
|
MasterTPD: 500_000_000,
|
|
MinRPMRequests1m: 50,
|
|
MinTPMTokens1m: 5_000_000,
|
|
}
|
|
if err := db.Create(&customCfg).Error; err != nil {
|
|
t.Fatalf("create config: %v", err)
|
|
}
|
|
|
|
detector := &AlertDetector{db: db}
|
|
cfg := detector.loadThresholdConfig()
|
|
|
|
if cfg.GlobalQPS != 500 {
|
|
t.Errorf("expected GlobalQPS=500, got %d", cfg.GlobalQPS)
|
|
}
|
|
if cfg.MasterRPM != 100 {
|
|
t.Errorf("expected MasterRPM=100, got %d", cfg.MasterRPM)
|
|
}
|
|
if cfg.MasterRPD != 5000 {
|
|
t.Errorf("expected MasterRPD=5000, got %d", cfg.MasterRPD)
|
|
}
|
|
}
|
|
|
|
func TestTrafficSpikeSeverity(t *testing.T) {
|
|
tests := []struct {
|
|
value int64
|
|
threshold int64
|
|
expected model.AlertSeverity
|
|
}{
|
|
{50, 100, model.AlertSeverityWarning}, // below threshold, but this func is only called when >= threshold
|
|
{100, 100, model.AlertSeverityWarning}, // exactly at threshold
|
|
{150, 100, model.AlertSeverityWarning}, // 1.5x threshold
|
|
{199, 100, model.AlertSeverityWarning}, // just below 2x
|
|
{200, 100, model.AlertSeverityCritical}, // exactly 2x threshold
|
|
{300, 100, model.AlertSeverityCritical}, // 3x threshold
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := trafficSpikeSeverity(tc.value, tc.threshold)
|
|
if result != tc.expected {
|
|
t.Errorf("trafficSpikeSeverity(%d, %d) = %s, expected %s", tc.value, tc.threshold, result, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrafficSpikeMetadataJSON(t *testing.T) {
|
|
meta := trafficSpikeMetadata{
|
|
Metric: "master_rpm",
|
|
Value: 150,
|
|
Threshold: 20,
|
|
Window: "1m",
|
|
}
|
|
|
|
json := meta.JSON()
|
|
if json == "" {
|
|
t.Error("expected non-empty JSON")
|
|
}
|
|
if len(json) < 10 {
|
|
t.Errorf("JSON too short: %s", json)
|
|
}
|
|
}
|
|
|
|
func TestAlertDetectorDeduplication(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
config.DeduplicationCooldown = 5 * time.Minute
|
|
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
// Create first alert
|
|
detector.createAlertIfNew(
|
|
model.AlertTypeRateLimit,
|
|
model.AlertSeverityWarning,
|
|
"Test Alert",
|
|
"Test Message",
|
|
1,
|
|
"master",
|
|
"test-master",
|
|
)
|
|
|
|
var count int64
|
|
db.Model(&model.Alert{}).Count(&count)
|
|
if count != 1 {
|
|
t.Fatalf("expected 1 alert, got %d", count)
|
|
}
|
|
|
|
// Try to create duplicate (should be skipped)
|
|
detector.createAlertIfNew(
|
|
model.AlertTypeRateLimit,
|
|
model.AlertSeverityWarning,
|
|
"Test Alert Duplicate",
|
|
"Test Message Duplicate",
|
|
1,
|
|
"master",
|
|
"test-master",
|
|
)
|
|
|
|
db.Model(&model.Alert{}).Count(&count)
|
|
if count != 1 {
|
|
t.Fatalf("expected still 1 alert after duplicate, got %d", count)
|
|
}
|
|
|
|
// Different fingerprint should create new alert
|
|
detector.createAlertIfNew(
|
|
model.AlertTypeRateLimit,
|
|
model.AlertSeverityWarning,
|
|
"Different Alert",
|
|
"Different Message",
|
|
2, // Different related_id
|
|
"master",
|
|
"test-master-2",
|
|
)
|
|
|
|
db.Model(&model.Alert{}).Count(&count)
|
|
if count != 2 {
|
|
t.Fatalf("expected 2 alerts with different fingerprint, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestAlertDetectorTrafficSpikeDeduplication(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
config.DeduplicationCooldown = 5 * time.Minute
|
|
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
meta := trafficSpikeMetadata{
|
|
Metric: "master_rpm",
|
|
Value: 150,
|
|
Threshold: 20,
|
|
Window: "1m",
|
|
}
|
|
|
|
// Create first traffic spike alert
|
|
detector.createTrafficSpikeAlert(
|
|
model.AlertSeverityWarning,
|
|
"RPM Exceeded",
|
|
"Master exceeded RPM",
|
|
1,
|
|
"master",
|
|
"test-master",
|
|
meta,
|
|
)
|
|
|
|
var count int64
|
|
db.Model(&model.Alert{}).Count(&count)
|
|
if count != 1 {
|
|
t.Fatalf("expected 1 alert, got %d", count)
|
|
}
|
|
|
|
// Try to create duplicate (same metric, same master)
|
|
detector.createTrafficSpikeAlert(
|
|
model.AlertSeverityWarning,
|
|
"RPM Exceeded Again",
|
|
"Master exceeded RPM again",
|
|
1,
|
|
"master",
|
|
"test-master",
|
|
meta,
|
|
)
|
|
|
|
db.Model(&model.Alert{}).Count(&count)
|
|
if count != 1 {
|
|
t.Fatalf("expected still 1 alert after duplicate, got %d", count)
|
|
}
|
|
|
|
// Different metric should create new alert
|
|
meta2 := trafficSpikeMetadata{
|
|
Metric: "master_tpm", // Different metric
|
|
Value: 15000000,
|
|
Threshold: 10000000,
|
|
Window: "1m",
|
|
}
|
|
detector.createTrafficSpikeAlert(
|
|
model.AlertSeverityWarning,
|
|
"TPM Exceeded",
|
|
"Master exceeded TPM",
|
|
1,
|
|
"master",
|
|
"test-master",
|
|
meta2,
|
|
)
|
|
|
|
db.Model(&model.Alert{}).Count(&count)
|
|
if count != 2 {
|
|
t.Fatalf("expected 2 alerts with different metric, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestAlertDetectorErrorRateSeverity(t *testing.T) {
|
|
detector := &AlertDetector{}
|
|
|
|
tests := []struct {
|
|
rate float64
|
|
expected model.AlertSeverity
|
|
}{
|
|
{0.05, model.AlertSeverityInfo}, // 5%
|
|
{0.10, model.AlertSeverityInfo}, // 10%
|
|
{0.24, model.AlertSeverityInfo}, // 24%
|
|
{0.25, model.AlertSeverityWarning}, // 25%
|
|
{0.40, model.AlertSeverityWarning}, // 40%
|
|
{0.49, model.AlertSeverityWarning}, // 49%
|
|
{0.50, model.AlertSeverityCritical}, // 50%
|
|
{0.75, model.AlertSeverityCritical}, // 75%
|
|
{1.00, model.AlertSeverityCritical}, // 100%
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := detector.errorRateSeverity(tc.rate)
|
|
if result != tc.expected {
|
|
t.Errorf("errorRateSeverity(%.2f) = %s, expected %s", tc.rate, result, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAlertDetectorDetectOnceNilSafe(t *testing.T) {
|
|
// Test nil detector
|
|
var nilDetector *AlertDetector
|
|
nilDetector.detectOnce(context.Background())
|
|
|
|
// Test detector with nil db
|
|
detector := &AlertDetector{}
|
|
detector.detectOnce(context.Background())
|
|
|
|
// Should not panic
|
|
}
|
|
|
|
func TestAlertDetectorStartDisabled(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
config.Enabled = false
|
|
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Should return immediately without blocking
|
|
done := make(chan struct{})
|
|
go func() {
|
|
detector.Start(ctx)
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Expected: Start returned immediately because Enabled=false
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("Start did not return immediately when disabled")
|
|
}
|
|
}
|
|
|
|
func TestDetectMasterMinuteSpikesRPM(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Create a master
|
|
master := model.Master{Name: "test-master", Status: "active"}
|
|
if err := db.Create(&master).Error; err != nil {
|
|
t.Fatalf("create master: %v", err)
|
|
}
|
|
|
|
// Create threshold config with low thresholds for testing
|
|
cfg := model.AlertThresholdConfig{
|
|
GlobalQPS: 100,
|
|
MasterRPM: 5, // Low threshold for testing
|
|
MasterRPD: 1000,
|
|
MasterTPM: 10_000_000,
|
|
MasterTPD: 100_000_000,
|
|
MinRPMRequests1m: 3, // Low minimum sample
|
|
MinTPMTokens1m: 1000,
|
|
}
|
|
if err := db.Create(&cfg).Error; err != nil {
|
|
t.Fatalf("create config: %v", err)
|
|
}
|
|
|
|
// Create log records within the last minute
|
|
now := time.Now().UTC()
|
|
for i := 0; i < 10; i++ {
|
|
log := model.LogRecord{
|
|
MasterID: master.ID,
|
|
KeyID: 1,
|
|
TokensIn: 100,
|
|
TokensOut: 200,
|
|
}
|
|
if err := db.Create(&log).Error; err != nil {
|
|
t.Fatalf("create log: %v", err)
|
|
}
|
|
// Update created_at to be within the last minute
|
|
db.Model(&log).Update("created_at", now.Add(-time.Duration(i)*time.Second))
|
|
}
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
// Run detection
|
|
detector.detectMasterMinuteSpikes(context.Background(), cfg)
|
|
|
|
// Should have created an RPM alert
|
|
var alerts []model.Alert
|
|
if err := db.Where("type = ?", model.AlertTypeTrafficSpike).Find(&alerts).Error; err != nil {
|
|
t.Fatalf("query alerts: %v", err)
|
|
}
|
|
|
|
if len(alerts) < 1 {
|
|
t.Error("expected at least 1 traffic spike alert for RPM")
|
|
}
|
|
|
|
// Check alert properties
|
|
var foundRPM bool
|
|
for _, a := range alerts {
|
|
if a.RelatedType == "master" && a.RelatedID == master.ID {
|
|
foundRPM = true
|
|
if a.Severity != model.AlertSeverityWarning && a.Severity != model.AlertSeverityCritical {
|
|
t.Errorf("expected warning or critical severity, got %s", a.Severity)
|
|
}
|
|
}
|
|
}
|
|
if !foundRPM {
|
|
t.Error("expected RPM alert for test master")
|
|
}
|
|
}
|
|
|
|
func TestDetectMasterDaySpikesRPD(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Create a master
|
|
master := model.Master{Name: "test-master-day", Status: "active"}
|
|
if err := db.Create(&master).Error; err != nil {
|
|
t.Fatalf("create master: %v", err)
|
|
}
|
|
|
|
// Create threshold config with low thresholds for testing
|
|
cfg := model.AlertThresholdConfig{
|
|
GlobalQPS: 100,
|
|
MasterRPM: 1000,
|
|
MasterRPD: 5, // Low threshold for testing
|
|
MasterTPM: 10_000_000,
|
|
MasterTPD: 100_000_000,
|
|
MinRPMRequests1m: 10,
|
|
MinTPMTokens1m: 1000,
|
|
}
|
|
if err := db.Create(&cfg).Error; err != nil {
|
|
t.Fatalf("create config: %v", err)
|
|
}
|
|
|
|
// Create log records within the last 24 hours
|
|
now := time.Now().UTC()
|
|
for i := 0; i < 10; i++ {
|
|
log := model.LogRecord{
|
|
MasterID: master.ID,
|
|
KeyID: 1,
|
|
TokensIn: 100,
|
|
TokensOut: 200,
|
|
}
|
|
if err := db.Create(&log).Error; err != nil {
|
|
t.Fatalf("create log: %v", err)
|
|
}
|
|
// Update created_at to be within the last 24 hours
|
|
db.Model(&log).Update("created_at", now.Add(-time.Duration(i)*time.Hour))
|
|
}
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
// Run detection
|
|
detector.detectMasterDaySpikes(context.Background(), cfg)
|
|
|
|
// Should have created an RPD alert
|
|
var alerts []model.Alert
|
|
if err := db.Where("type = ?", model.AlertTypeTrafficSpike).Find(&alerts).Error; err != nil {
|
|
t.Fatalf("query alerts: %v", err)
|
|
}
|
|
|
|
if len(alerts) < 1 {
|
|
t.Error("expected at least 1 traffic spike alert for RPD")
|
|
}
|
|
|
|
// Check alert properties
|
|
var foundRPD bool
|
|
for _, a := range alerts {
|
|
if a.RelatedType == "master" && a.RelatedID == master.ID {
|
|
foundRPD = true
|
|
if a.Severity != model.AlertSeverityWarning && a.Severity != model.AlertSeverityCritical {
|
|
t.Errorf("expected warning or critical severity, got %s", a.Severity)
|
|
}
|
|
}
|
|
}
|
|
if !foundRPD {
|
|
t.Error("expected RPD alert for test master")
|
|
}
|
|
}
|
|
|
|
func TestDetectMasterMinuteSpikesTPM(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Create a master
|
|
master := model.Master{Name: "test-master-tpm", Status: "active"}
|
|
if err := db.Create(&master).Error; err != nil {
|
|
t.Fatalf("create master: %v", err)
|
|
}
|
|
|
|
// Create threshold config with low token thresholds for testing
|
|
cfg := model.AlertThresholdConfig{
|
|
GlobalQPS: 100,
|
|
MasterRPM: 1000, // High so RPM won't trigger
|
|
MasterRPD: 10000,
|
|
MasterTPM: 1000, // Low token threshold for testing
|
|
MasterTPD: 100_000_000,
|
|
MinRPMRequests1m: 100, // High so RPM minimum won't be met
|
|
MinTPMTokens1m: 500, // Low minimum sample
|
|
}
|
|
if err := db.Create(&cfg).Error; err != nil {
|
|
t.Fatalf("create config: %v", err)
|
|
}
|
|
|
|
// Create log records with high token counts within the last minute
|
|
now := time.Now().UTC()
|
|
for i := 0; i < 5; i++ {
|
|
log := model.LogRecord{
|
|
MasterID: master.ID,
|
|
KeyID: 1,
|
|
TokensIn: 500,
|
|
TokensOut: 500, // 1000 total per record
|
|
}
|
|
if err := db.Create(&log).Error; err != nil {
|
|
t.Fatalf("create log: %v", err)
|
|
}
|
|
// Update created_at to be within the last minute
|
|
db.Model(&log).Update("created_at", now.Add(-time.Duration(i)*time.Second))
|
|
}
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
// Run detection
|
|
detector.detectMasterMinuteSpikes(context.Background(), cfg)
|
|
|
|
// Should have created a TPM alert (5 * 1000 = 5000 tokens >= 1000 threshold, and >= 500 min)
|
|
var alerts []model.Alert
|
|
if err := db.Where("type = ? AND related_id = ?", model.AlertTypeTrafficSpike, master.ID).Find(&alerts).Error; err != nil {
|
|
t.Fatalf("query alerts: %v", err)
|
|
}
|
|
|
|
if len(alerts) < 1 {
|
|
t.Error("expected at least 1 traffic spike alert for TPM")
|
|
}
|
|
}
|
|
|
|
func TestDetectTrafficSpikesWithoutRedis(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Create a master
|
|
master := model.Master{Name: "test-master-no-redis", Status: "active"}
|
|
if err := db.Create(&master).Error; err != nil {
|
|
t.Fatalf("create master: %v", err)
|
|
}
|
|
|
|
// Create threshold config
|
|
cfg := model.AlertThresholdConfig{
|
|
GlobalQPS: 100,
|
|
MasterRPM: 5, // Low threshold
|
|
MasterRPD: 5, // Low threshold
|
|
MasterTPM: 10_000_000,
|
|
MasterTPD: 100_000_000,
|
|
MinRPMRequests1m: 3,
|
|
MinTPMTokens1m: 1000,
|
|
}
|
|
if err := db.Create(&cfg).Error; err != nil {
|
|
t.Fatalf("create config: %v", err)
|
|
}
|
|
|
|
// Create log records
|
|
now := time.Now().UTC()
|
|
for i := 0; i < 10; i++ {
|
|
log := model.LogRecord{
|
|
MasterID: master.ID,
|
|
KeyID: 1,
|
|
TokensIn: 100,
|
|
TokensOut: 200,
|
|
}
|
|
if err := db.Create(&log).Error; err != nil {
|
|
t.Fatalf("create log: %v", err)
|
|
}
|
|
db.Model(&log).Update("created_at", now.Add(-time.Duration(i)*time.Second))
|
|
}
|
|
|
|
// Create detector WITHOUT Redis (rdb = nil)
|
|
config := DefaultAlertDetectorConfig()
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
// Run full detectTrafficSpikes - should not panic and should detect log-based spikes
|
|
detector.detectTrafficSpikes(context.Background())
|
|
|
|
// Should have created alerts for minute spikes (log-based detection works without Redis)
|
|
var alerts []model.Alert
|
|
if err := db.Where("type = ?", model.AlertTypeTrafficSpike).Find(&alerts).Error; err != nil {
|
|
t.Fatalf("query alerts: %v", err)
|
|
}
|
|
|
|
if len(alerts) < 1 {
|
|
t.Error("expected traffic spike alerts from log-based detection (without Redis)")
|
|
}
|
|
}
|
|
|
|
func TestDetectMasterMinuteSpikesNoAlertBelowThreshold(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Create a master
|
|
master := model.Master{Name: "test-master-low", Status: "active"}
|
|
if err := db.Create(&master).Error; err != nil {
|
|
t.Fatalf("create master: %v", err)
|
|
}
|
|
|
|
// Create threshold config with high thresholds
|
|
cfg := model.AlertThresholdConfig{
|
|
GlobalQPS: 100,
|
|
MasterRPM: 1000, // High threshold
|
|
MasterRPD: 10000,
|
|
MasterTPM: 10_000_000, // High threshold
|
|
MasterTPD: 100_000_000,
|
|
MinRPMRequests1m: 10,
|
|
MinTPMTokens1m: 1_000_000,
|
|
}
|
|
if err := db.Create(&cfg).Error; err != nil {
|
|
t.Fatalf("create config: %v", err)
|
|
}
|
|
|
|
// Create only a few log records (below threshold)
|
|
now := time.Now().UTC()
|
|
for i := 0; i < 3; i++ {
|
|
log := model.LogRecord{
|
|
MasterID: master.ID,
|
|
KeyID: 1,
|
|
TokensIn: 100,
|
|
TokensOut: 200,
|
|
}
|
|
if err := db.Create(&log).Error; err != nil {
|
|
t.Fatalf("create log: %v", err)
|
|
}
|
|
db.Model(&log).Update("created_at", now.Add(-time.Duration(i)*time.Second))
|
|
}
|
|
|
|
config := DefaultAlertDetectorConfig()
|
|
detector := NewAlertDetector(db, db, nil, nil, config, nil)
|
|
|
|
// Run detection
|
|
detector.detectMasterMinuteSpikes(context.Background(), cfg)
|
|
|
|
// Should NOT have created any alerts (below threshold and minimum sample)
|
|
var count int64
|
|
if err := db.Model(&model.Alert{}).Where("type = ?", model.AlertTypeTrafficSpike).Count(&count).Error; err != nil {
|
|
t.Fatalf("count alerts: %v", err)
|
|
}
|
|
|
|
if count != 0 {
|
|
t.Errorf("expected 0 alerts when below threshold, got %d", count)
|
|
}
|
|
}
|