ops_system_log_sink.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "sync/atomic"
  11. "time"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  13. "github.com/Wei-Shaw/sub2api/internal/util/logredact"
  14. )
  15. type OpsSystemLogSinkHealth struct {
  16. QueueDepth int64 `json:"queue_depth"`
  17. QueueCapacity int64 `json:"queue_capacity"`
  18. DroppedCount uint64 `json:"dropped_count"`
  19. WriteFailed uint64 `json:"write_failed_count"`
  20. WrittenCount uint64 `json:"written_count"`
  21. AvgWriteDelayMs uint64 `json:"avg_write_delay_ms"`
  22. LastError string `json:"last_error"`
  23. }
  24. type OpsSystemLogSink struct {
  25. opsRepo OpsRepository
  26. host string
  27. queue chan *logger.LogEvent
  28. batchSize int
  29. flushInterval time.Duration
  30. // 连续写入失败后的退避参数。构造后只读,测试可在 Start 前覆盖。
  31. flushBackoff time.Duration
  32. flushBackoffMax time.Duration
  33. ctx context.Context
  34. cancel context.CancelFunc
  35. wg sync.WaitGroup
  36. droppedCount uint64
  37. writeFailed uint64
  38. writtenCount uint64
  39. totalDelayNs uint64
  40. persistAccessLogs atomic.Bool
  41. lastError atomic.Value
  42. }
  43. const maxSystemLogHostLength = 255
  44. const (
  45. // 首次写入失败后暂停落库的时长,之后逐次翻倍到上限。
  46. defaultOpsSystemLogFlushBackoff = 2 * time.Second
  47. // 退避上限。日志是尽力而为的观测数据,不值得为它无限期占用连接池。
  48. defaultOpsSystemLogFlushBackoffMax = 60 * time.Second
  49. )
  50. func NewOpsSystemLogSink(opsRepo OpsRepository) *OpsSystemLogSink {
  51. ctx, cancel := context.WithCancel(context.Background())
  52. rawHost, err := os.Hostname()
  53. s := &OpsSystemLogSink{
  54. opsRepo: opsRepo,
  55. host: normalizeSystemLogHost(rawHost, err),
  56. queue: make(chan *logger.LogEvent, 5000),
  57. batchSize: 200,
  58. flushInterval: time.Second,
  59. flushBackoff: defaultOpsSystemLogFlushBackoff,
  60. flushBackoffMax: defaultOpsSystemLogFlushBackoffMax,
  61. ctx: ctx,
  62. cancel: cancel,
  63. }
  64. s.lastError.Store("")
  65. return s
  66. }
  67. // flushBackoffFor 返回第 failures 次连续失败后的退避时长(指数退避,封顶)。
  68. func (s *OpsSystemLogSink) flushBackoffFor(failures int) time.Duration {
  69. base := s.flushBackoff
  70. if base <= 0 {
  71. base = defaultOpsSystemLogFlushBackoff
  72. }
  73. maxBackoff := s.flushBackoffMax
  74. if maxBackoff <= 0 {
  75. maxBackoff = defaultOpsSystemLogFlushBackoffMax
  76. }
  77. if maxBackoff < base {
  78. maxBackoff = base
  79. }
  80. backoff := base
  81. for i := 1; i < failures && backoff < maxBackoff; i++ {
  82. backoff *= 2
  83. }
  84. if backoff > maxBackoff {
  85. backoff = maxBackoff
  86. }
  87. return backoff
  88. }
  89. func normalizeSystemLogHost(host string, err error) string {
  90. host = strings.TrimSpace(host)
  91. if err != nil || host == "" {
  92. return "unknown"
  93. }
  94. runes := []rune(host)
  95. if len(runes) > maxSystemLogHostLength {
  96. return string(runes[:maxSystemLogHostLength])
  97. }
  98. return host
  99. }
  100. func (s *OpsSystemLogSink) Start() {
  101. if s == nil || s.opsRepo == nil {
  102. return
  103. }
  104. s.wg.Add(1)
  105. go s.run()
  106. }
  107. func (s *OpsSystemLogSink) Stop() {
  108. if s == nil {
  109. return
  110. }
  111. s.cancel()
  112. s.wg.Wait()
  113. }
  114. func (s *OpsSystemLogSink) WriteLogEvent(event *logger.LogEvent) {
  115. if s == nil || event == nil || !s.shouldIndex(event) {
  116. return
  117. }
  118. if s.ctx != nil {
  119. select {
  120. case <-s.ctx.Done():
  121. return
  122. default:
  123. }
  124. }
  125. select {
  126. case s.queue <- event:
  127. default:
  128. atomic.AddUint64(&s.droppedCount, 1)
  129. }
  130. }
  131. // SetPersistAccessLogs controls whether high-volume request access logs are
  132. // copied into PostgreSQL. Warning/error and audit events are always retained.
  133. func (s *OpsSystemLogSink) SetPersistAccessLogs(enabled bool) {
  134. if s == nil {
  135. return
  136. }
  137. s.persistAccessLogs.Store(enabled)
  138. }
  139. func (s *OpsSystemLogSink) shouldIndex(event *logger.LogEvent) bool {
  140. if event != nil && event.Fields != nil {
  141. if skip, _ := event.Fields[logger.OpsSystemLogSkipField].(bool); skip {
  142. return false
  143. }
  144. }
  145. level := strings.ToLower(strings.TrimSpace(event.Level))
  146. switch level {
  147. case "warn", "warning", "error", "fatal", "panic", "dpanic":
  148. return true
  149. }
  150. component := strings.ToLower(strings.TrimSpace(event.Component))
  151. // zap 的 LoggerName 往往为空或不等于业务组件名;业务组件名通常以字段 component 透传。
  152. if event.Fields != nil {
  153. if fc := strings.ToLower(strings.TrimSpace(asString(event.Fields["component"]))); fc != "" {
  154. component = fc
  155. }
  156. }
  157. if strings.Contains(component, "http.access") {
  158. return s.persistAccessLogs.Load()
  159. }
  160. if strings.Contains(component, "audit") {
  161. return true
  162. }
  163. return false
  164. }
  165. func (s *OpsSystemLogSink) run() {
  166. defer s.wg.Done()
  167. ticker := time.NewTicker(s.flushInterval)
  168. defer ticker.Stop()
  169. batch := make([]*logger.LogEvent, 0, s.batchSize)
  170. // 仅在本 goroutine 内读写,无需加锁。
  171. failures := 0
  172. var suppressedUntil time.Time
  173. flush := func(baseCtx context.Context) {
  174. if len(batch) == 0 {
  175. return
  176. }
  177. now := time.Now()
  178. if now.Before(suppressedUntil) {
  179. // 退避窗口内直接丢弃本批:日志是尽力而为的观测数据,继续攒批只会把
  180. // 压力转移到内存,而每次重试都会再占用并取消一条池内连接。
  181. atomic.AddUint64(&s.droppedCount, uint64(len(batch)))
  182. batch = batch[:0]
  183. return
  184. }
  185. started := time.Now()
  186. inserted, err := s.flushBatch(baseCtx, batch)
  187. delay := time.Since(started)
  188. if err != nil {
  189. failures++
  190. backoff := s.flushBackoffFor(failures)
  191. suppressedUntil = time.Now().Add(backoff)
  192. atomic.AddUint64(&s.writeFailed, uint64(len(batch)))
  193. s.lastError.Store(err.Error())
  194. // 每个退避窗口至多一条,避免数据库故障期间刷屏。
  195. _, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"ops system log sink flush failed\" err=%v batch=%d failures=%d backoff=%s\n",
  196. time.Now().Format(time.RFC3339Nano), err, len(batch), failures, backoff,
  197. )
  198. } else {
  199. failures = 0
  200. suppressedUntil = time.Time{}
  201. atomic.AddUint64(&s.writtenCount, uint64(inserted))
  202. atomic.AddUint64(&s.totalDelayNs, uint64(delay.Nanoseconds()))
  203. s.lastError.Store("")
  204. }
  205. batch = batch[:0]
  206. }
  207. drainAndFlush := func() {
  208. for {
  209. select {
  210. case item := <-s.queue:
  211. if item == nil {
  212. continue
  213. }
  214. batch = append(batch, item)
  215. if len(batch) >= s.batchSize {
  216. flush(context.Background())
  217. }
  218. default:
  219. flush(context.Background())
  220. return
  221. }
  222. }
  223. }
  224. for {
  225. select {
  226. case <-s.ctx.Done():
  227. drainAndFlush()
  228. return
  229. case item := <-s.queue:
  230. if item == nil {
  231. continue
  232. }
  233. batch = append(batch, item)
  234. if len(batch) >= s.batchSize {
  235. flush(s.ctx)
  236. }
  237. case <-ticker.C:
  238. flush(s.ctx)
  239. }
  240. }
  241. }
  242. func (s *OpsSystemLogSink) flushBatch(baseCtx context.Context, batch []*logger.LogEvent) (int, error) {
  243. inputs := make([]*OpsInsertSystemLogInput, 0, len(batch))
  244. for _, event := range batch {
  245. if event == nil {
  246. continue
  247. }
  248. createdAt := event.Time.UTC()
  249. if createdAt.IsZero() {
  250. createdAt = time.Now().UTC()
  251. }
  252. fields := copyMap(event.Fields)
  253. requestID := asString(fields["request_id"])
  254. clientRequestID := asString(fields["client_request_id"])
  255. platform := asString(fields["platform"])
  256. model := asString(fields["model"])
  257. component := strings.TrimSpace(event.Component)
  258. if fieldComponent := asString(fields["component"]); fieldComponent != "" {
  259. component = fieldComponent
  260. }
  261. if component == "" {
  262. component = "app"
  263. }
  264. userID := asInt64Ptr(fields["user_id"])
  265. apiKeyID := asInt64Ptr(fields["api_key_id"])
  266. accountID := asInt64Ptr(fields["account_id"])
  267. // 统一脱敏后写入索引。
  268. message := logredact.RedactText(strings.TrimSpace(event.Message))
  269. redactedExtra := logredact.RedactMap(fields)
  270. extraJSONBytes, _ := json.Marshal(redactedExtra)
  271. extraJSON := string(extraJSONBytes)
  272. if strings.TrimSpace(extraJSON) == "" {
  273. extraJSON = "{}"
  274. }
  275. inputs = append(inputs, &OpsInsertSystemLogInput{
  276. CreatedAt: createdAt,
  277. Host: s.host,
  278. Level: strings.ToLower(strings.TrimSpace(event.Level)),
  279. Component: component,
  280. Message: message,
  281. RequestID: requestID,
  282. ClientRequestID: clientRequestID,
  283. UserID: userID,
  284. APIKeyID: apiKeyID,
  285. AccountID: accountID,
  286. Platform: platform,
  287. Model: model,
  288. ExtraJSON: extraJSON,
  289. })
  290. }
  291. if len(inputs) == 0 {
  292. return 0, nil
  293. }
  294. if baseCtx == nil || baseCtx.Err() != nil {
  295. baseCtx = context.Background()
  296. }
  297. ctx, cancel := context.WithTimeout(baseCtx, 5*time.Second)
  298. defer cancel()
  299. inserted, err := s.opsRepo.BatchInsertSystemLogs(ctx, inputs)
  300. if err != nil {
  301. return 0, err
  302. }
  303. return int(inserted), nil
  304. }
  305. func (s *OpsSystemLogSink) Health() OpsSystemLogSinkHealth {
  306. if s == nil {
  307. return OpsSystemLogSinkHealth{}
  308. }
  309. written := atomic.LoadUint64(&s.writtenCount)
  310. totalDelay := atomic.LoadUint64(&s.totalDelayNs)
  311. var avgDelay uint64
  312. if written > 0 {
  313. avgDelay = (totalDelay / written) / uint64(time.Millisecond)
  314. }
  315. lastErr, _ := s.lastError.Load().(string)
  316. return OpsSystemLogSinkHealth{
  317. QueueDepth: int64(len(s.queue)),
  318. QueueCapacity: int64(cap(s.queue)),
  319. DroppedCount: atomic.LoadUint64(&s.droppedCount),
  320. WriteFailed: atomic.LoadUint64(&s.writeFailed),
  321. WrittenCount: written,
  322. AvgWriteDelayMs: avgDelay,
  323. LastError: strings.TrimSpace(lastErr),
  324. }
  325. }
  326. func copyMap(in map[string]any) map[string]any {
  327. if len(in) == 0 {
  328. return map[string]any{}
  329. }
  330. out := make(map[string]any, len(in))
  331. for k, v := range in {
  332. out[k] = v
  333. }
  334. return out
  335. }
  336. func asString(v any) string {
  337. switch t := v.(type) {
  338. case string:
  339. return strings.TrimSpace(t)
  340. case fmt.Stringer:
  341. return strings.TrimSpace(t.String())
  342. default:
  343. return ""
  344. }
  345. }
  346. func asInt64Ptr(v any) *int64 {
  347. switch t := v.(type) {
  348. case int:
  349. n := int64(t)
  350. if n <= 0 {
  351. return nil
  352. }
  353. return &n
  354. case int64:
  355. n := t
  356. if n <= 0 {
  357. return nil
  358. }
  359. return &n
  360. case float64:
  361. n := int64(t)
  362. if n <= 0 {
  363. return nil
  364. }
  365. return &n
  366. case json.Number:
  367. if n, err := t.Int64(); err == nil {
  368. if n <= 0 {
  369. return nil
  370. }
  371. return &n
  372. }
  373. case string:
  374. raw := strings.TrimSpace(t)
  375. if raw == "" {
  376. return nil
  377. }
  378. if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
  379. if n <= 0 {
  380. return nil
  381. }
  382. return &n
  383. }
  384. }
  385. return nil
  386. }