| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "go.uber.org/zap"
- )
- func defaultOpsRuntimeLogConfig(cfg *config.Config) *OpsRuntimeLogConfig {
- out := &OpsRuntimeLogConfig{
- Level: "info",
- PersistAccessLogs: false,
- EnableSampling: false,
- SamplingInitial: 100,
- SamplingNext: 100,
- Caller: true,
- StacktraceLevel: "error",
- RetentionDays: 30,
- }
- if cfg == nil {
- return out
- }
- out.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
- out.EnableSampling = cfg.Log.Sampling.Enabled
- out.SamplingInitial = cfg.Log.Sampling.Initial
- out.SamplingNext = cfg.Log.Sampling.Thereafter
- out.Caller = cfg.Log.Caller
- out.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.Log.StacktraceLevel))
- if cfg.Ops.Cleanup.SystemLogRetentionDays > 0 {
- out.RetentionDays = cfg.Ops.Cleanup.SystemLogRetentionDays
- }
- return out
- }
- func normalizeOpsRuntimeLogConfig(cfg *OpsRuntimeLogConfig, defaults *OpsRuntimeLogConfig) {
- if cfg == nil || defaults == nil {
- return
- }
- cfg.Level = strings.ToLower(strings.TrimSpace(cfg.Level))
- if cfg.Level == "" {
- cfg.Level = defaults.Level
- }
- cfg.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.StacktraceLevel))
- if cfg.StacktraceLevel == "" {
- cfg.StacktraceLevel = defaults.StacktraceLevel
- }
- if cfg.SamplingInitial <= 0 {
- cfg.SamplingInitial = defaults.SamplingInitial
- }
- if cfg.SamplingNext <= 0 {
- cfg.SamplingNext = defaults.SamplingNext
- }
- if cfg.RetentionDays <= 0 {
- cfg.RetentionDays = defaults.RetentionDays
- }
- }
- func validateOpsRuntimeLogConfig(cfg *OpsRuntimeLogConfig) error {
- if cfg == nil {
- return errors.New("invalid config")
- }
- switch strings.ToLower(strings.TrimSpace(cfg.Level)) {
- case "debug", "info", "warn", "error":
- default:
- return errors.New("level must be one of: debug/info/warn/error")
- }
- switch strings.ToLower(strings.TrimSpace(cfg.StacktraceLevel)) {
- case "none", "error", "fatal":
- default:
- return errors.New("stacktrace_level must be one of: none/error/fatal")
- }
- if cfg.SamplingInitial <= 0 {
- return errors.New("sampling_initial must be positive")
- }
- if cfg.SamplingNext <= 0 {
- return errors.New("sampling_thereafter must be positive")
- }
- if cfg.RetentionDays < 1 || cfg.RetentionDays > 3650 {
- return errors.New("retention_days must be between 1 and 3650")
- }
- return nil
- }
- func (s *OpsService) GetRuntimeLogConfig(ctx context.Context) (*OpsRuntimeLogConfig, error) {
- if s == nil || s.settingRepo == nil {
- var cfg *config.Config
- if s != nil {
- cfg = s.cfg
- }
- defaultCfg := defaultOpsRuntimeLogConfig(cfg)
- return defaultCfg, nil
- }
- defaultCfg := defaultOpsRuntimeLogConfig(s.cfg)
- if ctx == nil {
- ctx = context.Background()
- }
- raw, err := s.settingRepo.GetValue(ctx, SettingKeyOpsRuntimeLogConfig)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- b, _ := json.Marshal(defaultCfg)
- _ = s.settingRepo.Set(ctx, SettingKeyOpsRuntimeLogConfig, string(b))
- return defaultCfg, nil
- }
- return nil, err
- }
- cfg := &OpsRuntimeLogConfig{}
- if err := json.Unmarshal([]byte(raw), cfg); err != nil {
- return defaultCfg, nil
- }
- normalizeOpsRuntimeLogConfig(cfg, defaultCfg)
- return cfg, nil
- }
- func (s *OpsService) UpdateRuntimeLogConfig(ctx context.Context, req *OpsRuntimeLogConfig, operatorID int64) (*OpsRuntimeLogConfig, error) {
- if s == nil || s.settingRepo == nil {
- return nil, errors.New("setting repository not initialized")
- }
- if req == nil {
- return nil, errors.New("invalid config")
- }
- if ctx == nil {
- ctx = context.Background()
- }
- if operatorID <= 0 {
- return nil, errors.New("invalid operator id")
- }
- oldCfg, err := s.GetRuntimeLogConfig(ctx)
- if err != nil {
- return nil, err
- }
- next := *req
- normalizeOpsRuntimeLogConfig(&next, defaultOpsRuntimeLogConfig(s.cfg))
- if err := validateOpsRuntimeLogConfig(&next); err != nil {
- s.auditRuntimeLogConfigFailure(operatorID, oldCfg, &next, "validation_failed: "+err.Error())
- return nil, err
- }
- if err := s.applyRuntimeLogConfig(&next); err != nil {
- s.auditRuntimeLogConfigFailure(operatorID, oldCfg, &next, "apply_failed: "+err.Error())
- return nil, err
- }
- next.Source = "runtime_setting"
- next.UpdatedAt = time.Now().UTC().Format(time.RFC3339Nano)
- next.UpdatedByUserID = operatorID
- encoded, err := json.Marshal(&next)
- if err != nil {
- return nil, err
- }
- if err := s.settingRepo.Set(ctx, SettingKeyOpsRuntimeLogConfig, string(encoded)); err != nil {
- // 存储失败时回滚到旧配置,避免内存状态与持久化状态不一致。
- _ = s.applyRuntimeLogConfig(oldCfg)
- s.auditRuntimeLogConfigFailure(operatorID, oldCfg, &next, "persist_failed: "+err.Error())
- return nil, err
- }
- s.auditRuntimeLogConfigChange(operatorID, oldCfg, &next, "updated")
- s.reloadOpsCleanup(ctx)
- return &next, nil
- }
- func (s *OpsService) ResetRuntimeLogConfig(ctx context.Context, operatorID int64) (*OpsRuntimeLogConfig, error) {
- if s == nil || s.settingRepo == nil {
- return nil, errors.New("setting repository not initialized")
- }
- if ctx == nil {
- ctx = context.Background()
- }
- if operatorID <= 0 {
- return nil, errors.New("invalid operator id")
- }
- oldCfg, err := s.GetRuntimeLogConfig(ctx)
- if err != nil {
- return nil, err
- }
- resetCfg := defaultOpsRuntimeLogConfig(s.cfg)
- normalizeOpsRuntimeLogConfig(resetCfg, defaultOpsRuntimeLogConfig(s.cfg))
- if err := validateOpsRuntimeLogConfig(resetCfg); err != nil {
- s.auditRuntimeLogConfigFailure(operatorID, oldCfg, resetCfg, "reset_validation_failed: "+err.Error())
- return nil, err
- }
- if err := s.applyRuntimeLogConfig(resetCfg); err != nil {
- s.auditRuntimeLogConfigFailure(operatorID, oldCfg, resetCfg, "reset_apply_failed: "+err.Error())
- return nil, err
- }
- // 清理 runtime 覆盖配置,回退到 env/yaml baseline。
- if err := s.settingRepo.Delete(ctx, SettingKeyOpsRuntimeLogConfig); err != nil && !errors.Is(err, ErrSettingNotFound) {
- _ = s.applyRuntimeLogConfig(oldCfg)
- s.auditRuntimeLogConfigFailure(operatorID, oldCfg, resetCfg, "reset_persist_failed: "+err.Error())
- return nil, err
- }
- now := time.Now().UTC().Format(time.RFC3339Nano)
- resetCfg.Source = "baseline"
- resetCfg.UpdatedAt = now
- resetCfg.UpdatedByUserID = operatorID
- s.auditRuntimeLogConfigChange(operatorID, oldCfg, resetCfg, "reset")
- s.reloadOpsCleanup(ctx)
- return resetCfg, nil
- }
- func (s *OpsService) applyRuntimeLogConfig(cfg *OpsRuntimeLogConfig) error {
- if err := applyOpsRuntimeLogConfig(cfg); err != nil {
- return err
- }
- if s != nil && s.systemLogSink != nil {
- s.systemLogSink.SetPersistAccessLogs(cfg.PersistAccessLogs)
- }
- return nil
- }
- func (s *OpsService) reloadOpsCleanup(ctx context.Context) {
- if s == nil || s.cleanupReloader == nil {
- return
- }
- if err := s.cleanupReloader.Reload(ctx); err != nil {
- logger.LegacyPrintf("service.ops_log_runtime", "[OpsLogRuntime] cleanup reload failed: %v", err)
- }
- }
- func applyOpsRuntimeLogConfig(cfg *OpsRuntimeLogConfig) error {
- if cfg == nil {
- return fmt.Errorf("nil runtime log config")
- }
- if err := logger.Reconfigure(func(opts *logger.InitOptions) error {
- opts.Level = strings.ToLower(strings.TrimSpace(cfg.Level))
- opts.Caller = cfg.Caller
- opts.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.StacktraceLevel))
- opts.Sampling.Enabled = cfg.EnableSampling
- opts.Sampling.Initial = cfg.SamplingInitial
- opts.Sampling.Thereafter = cfg.SamplingNext
- return nil
- }); err != nil {
- return err
- }
- return nil
- }
- func (s *OpsService) applyRuntimeLogConfigOnStartup(ctx context.Context) {
- if s == nil {
- return
- }
- cfg, err := s.GetRuntimeLogConfig(ctx)
- if err != nil {
- return
- }
- _ = s.applyRuntimeLogConfig(cfg)
- }
- func (s *OpsService) auditRuntimeLogConfigChange(operatorID int64, oldCfg *OpsRuntimeLogConfig, newCfg *OpsRuntimeLogConfig, action string) {
- oldRaw, _ := json.Marshal(oldCfg)
- newRaw, _ := json.Marshal(newCfg)
- logger.With(
- zap.String("component", "audit.log_config_change"),
- zap.String("action", strings.TrimSpace(action)),
- zap.Int64("operator_id", operatorID),
- zap.String("old", string(oldRaw)),
- zap.String("new", string(newRaw)),
- ).Info("runtime log config changed")
- }
- func (s *OpsService) auditRuntimeLogConfigFailure(operatorID int64, oldCfg *OpsRuntimeLogConfig, newCfg *OpsRuntimeLogConfig, reason string) {
- oldRaw, _ := json.Marshal(oldCfg)
- newRaw, _ := json.Marshal(newCfg)
- logger.With(
- zap.String("component", "audit.log_config_change"),
- zap.String("action", "failed"),
- zap.Int64("operator_id", operatorID),
- zap.String("reason", strings.TrimSpace(reason)),
- zap.String("old", string(oldRaw)),
- zap.String("new", string(newRaw)),
- ).Warn("runtime log config change failed")
- }
|