idempotency_observability.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. package service
  2. import (
  3. "sort"
  4. "strconv"
  5. "strings"
  6. "sync/atomic"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  9. )
  10. // IdempotencyMetricsSnapshot 提供幂等核心指标快照(进程内累计)。
  11. type IdempotencyMetricsSnapshot struct {
  12. ClaimTotal uint64 `json:"claim_total"`
  13. ReplayTotal uint64 `json:"replay_total"`
  14. ConflictTotal uint64 `json:"conflict_total"`
  15. RetryBackoffTotal uint64 `json:"retry_backoff_total"`
  16. ProcessingDurationCount uint64 `json:"processing_duration_count"`
  17. ProcessingDurationTotalMs float64 `json:"processing_duration_total_ms"`
  18. StoreUnavailableTotal uint64 `json:"store_unavailable_total"`
  19. }
  20. type idempotencyMetrics struct {
  21. claimTotal atomic.Uint64
  22. replayTotal atomic.Uint64
  23. conflictTotal atomic.Uint64
  24. retryBackoffTotal atomic.Uint64
  25. processingDurationCount atomic.Uint64
  26. processingDurationMicros atomic.Uint64
  27. storeUnavailableTotal atomic.Uint64
  28. }
  29. var defaultIdempotencyMetrics idempotencyMetrics
  30. // GetIdempotencyMetricsSnapshot 返回当前幂等指标快照。
  31. func GetIdempotencyMetricsSnapshot() IdempotencyMetricsSnapshot {
  32. totalMicros := defaultIdempotencyMetrics.processingDurationMicros.Load()
  33. return IdempotencyMetricsSnapshot{
  34. ClaimTotal: defaultIdempotencyMetrics.claimTotal.Load(),
  35. ReplayTotal: defaultIdempotencyMetrics.replayTotal.Load(),
  36. ConflictTotal: defaultIdempotencyMetrics.conflictTotal.Load(),
  37. RetryBackoffTotal: defaultIdempotencyMetrics.retryBackoffTotal.Load(),
  38. ProcessingDurationCount: defaultIdempotencyMetrics.processingDurationCount.Load(),
  39. ProcessingDurationTotalMs: float64(totalMicros) / 1000.0,
  40. StoreUnavailableTotal: defaultIdempotencyMetrics.storeUnavailableTotal.Load(),
  41. }
  42. }
  43. func recordIdempotencyClaim(endpoint, scope string, attrs map[string]string) {
  44. defaultIdempotencyMetrics.claimTotal.Add(1)
  45. logIdempotencyMetric("idempotency_claim_total", endpoint, scope, "1", attrs)
  46. }
  47. func recordIdempotencyReplay(endpoint, scope string, attrs map[string]string) {
  48. defaultIdempotencyMetrics.replayTotal.Add(1)
  49. logIdempotencyMetric("idempotency_replay_total", endpoint, scope, "1", attrs)
  50. }
  51. func recordIdempotencyConflict(endpoint, scope string, attrs map[string]string) {
  52. defaultIdempotencyMetrics.conflictTotal.Add(1)
  53. logIdempotencyMetric("idempotency_conflict_total", endpoint, scope, "1", attrs)
  54. }
  55. func recordIdempotencyRetryBackoff(endpoint, scope string, attrs map[string]string) {
  56. defaultIdempotencyMetrics.retryBackoffTotal.Add(1)
  57. logIdempotencyMetric("idempotency_retry_backoff_total", endpoint, scope, "1", attrs)
  58. }
  59. func recordIdempotencyProcessingDuration(endpoint, scope string, duration time.Duration, attrs map[string]string) {
  60. if duration < 0 {
  61. duration = 0
  62. }
  63. defaultIdempotencyMetrics.processingDurationCount.Add(1)
  64. defaultIdempotencyMetrics.processingDurationMicros.Add(uint64(duration.Microseconds()))
  65. logIdempotencyMetric("idempotency_processing_duration_ms", endpoint, scope, strconv.FormatFloat(duration.Seconds()*1000, 'f', 3, 64), attrs)
  66. }
  67. // RecordIdempotencyStoreUnavailable 记录幂等存储不可用事件(用于降级路径观测)。
  68. func RecordIdempotencyStoreUnavailable(endpoint, scope, strategy string) {
  69. defaultIdempotencyMetrics.storeUnavailableTotal.Add(1)
  70. attrs := map[string]string{}
  71. if strategy != "" {
  72. attrs["strategy"] = strategy
  73. }
  74. logIdempotencyMetric("idempotency_store_unavailable_total", endpoint, scope, "1", attrs)
  75. }
  76. func logIdempotencyAudit(endpoint, scope, keyHash, stateTransition string, replayed bool, attrs map[string]string) {
  77. var b strings.Builder
  78. builderWriteString(&b, "[IdempotencyAudit]")
  79. builderWriteString(&b, " endpoint=")
  80. builderWriteString(&b, safeAuditField(endpoint))
  81. builderWriteString(&b, " scope=")
  82. builderWriteString(&b, safeAuditField(scope))
  83. builderWriteString(&b, " key_hash=")
  84. builderWriteString(&b, safeAuditField(keyHash))
  85. builderWriteString(&b, " state_transition=")
  86. builderWriteString(&b, safeAuditField(stateTransition))
  87. builderWriteString(&b, " replayed=")
  88. builderWriteString(&b, strconv.FormatBool(replayed))
  89. if len(attrs) > 0 {
  90. appendSortedAttrs(&b, attrs)
  91. }
  92. logger.LegacyPrintf("service.idempotency", "%s", b.String())
  93. }
  94. func logIdempotencyMetric(name, endpoint, scope, value string, attrs map[string]string) {
  95. var b strings.Builder
  96. builderWriteString(&b, "[IdempotencyMetric]")
  97. builderWriteString(&b, " name=")
  98. builderWriteString(&b, safeAuditField(name))
  99. builderWriteString(&b, " endpoint=")
  100. builderWriteString(&b, safeAuditField(endpoint))
  101. builderWriteString(&b, " scope=")
  102. builderWriteString(&b, safeAuditField(scope))
  103. builderWriteString(&b, " value=")
  104. builderWriteString(&b, safeAuditField(value))
  105. if len(attrs) > 0 {
  106. appendSortedAttrs(&b, attrs)
  107. }
  108. logger.LegacyPrintf("service.idempotency", "%s", b.String())
  109. }
  110. func appendSortedAttrs(builder *strings.Builder, attrs map[string]string) {
  111. if len(attrs) == 0 {
  112. return
  113. }
  114. keys := make([]string, 0, len(attrs))
  115. for k := range attrs {
  116. keys = append(keys, k)
  117. }
  118. sort.Strings(keys)
  119. for _, k := range keys {
  120. builderWriteByte(builder, ' ')
  121. builderWriteString(builder, k)
  122. builderWriteByte(builder, '=')
  123. builderWriteString(builder, safeAuditField(attrs[k]))
  124. }
  125. }
  126. func safeAuditField(v string) string {
  127. value := strings.TrimSpace(v)
  128. if value == "" {
  129. return "-"
  130. }
  131. // 日志按 key=value 输出,替换空白避免解析歧义。
  132. value = strings.ReplaceAll(value, "\n", "_")
  133. value = strings.ReplaceAll(value, "\r", "_")
  134. value = strings.ReplaceAll(value, "\t", "_")
  135. value = strings.ReplaceAll(value, " ", "_")
  136. return value
  137. }
  138. func resetIdempotencyMetricsForTest() {
  139. defaultIdempotencyMetrics.claimTotal.Store(0)
  140. defaultIdempotencyMetrics.replayTotal.Store(0)
  141. defaultIdempotencyMetrics.conflictTotal.Store(0)
  142. defaultIdempotencyMetrics.retryBackoffTotal.Store(0)
  143. defaultIdempotencyMetrics.processingDurationCount.Store(0)
  144. defaultIdempotencyMetrics.processingDurationMicros.Store(0)
  145. defaultIdempotencyMetrics.storeUnavailableTotal.Store(0)
  146. }
  147. func builderWriteString(builder *strings.Builder, value string) {
  148. _, _ = builder.WriteString(value)
  149. }
  150. func builderWriteByte(builder *strings.Builder, value byte) {
  151. _ = builder.WriteByte(value)
  152. }