ops_log_runtime.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  11. "go.uber.org/zap"
  12. )
  13. func defaultOpsRuntimeLogConfig(cfg *config.Config) *OpsRuntimeLogConfig {
  14. out := &OpsRuntimeLogConfig{
  15. Level: "info",
  16. PersistAccessLogs: false,
  17. EnableSampling: false,
  18. SamplingInitial: 100,
  19. SamplingNext: 100,
  20. Caller: true,
  21. StacktraceLevel: "error",
  22. RetentionDays: 30,
  23. }
  24. if cfg == nil {
  25. return out
  26. }
  27. out.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
  28. out.EnableSampling = cfg.Log.Sampling.Enabled
  29. out.SamplingInitial = cfg.Log.Sampling.Initial
  30. out.SamplingNext = cfg.Log.Sampling.Thereafter
  31. out.Caller = cfg.Log.Caller
  32. out.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.Log.StacktraceLevel))
  33. if cfg.Ops.Cleanup.SystemLogRetentionDays > 0 {
  34. out.RetentionDays = cfg.Ops.Cleanup.SystemLogRetentionDays
  35. }
  36. return out
  37. }
  38. func normalizeOpsRuntimeLogConfig(cfg *OpsRuntimeLogConfig, defaults *OpsRuntimeLogConfig) {
  39. if cfg == nil || defaults == nil {
  40. return
  41. }
  42. cfg.Level = strings.ToLower(strings.TrimSpace(cfg.Level))
  43. if cfg.Level == "" {
  44. cfg.Level = defaults.Level
  45. }
  46. cfg.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.StacktraceLevel))
  47. if cfg.StacktraceLevel == "" {
  48. cfg.StacktraceLevel = defaults.StacktraceLevel
  49. }
  50. if cfg.SamplingInitial <= 0 {
  51. cfg.SamplingInitial = defaults.SamplingInitial
  52. }
  53. if cfg.SamplingNext <= 0 {
  54. cfg.SamplingNext = defaults.SamplingNext
  55. }
  56. if cfg.RetentionDays <= 0 {
  57. cfg.RetentionDays = defaults.RetentionDays
  58. }
  59. }
  60. func validateOpsRuntimeLogConfig(cfg *OpsRuntimeLogConfig) error {
  61. if cfg == nil {
  62. return errors.New("invalid config")
  63. }
  64. switch strings.ToLower(strings.TrimSpace(cfg.Level)) {
  65. case "debug", "info", "warn", "error":
  66. default:
  67. return errors.New("level must be one of: debug/info/warn/error")
  68. }
  69. switch strings.ToLower(strings.TrimSpace(cfg.StacktraceLevel)) {
  70. case "none", "error", "fatal":
  71. default:
  72. return errors.New("stacktrace_level must be one of: none/error/fatal")
  73. }
  74. if cfg.SamplingInitial <= 0 {
  75. return errors.New("sampling_initial must be positive")
  76. }
  77. if cfg.SamplingNext <= 0 {
  78. return errors.New("sampling_thereafter must be positive")
  79. }
  80. if cfg.RetentionDays < 1 || cfg.RetentionDays > 3650 {
  81. return errors.New("retention_days must be between 1 and 3650")
  82. }
  83. return nil
  84. }
  85. func (s *OpsService) GetRuntimeLogConfig(ctx context.Context) (*OpsRuntimeLogConfig, error) {
  86. if s == nil || s.settingRepo == nil {
  87. var cfg *config.Config
  88. if s != nil {
  89. cfg = s.cfg
  90. }
  91. defaultCfg := defaultOpsRuntimeLogConfig(cfg)
  92. return defaultCfg, nil
  93. }
  94. defaultCfg := defaultOpsRuntimeLogConfig(s.cfg)
  95. if ctx == nil {
  96. ctx = context.Background()
  97. }
  98. raw, err := s.settingRepo.GetValue(ctx, SettingKeyOpsRuntimeLogConfig)
  99. if err != nil {
  100. if errors.Is(err, ErrSettingNotFound) {
  101. b, _ := json.Marshal(defaultCfg)
  102. _ = s.settingRepo.Set(ctx, SettingKeyOpsRuntimeLogConfig, string(b))
  103. return defaultCfg, nil
  104. }
  105. return nil, err
  106. }
  107. cfg := &OpsRuntimeLogConfig{}
  108. if err := json.Unmarshal([]byte(raw), cfg); err != nil {
  109. return defaultCfg, nil
  110. }
  111. normalizeOpsRuntimeLogConfig(cfg, defaultCfg)
  112. return cfg, nil
  113. }
  114. func (s *OpsService) UpdateRuntimeLogConfig(ctx context.Context, req *OpsRuntimeLogConfig, operatorID int64) (*OpsRuntimeLogConfig, error) {
  115. if s == nil || s.settingRepo == nil {
  116. return nil, errors.New("setting repository not initialized")
  117. }
  118. if req == nil {
  119. return nil, errors.New("invalid config")
  120. }
  121. if ctx == nil {
  122. ctx = context.Background()
  123. }
  124. if operatorID <= 0 {
  125. return nil, errors.New("invalid operator id")
  126. }
  127. oldCfg, err := s.GetRuntimeLogConfig(ctx)
  128. if err != nil {
  129. return nil, err
  130. }
  131. next := *req
  132. normalizeOpsRuntimeLogConfig(&next, defaultOpsRuntimeLogConfig(s.cfg))
  133. if err := validateOpsRuntimeLogConfig(&next); err != nil {
  134. s.auditRuntimeLogConfigFailure(operatorID, oldCfg, &next, "validation_failed: "+err.Error())
  135. return nil, err
  136. }
  137. if err := s.applyRuntimeLogConfig(&next); err != nil {
  138. s.auditRuntimeLogConfigFailure(operatorID, oldCfg, &next, "apply_failed: "+err.Error())
  139. return nil, err
  140. }
  141. next.Source = "runtime_setting"
  142. next.UpdatedAt = time.Now().UTC().Format(time.RFC3339Nano)
  143. next.UpdatedByUserID = operatorID
  144. encoded, err := json.Marshal(&next)
  145. if err != nil {
  146. return nil, err
  147. }
  148. if err := s.settingRepo.Set(ctx, SettingKeyOpsRuntimeLogConfig, string(encoded)); err != nil {
  149. // 存储失败时回滚到旧配置,避免内存状态与持久化状态不一致。
  150. _ = s.applyRuntimeLogConfig(oldCfg)
  151. s.auditRuntimeLogConfigFailure(operatorID, oldCfg, &next, "persist_failed: "+err.Error())
  152. return nil, err
  153. }
  154. s.auditRuntimeLogConfigChange(operatorID, oldCfg, &next, "updated")
  155. s.reloadOpsCleanup(ctx)
  156. return &next, nil
  157. }
  158. func (s *OpsService) ResetRuntimeLogConfig(ctx context.Context, operatorID int64) (*OpsRuntimeLogConfig, error) {
  159. if s == nil || s.settingRepo == nil {
  160. return nil, errors.New("setting repository not initialized")
  161. }
  162. if ctx == nil {
  163. ctx = context.Background()
  164. }
  165. if operatorID <= 0 {
  166. return nil, errors.New("invalid operator id")
  167. }
  168. oldCfg, err := s.GetRuntimeLogConfig(ctx)
  169. if err != nil {
  170. return nil, err
  171. }
  172. resetCfg := defaultOpsRuntimeLogConfig(s.cfg)
  173. normalizeOpsRuntimeLogConfig(resetCfg, defaultOpsRuntimeLogConfig(s.cfg))
  174. if err := validateOpsRuntimeLogConfig(resetCfg); err != nil {
  175. s.auditRuntimeLogConfigFailure(operatorID, oldCfg, resetCfg, "reset_validation_failed: "+err.Error())
  176. return nil, err
  177. }
  178. if err := s.applyRuntimeLogConfig(resetCfg); err != nil {
  179. s.auditRuntimeLogConfigFailure(operatorID, oldCfg, resetCfg, "reset_apply_failed: "+err.Error())
  180. return nil, err
  181. }
  182. // 清理 runtime 覆盖配置,回退到 env/yaml baseline。
  183. if err := s.settingRepo.Delete(ctx, SettingKeyOpsRuntimeLogConfig); err != nil && !errors.Is(err, ErrSettingNotFound) {
  184. _ = s.applyRuntimeLogConfig(oldCfg)
  185. s.auditRuntimeLogConfigFailure(operatorID, oldCfg, resetCfg, "reset_persist_failed: "+err.Error())
  186. return nil, err
  187. }
  188. now := time.Now().UTC().Format(time.RFC3339Nano)
  189. resetCfg.Source = "baseline"
  190. resetCfg.UpdatedAt = now
  191. resetCfg.UpdatedByUserID = operatorID
  192. s.auditRuntimeLogConfigChange(operatorID, oldCfg, resetCfg, "reset")
  193. s.reloadOpsCleanup(ctx)
  194. return resetCfg, nil
  195. }
  196. func (s *OpsService) applyRuntimeLogConfig(cfg *OpsRuntimeLogConfig) error {
  197. if err := applyOpsRuntimeLogConfig(cfg); err != nil {
  198. return err
  199. }
  200. if s != nil && s.systemLogSink != nil {
  201. s.systemLogSink.SetPersistAccessLogs(cfg.PersistAccessLogs)
  202. }
  203. return nil
  204. }
  205. func (s *OpsService) reloadOpsCleanup(ctx context.Context) {
  206. if s == nil || s.cleanupReloader == nil {
  207. return
  208. }
  209. if err := s.cleanupReloader.Reload(ctx); err != nil {
  210. logger.LegacyPrintf("service.ops_log_runtime", "[OpsLogRuntime] cleanup reload failed: %v", err)
  211. }
  212. }
  213. func applyOpsRuntimeLogConfig(cfg *OpsRuntimeLogConfig) error {
  214. if cfg == nil {
  215. return fmt.Errorf("nil runtime log config")
  216. }
  217. if err := logger.Reconfigure(func(opts *logger.InitOptions) error {
  218. opts.Level = strings.ToLower(strings.TrimSpace(cfg.Level))
  219. opts.Caller = cfg.Caller
  220. opts.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.StacktraceLevel))
  221. opts.Sampling.Enabled = cfg.EnableSampling
  222. opts.Sampling.Initial = cfg.SamplingInitial
  223. opts.Sampling.Thereafter = cfg.SamplingNext
  224. return nil
  225. }); err != nil {
  226. return err
  227. }
  228. return nil
  229. }
  230. func (s *OpsService) applyRuntimeLogConfigOnStartup(ctx context.Context) {
  231. if s == nil {
  232. return
  233. }
  234. cfg, err := s.GetRuntimeLogConfig(ctx)
  235. if err != nil {
  236. return
  237. }
  238. _ = s.applyRuntimeLogConfig(cfg)
  239. }
  240. func (s *OpsService) auditRuntimeLogConfigChange(operatorID int64, oldCfg *OpsRuntimeLogConfig, newCfg *OpsRuntimeLogConfig, action string) {
  241. oldRaw, _ := json.Marshal(oldCfg)
  242. newRaw, _ := json.Marshal(newCfg)
  243. logger.With(
  244. zap.String("component", "audit.log_config_change"),
  245. zap.String("action", strings.TrimSpace(action)),
  246. zap.Int64("operator_id", operatorID),
  247. zap.String("old", string(oldRaw)),
  248. zap.String("new", string(newRaw)),
  249. ).Info("runtime log config changed")
  250. }
  251. func (s *OpsService) auditRuntimeLogConfigFailure(operatorID int64, oldCfg *OpsRuntimeLogConfig, newCfg *OpsRuntimeLogConfig, reason string) {
  252. oldRaw, _ := json.Marshal(oldCfg)
  253. newRaw, _ := json.Marshal(newCfg)
  254. logger.With(
  255. zap.String("component", "audit.log_config_change"),
  256. zap.String("action", "failed"),
  257. zap.Int64("operator_id", operatorID),
  258. zap.String("reason", strings.TrimSpace(reason)),
  259. zap.String("old", string(oldRaw)),
  260. zap.String("new", string(newRaw)),
  261. ).Warn("runtime log config change failed")
  262. }