dev_service.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package service
  2. import (
  3. "context"
  4. "net/http"
  5. "sort"
  6. "sync"
  7. "time"
  8. "cmr-backend/internal/apperr"
  9. "cmr-backend/internal/store/postgres"
  10. )
  11. type DevService struct {
  12. appEnv string
  13. store *postgres.Store
  14. mu sync.Mutex
  15. logSeq int64
  16. logs []ClientDebugLogEntry
  17. }
  18. type ClientDebugLogEntry struct {
  19. ID int64 `json:"id"`
  20. Source string `json:"source"`
  21. Level string `json:"level"`
  22. Category string `json:"category,omitempty"`
  23. Message string `json:"message"`
  24. EventID string `json:"eventId,omitempty"`
  25. ReleaseID string `json:"releaseId,omitempty"`
  26. SessionID string `json:"sessionId,omitempty"`
  27. ManifestURL string `json:"manifestUrl,omitempty"`
  28. Route string `json:"route,omitempty"`
  29. OccurredAt time.Time `json:"occurredAt"`
  30. ReceivedAt time.Time `json:"receivedAt"`
  31. Details map[string]any `json:"details,omitempty"`
  32. }
  33. type CreateClientDebugLogInput struct {
  34. Source string `json:"source"`
  35. Level string `json:"level"`
  36. Category string `json:"category"`
  37. Message string `json:"message"`
  38. EventID string `json:"eventId"`
  39. ReleaseID string `json:"releaseId"`
  40. SessionID string `json:"sessionId"`
  41. ManifestURL string `json:"manifestUrl"`
  42. Route string `json:"route"`
  43. OccurredAt string `json:"occurredAt"`
  44. Details map[string]any `json:"details"`
  45. }
  46. func NewDevService(appEnv string, store *postgres.Store) *DevService {
  47. return &DevService{
  48. appEnv: appEnv,
  49. store: store,
  50. }
  51. }
  52. func (s *DevService) Enabled() bool {
  53. return s.appEnv != "production"
  54. }
  55. func (s *DevService) BootstrapDemo(ctx context.Context) (*postgres.DemoBootstrapSummary, error) {
  56. if !s.Enabled() {
  57. return nil, apperr.New(http.StatusNotFound, "not_found", "dev bootstrap is disabled")
  58. }
  59. return s.store.EnsureDemoData(ctx)
  60. }
  61. func (s *DevService) AddClientDebugLog(_ context.Context, input CreateClientDebugLogInput) (*ClientDebugLogEntry, error) {
  62. if !s.Enabled() {
  63. return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
  64. }
  65. if input.Message == "" {
  66. return nil, apperr.New(http.StatusBadRequest, "invalid_request", "message is required")
  67. }
  68. if input.Source == "" {
  69. input.Source = "unknown"
  70. }
  71. if input.Level == "" {
  72. input.Level = "info"
  73. }
  74. occurredAt := time.Now().UTC()
  75. if input.OccurredAt != "" {
  76. parsed, err := time.Parse(time.RFC3339, input.OccurredAt)
  77. if err != nil {
  78. return nil, apperr.New(http.StatusBadRequest, "invalid_request", "occurredAt must be RFC3339")
  79. }
  80. occurredAt = parsed.UTC()
  81. }
  82. entry := ClientDebugLogEntry{
  83. Source: input.Source,
  84. Level: input.Level,
  85. Category: input.Category,
  86. Message: input.Message,
  87. EventID: input.EventID,
  88. ReleaseID: input.ReleaseID,
  89. SessionID: input.SessionID,
  90. ManifestURL: input.ManifestURL,
  91. Route: input.Route,
  92. OccurredAt: occurredAt,
  93. ReceivedAt: time.Now().UTC(),
  94. Details: input.Details,
  95. }
  96. s.mu.Lock()
  97. defer s.mu.Unlock()
  98. s.logSeq++
  99. entry.ID = s.logSeq
  100. s.logs = append(s.logs, entry)
  101. if len(s.logs) > 200 {
  102. s.logs = append([]ClientDebugLogEntry(nil), s.logs[len(s.logs)-200:]...)
  103. }
  104. copyEntry := entry
  105. return &copyEntry, nil
  106. }
  107. func (s *DevService) ListClientDebugLogs(_ context.Context, limit int) ([]ClientDebugLogEntry, error) {
  108. if !s.Enabled() {
  109. return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
  110. }
  111. if limit <= 0 || limit > 200 {
  112. limit = 50
  113. }
  114. s.mu.Lock()
  115. defer s.mu.Unlock()
  116. items := append([]ClientDebugLogEntry(nil), s.logs...)
  117. sort.Slice(items, func(i, j int) bool {
  118. return items[i].ID > items[j].ID
  119. })
  120. if len(items) > limit {
  121. items = items[:limit]
  122. }
  123. return items, nil
  124. }
  125. func (s *DevService) ClearClientDebugLogs(_ context.Context) error {
  126. if !s.Enabled() {
  127. return apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
  128. }
  129. s.mu.Lock()
  130. defer s.mu.Unlock()
  131. s.logs = nil
  132. return nil
  133. }