| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- )
- const (
- opsAlertEvaluatorLeaderLockKeyDefault = "ops:alert:evaluator:leader"
- opsAlertEvaluatorLeaderLockTTLDefault = 30 * time.Second
- )
- // =========================
- // Email notification config
- // =========================
- func (s *OpsService) GetEmailNotificationConfig(ctx context.Context) (*OpsEmailNotificationConfig, error) {
- defaultCfg := defaultOpsEmailNotificationConfig()
- if s == nil || s.settingRepo == nil {
- return defaultCfg, nil
- }
- if ctx == nil {
- ctx = context.Background()
- }
- raw, err := s.settingRepo.GetValue(ctx, SettingKeyOpsEmailNotificationConfig)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- // Initialize defaults on first read (best-effort).
- if b, mErr := json.Marshal(defaultCfg); mErr == nil {
- _ = s.settingRepo.Set(ctx, SettingKeyOpsEmailNotificationConfig, string(b))
- }
- return defaultCfg, nil
- }
- return nil, err
- }
- cfg := &OpsEmailNotificationConfig{}
- if err := json.Unmarshal([]byte(raw), cfg); err != nil {
- // Corrupted JSON should not break ops UI; fall back to defaults.
- return defaultCfg, nil
- }
- normalizeOpsEmailNotificationConfig(cfg)
- return cfg, nil
- }
- func (s *OpsService) UpdateEmailNotificationConfig(ctx context.Context, req *OpsEmailNotificationConfigUpdateRequest) (*OpsEmailNotificationConfig, error) {
- if s == nil || s.settingRepo == nil {
- return nil, errors.New("setting repository not initialized")
- }
- if ctx == nil {
- ctx = context.Background()
- }
- if req == nil {
- return nil, errors.New("invalid request")
- }
- cfg, err := s.GetEmailNotificationConfig(ctx)
- if err != nil {
- return nil, err
- }
- if req.Alert != nil {
- cfg.Alert.Enabled = req.Alert.Enabled
- if req.Alert.Recipients != nil {
- cfg.Alert.Recipients = req.Alert.Recipients
- }
- cfg.Alert.MinSeverity = strings.TrimSpace(req.Alert.MinSeverity)
- cfg.Alert.RateLimitPerHour = req.Alert.RateLimitPerHour
- cfg.Alert.BatchingWindowSeconds = req.Alert.BatchingWindowSeconds
- cfg.Alert.IncludeResolvedAlerts = req.Alert.IncludeResolvedAlerts
- }
- if req.Report != nil {
- cfg.Report.Enabled = req.Report.Enabled
- if req.Report.Recipients != nil {
- cfg.Report.Recipients = req.Report.Recipients
- }
- cfg.Report.DailySummaryEnabled = req.Report.DailySummaryEnabled
- cfg.Report.DailySummarySchedule = strings.TrimSpace(req.Report.DailySummarySchedule)
- cfg.Report.WeeklySummaryEnabled = req.Report.WeeklySummaryEnabled
- cfg.Report.WeeklySummarySchedule = strings.TrimSpace(req.Report.WeeklySummarySchedule)
- cfg.Report.ErrorDigestEnabled = req.Report.ErrorDigestEnabled
- cfg.Report.ErrorDigestSchedule = strings.TrimSpace(req.Report.ErrorDigestSchedule)
- cfg.Report.ErrorDigestMinCount = req.Report.ErrorDigestMinCount
- cfg.Report.AccountHealthEnabled = req.Report.AccountHealthEnabled
- cfg.Report.AccountHealthSchedule = strings.TrimSpace(req.Report.AccountHealthSchedule)
- cfg.Report.AccountHealthErrorRateThreshold = req.Report.AccountHealthErrorRateThreshold
- }
- if err := validateOpsEmailNotificationConfig(cfg); err != nil {
- return nil, err
- }
- normalizeOpsEmailNotificationConfig(cfg)
- raw, err := json.Marshal(cfg)
- if err != nil {
- return nil, err
- }
- if err := s.settingRepo.Set(ctx, SettingKeyOpsEmailNotificationConfig, string(raw)); err != nil {
- return nil, err
- }
- return cfg, nil
- }
- func defaultOpsEmailNotificationConfig() *OpsEmailNotificationConfig {
- return &OpsEmailNotificationConfig{
- Alert: OpsEmailAlertConfig{
- Enabled: true,
- Recipients: []string{},
- MinSeverity: "",
- RateLimitPerHour: 0,
- BatchingWindowSeconds: 0,
- IncludeResolvedAlerts: false,
- },
- Report: OpsEmailReportConfig{
- Enabled: false,
- Recipients: []string{},
- DailySummaryEnabled: false,
- DailySummarySchedule: "0 9 * * *",
- WeeklySummaryEnabled: false,
- WeeklySummarySchedule: "0 9 * * 1",
- ErrorDigestEnabled: false,
- ErrorDigestSchedule: "0 9 * * *",
- ErrorDigestMinCount: 10,
- AccountHealthEnabled: false,
- AccountHealthSchedule: "0 9 * * *",
- AccountHealthErrorRateThreshold: 10.0,
- },
- }
- }
- func normalizeOpsEmailNotificationConfig(cfg *OpsEmailNotificationConfig) {
- if cfg == nil {
- return
- }
- if cfg.Alert.Recipients == nil {
- cfg.Alert.Recipients = []string{}
- }
- if cfg.Report.Recipients == nil {
- cfg.Report.Recipients = []string{}
- }
- cfg.Alert.MinSeverity = strings.TrimSpace(cfg.Alert.MinSeverity)
- cfg.Report.DailySummarySchedule = strings.TrimSpace(cfg.Report.DailySummarySchedule)
- cfg.Report.WeeklySummarySchedule = strings.TrimSpace(cfg.Report.WeeklySummarySchedule)
- cfg.Report.ErrorDigestSchedule = strings.TrimSpace(cfg.Report.ErrorDigestSchedule)
- cfg.Report.AccountHealthSchedule = strings.TrimSpace(cfg.Report.AccountHealthSchedule)
- // Fill missing schedules with defaults to avoid breaking cron logic if clients send empty strings.
- if cfg.Report.DailySummarySchedule == "" {
- cfg.Report.DailySummarySchedule = "0 9 * * *"
- }
- if cfg.Report.WeeklySummarySchedule == "" {
- cfg.Report.WeeklySummarySchedule = "0 9 * * 1"
- }
- if cfg.Report.ErrorDigestSchedule == "" {
- cfg.Report.ErrorDigestSchedule = "0 9 * * *"
- }
- if cfg.Report.AccountHealthSchedule == "" {
- cfg.Report.AccountHealthSchedule = "0 9 * * *"
- }
- }
- func validateOpsEmailNotificationConfig(cfg *OpsEmailNotificationConfig) error {
- if cfg == nil {
- return errors.New("invalid config")
- }
- if cfg.Alert.RateLimitPerHour < 0 {
- return errors.New("alert.rate_limit_per_hour must be >= 0")
- }
- if cfg.Alert.BatchingWindowSeconds < 0 {
- return errors.New("alert.batching_window_seconds must be >= 0")
- }
- switch strings.TrimSpace(cfg.Alert.MinSeverity) {
- case "", "critical", "warning", "info":
- default:
- return errors.New("alert.min_severity must be one of: critical, warning, info, or empty")
- }
- if cfg.Report.ErrorDigestMinCount < 0 {
- return errors.New("report.error_digest_min_count must be >= 0")
- }
- if cfg.Report.AccountHealthErrorRateThreshold < 0 || cfg.Report.AccountHealthErrorRateThreshold > 100 {
- return errors.New("report.account_health_error_rate_threshold must be between 0 and 100")
- }
- return nil
- }
- // =========================
- // Alert runtime settings
- // =========================
- func defaultOpsAlertRuntimeSettings() *OpsAlertRuntimeSettings {
- return &OpsAlertRuntimeSettings{
- EvaluationIntervalSeconds: 60,
- DistributedLock: OpsDistributedLockSettings{
- Enabled: true,
- Key: opsAlertEvaluatorLeaderLockKeyDefault,
- TTLSeconds: int(opsAlertEvaluatorLeaderLockTTLDefault.Seconds()),
- },
- Silencing: OpsAlertSilencingSettings{
- Enabled: false,
- GlobalUntilRFC3339: "",
- GlobalReason: "",
- Entries: []OpsAlertSilenceEntry{},
- },
- }
- }
- func normalizeOpsDistributedLockSettings(s *OpsDistributedLockSettings, defaultKey string, defaultTTLSeconds int) {
- if s == nil {
- return
- }
- s.Key = strings.TrimSpace(s.Key)
- if s.Key == "" {
- s.Key = defaultKey
- }
- if s.TTLSeconds <= 0 {
- s.TTLSeconds = defaultTTLSeconds
- }
- }
- func normalizeOpsAlertSilencingSettings(s *OpsAlertSilencingSettings) {
- if s == nil {
- return
- }
- s.GlobalUntilRFC3339 = strings.TrimSpace(s.GlobalUntilRFC3339)
- s.GlobalReason = strings.TrimSpace(s.GlobalReason)
- if s.Entries == nil {
- s.Entries = []OpsAlertSilenceEntry{}
- }
- for i := range s.Entries {
- s.Entries[i].UntilRFC3339 = strings.TrimSpace(s.Entries[i].UntilRFC3339)
- s.Entries[i].Reason = strings.TrimSpace(s.Entries[i].Reason)
- }
- }
- func validateOpsDistributedLockSettings(s OpsDistributedLockSettings) error {
- if strings.TrimSpace(s.Key) == "" {
- return errors.New("distributed_lock.key is required")
- }
- if s.TTLSeconds <= 0 || s.TTLSeconds > int((24*time.Hour).Seconds()) {
- return errors.New("distributed_lock.ttl_seconds must be between 1 and 86400")
- }
- return nil
- }
- func validateOpsAlertSilencingSettings(s OpsAlertSilencingSettings) error {
- parse := func(raw string) error {
- if strings.TrimSpace(raw) == "" {
- return nil
- }
- if _, err := time.Parse(time.RFC3339, raw); err != nil {
- return errors.New("silencing time must be RFC3339")
- }
- return nil
- }
- if err := parse(s.GlobalUntilRFC3339); err != nil {
- return err
- }
- for _, entry := range s.Entries {
- if strings.TrimSpace(entry.UntilRFC3339) == "" {
- return errors.New("silencing.entries.until_rfc3339 is required")
- }
- if _, err := time.Parse(time.RFC3339, entry.UntilRFC3339); err != nil {
- return errors.New("silencing.entries.until_rfc3339 must be RFC3339")
- }
- }
- return nil
- }
- func (s *OpsService) GetOpsAlertRuntimeSettings(ctx context.Context) (*OpsAlertRuntimeSettings, error) {
- defaultCfg := defaultOpsAlertRuntimeSettings()
- if s == nil || s.settingRepo == nil {
- return defaultCfg, nil
- }
- if ctx == nil {
- ctx = context.Background()
- }
- raw, err := s.settingRepo.GetValue(ctx, SettingKeyOpsAlertRuntimeSettings)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- if b, mErr := json.Marshal(defaultCfg); mErr == nil {
- _ = s.settingRepo.Set(ctx, SettingKeyOpsAlertRuntimeSettings, string(b))
- }
- return defaultCfg, nil
- }
- return nil, err
- }
- cfg := &OpsAlertRuntimeSettings{}
- if err := json.Unmarshal([]byte(raw), cfg); err != nil {
- return defaultCfg, nil
- }
- if cfg.EvaluationIntervalSeconds <= 0 {
- cfg.EvaluationIntervalSeconds = defaultCfg.EvaluationIntervalSeconds
- }
- normalizeOpsDistributedLockSettings(&cfg.DistributedLock, opsAlertEvaluatorLeaderLockKeyDefault, defaultCfg.DistributedLock.TTLSeconds)
- normalizeOpsAlertSilencingSettings(&cfg.Silencing)
- return cfg, nil
- }
- func (s *OpsService) UpdateOpsAlertRuntimeSettings(ctx context.Context, cfg *OpsAlertRuntimeSettings) (*OpsAlertRuntimeSettings, error) {
- if s == nil || s.settingRepo == nil {
- return nil, errors.New("setting repository not initialized")
- }
- if ctx == nil {
- ctx = context.Background()
- }
- if cfg == nil {
- return nil, errors.New("invalid config")
- }
- if cfg.EvaluationIntervalSeconds < 1 || cfg.EvaluationIntervalSeconds > int((24*time.Hour).Seconds()) {
- return nil, errors.New("evaluation_interval_seconds must be between 1 and 86400")
- }
- if cfg.DistributedLock.Enabled {
- if err := validateOpsDistributedLockSettings(cfg.DistributedLock); err != nil {
- return nil, err
- }
- }
- if cfg.Silencing.Enabled {
- if err := validateOpsAlertSilencingSettings(cfg.Silencing); err != nil {
- return nil, err
- }
- }
- defaultCfg := defaultOpsAlertRuntimeSettings()
- normalizeOpsDistributedLockSettings(&cfg.DistributedLock, opsAlertEvaluatorLeaderLockKeyDefault, defaultCfg.DistributedLock.TTLSeconds)
- normalizeOpsAlertSilencingSettings(&cfg.Silencing)
- raw, err := json.Marshal(cfg)
- if err != nil {
- return nil, err
- }
- if err := s.settingRepo.Set(ctx, SettingKeyOpsAlertRuntimeSettings, string(raw)); err != nil {
- return nil, err
- }
- // Return a fresh copy (avoid callers holding pointers into internal slices that may be mutated).
- updated := &OpsAlertRuntimeSettings{}
- _ = json.Unmarshal(raw, updated)
- return updated, nil
- }
- // =========================
- // Advanced settings
- // =========================
- func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
- return &OpsAdvancedSettings{
- DataRetention: OpsDataRetentionSettings{
- // Keep the DB-backed default aligned with ops.cleanup.enabled so a
- // first settings read cannot silently disable configured retention.
- CleanupEnabled: true,
- CleanupSchedule: opsCleanupDefaultSchedule,
- ErrorLogRetentionDays: 30,
- MinuteMetricsRetentionDays: 30,
- HourlyMetricsRetentionDays: 30,
- },
- Aggregation: OpsAggregationSettings{
- AggregationEnabled: false,
- },
- OpenAIAccountQuotaAutoPause: OpsOpenAIAccountQuotaAutoPauseSettings{},
- IgnoreCountTokensErrors: true, // count_tokens 404 是预期行为,默认忽略
- IgnoreContextCanceled: true, // Default to true - client disconnects are not errors
- IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue
- IgnoreInvalidApiKeyErrors: true, // Legacy compatibility field; admission rejects are always excluded.
- IgnoreInsufficientBalanceErrors: false, // 默认不忽略,余额不足可能需要关注
- DisplayOpenAITokenStats: false,
- DisplayAlertEvents: true,
- AutoRefreshEnabled: false,
- AutoRefreshIntervalSec: 30,
- }
- }
- func defaultOpsAdvancedSettingsForConfig(cfg *config.Config) *OpsAdvancedSettings {
- defaults := defaultOpsAdvancedSettings()
- if cfg != nil {
- defaults.DataRetention.CleanupEnabled = cfg.Ops.Cleanup.Enabled
- }
- return defaults
- }
- func normalizeOpsAdvancedSettings(cfg *OpsAdvancedSettings) {
- if cfg == nil {
- return
- }
- // Admission rejects are a security/traffic concern, not an operational
- // request-error category. Keep the legacy field true for old clients but do
- // not allow it to re-enable those rows.
- cfg.IgnoreInvalidApiKeyErrors = true
- cfg.OpenAIAccountQuotaAutoPause.DefaultThreshold5h = clampOpsQuotaAutoPauseThreshold(cfg.OpenAIAccountQuotaAutoPause.DefaultThreshold5h)
- cfg.OpenAIAccountQuotaAutoPause.DefaultThreshold7d = clampOpsQuotaAutoPauseThreshold(cfg.OpenAIAccountQuotaAutoPause.DefaultThreshold7d)
- cfg.DataRetention.CleanupSchedule = strings.TrimSpace(cfg.DataRetention.CleanupSchedule)
- if cfg.DataRetention.CleanupSchedule == "" {
- cfg.DataRetention.CleanupSchedule = opsCleanupDefaultSchedule
- }
- // 保留天数:0 表示每次定时清理全部(清空所有),> 0 表示按天数保留;
- // 仅在拿到非法的负数时回填默认值,避免覆盖用户主动设的 0。
- if cfg.DataRetention.ErrorLogRetentionDays < 0 {
- cfg.DataRetention.ErrorLogRetentionDays = 30
- }
- if cfg.DataRetention.MinuteMetricsRetentionDays < 0 {
- cfg.DataRetention.MinuteMetricsRetentionDays = 30
- }
- if cfg.DataRetention.HourlyMetricsRetentionDays < 0 {
- cfg.DataRetention.HourlyMetricsRetentionDays = 30
- }
- // Normalize auto refresh interval (default 30 seconds)
- if cfg.AutoRefreshIntervalSec <= 0 {
- cfg.AutoRefreshIntervalSec = 30
- }
- }
- func clampOpsQuotaAutoPauseThreshold(value float64) float64 {
- if value <= 0 {
- return 0
- }
- if value > 1 {
- return 1
- }
- return value
- }
- func validateOpsAdvancedSettings(cfg *OpsAdvancedSettings) error {
- if cfg == nil {
- return errors.New("invalid config")
- }
- // 保留天数:0 表示每次清理全部,1-365 表示按天数保留。
- if cfg.DataRetention.ErrorLogRetentionDays < 0 || cfg.DataRetention.ErrorLogRetentionDays > 365 {
- return errors.New("error_log_retention_days must be between 0 and 365")
- }
- if cfg.DataRetention.MinuteMetricsRetentionDays < 0 || cfg.DataRetention.MinuteMetricsRetentionDays > 365 {
- return errors.New("minute_metrics_retention_days must be between 0 and 365")
- }
- if cfg.DataRetention.HourlyMetricsRetentionDays < 0 || cfg.DataRetention.HourlyMetricsRetentionDays > 365 {
- return errors.New("hourly_metrics_retention_days must be between 0 and 365")
- }
- if cfg.AutoRefreshIntervalSec < 15 || cfg.AutoRefreshIntervalSec > 300 {
- return errors.New("auto_refresh_interval_seconds must be between 15 and 300")
- }
- return nil
- }
- func (s *OpsService) GetOpsAdvancedSettings(ctx context.Context) (*OpsAdvancedSettings, error) {
- _ = ctx
- cfg := s.OpsAdvancedSettingsSnapshot()
- return &cfg, nil
- }
- // OpsAdvancedSettingsSnapshot returns a value copy for request hot paths. It
- // avoids both repository I/O and pointer escape/allocation.
- func (s *OpsService) OpsAdvancedSettingsSnapshot() OpsAdvancedSettings {
- if s != nil {
- if snapshot := s.runtimeSettings.Load(); snapshot != nil {
- return snapshot.advanced
- }
- return *defaultOpsAdvancedSettingsForConfig(s.cfg)
- }
- return *defaultOpsAdvancedSettings()
- }
- func (s *OpsService) UpdateOpsAdvancedSettings(ctx context.Context, cfg *OpsAdvancedSettings) (*OpsAdvancedSettings, error) {
- if s == nil || s.settingRepo == nil {
- return nil, errors.New("setting repository not initialized")
- }
- if ctx == nil {
- ctx = context.Background()
- }
- if cfg == nil {
- return nil, errors.New("invalid config")
- }
- if err := validateOpsAdvancedSettings(cfg); err != nil {
- return nil, err
- }
- normalizeOpsAdvancedSettings(cfg)
- raw, err := json.Marshal(cfg)
- if err != nil {
- return nil, err
- }
- if err := s.settingRepo.Set(ctx, SettingKeyOpsAdvancedSettings, string(raw)); err != nil {
- return nil, err
- }
- s.storeAdvancedSettingsSnapshot(cfg)
- // Push the new quota auto-pause settings straight into the in-memory cache that
- // the OpenAI scheduling hot path reads, so the next request observes the new value
- // without waiting for the background refresher's TTL.
- if s.quotaAutoPauseSink != nil {
- s.quotaAutoPauseSink(cfg.OpenAIAccountQuotaAutoPause)
- }
- // notify cleanup service to reload schedule/enabled.
- if s.cleanupReloader != nil {
- if rerr := s.cleanupReloader.Reload(ctx); rerr != nil {
- logger.LegacyPrintf("service.ops_settings",
- "[OpsSettings] cleanup reload after advanced-settings update failed: %v", rerr)
- }
- }
- updated := &OpsAdvancedSettings{}
- _ = json.Unmarshal(raw, updated)
- return updated, nil
- }
- // =========================
- // Metric thresholds
- // =========================
- const SettingKeyOpsMetricThresholds = "ops_metric_thresholds"
- func defaultOpsMetricThresholds() *OpsMetricThresholds {
- slaMin := 99.5
- ttftMax := 500.0
- reqErrMax := 5.0
- upstreamErrMax := 5.0
- return &OpsMetricThresholds{
- SLAPercentMin: &slaMin,
- TTFTp99MsMax: &ttftMax,
- RequestErrorRatePercentMax: &reqErrMax,
- UpstreamErrorRatePercentMax: &upstreamErrMax,
- }
- }
- func (s *OpsService) GetMetricThresholds(ctx context.Context) (*OpsMetricThresholds, error) {
- defaultCfg := defaultOpsMetricThresholds()
- if s == nil || s.settingRepo == nil {
- return defaultCfg, nil
- }
- if ctx == nil {
- ctx = context.Background()
- }
- raw, err := s.settingRepo.GetValue(ctx, SettingKeyOpsMetricThresholds)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- if b, mErr := json.Marshal(defaultCfg); mErr == nil {
- _ = s.settingRepo.Set(ctx, SettingKeyOpsMetricThresholds, string(b))
- }
- return defaultCfg, nil
- }
- return nil, err
- }
- cfg := &OpsMetricThresholds{}
- if err := json.Unmarshal([]byte(raw), cfg); err != nil {
- return defaultCfg, nil
- }
- return cfg, nil
- }
- func (s *OpsService) UpdateMetricThresholds(ctx context.Context, cfg *OpsMetricThresholds) (*OpsMetricThresholds, error) {
- if s == nil || s.settingRepo == nil {
- return nil, errors.New("setting repository not initialized")
- }
- if ctx == nil {
- ctx = context.Background()
- }
- if cfg == nil {
- return nil, errors.New("invalid config")
- }
- // Validate thresholds
- if cfg.SLAPercentMin != nil && (*cfg.SLAPercentMin < 0 || *cfg.SLAPercentMin > 100) {
- return nil, errors.New("sla_percent_min must be between 0 and 100")
- }
- if cfg.TTFTp99MsMax != nil && *cfg.TTFTp99MsMax < 0 {
- return nil, errors.New("ttft_p99_ms_max must be >= 0")
- }
- if cfg.RequestErrorRatePercentMax != nil && (*cfg.RequestErrorRatePercentMax < 0 || *cfg.RequestErrorRatePercentMax > 100) {
- return nil, errors.New("request_error_rate_percent_max must be between 0 and 100")
- }
- if cfg.UpstreamErrorRatePercentMax != nil && (*cfg.UpstreamErrorRatePercentMax < 0 || *cfg.UpstreamErrorRatePercentMax > 100) {
- return nil, errors.New("upstream_error_rate_percent_max must be between 0 and 100")
- }
- raw, err := json.Marshal(cfg)
- if err != nil {
- return nil, err
- }
- if err := s.settingRepo.Set(ctx, SettingKeyOpsMetricThresholds, string(raw)); err != nil {
- return nil, err
- }
- updated := &OpsMetricThresholds{}
- _ = json.Unmarshal(raw, updated)
- return updated, nil
- }
|