Files
cmr-mini/backend/internal/service/dev_service.go

149 lines
4.0 KiB
Go

package service
import (
"context"
"net/http"
"sort"
"sync"
"time"
"cmr-backend/internal/apperr"
"cmr-backend/internal/store/postgres"
)
type DevService struct {
appEnv string
store *postgres.Store
mu sync.Mutex
logSeq int64
logs []ClientDebugLogEntry
}
type ClientDebugLogEntry struct {
ID int64 `json:"id"`
Source string `json:"source"`
Level string `json:"level"`
Category string `json:"category,omitempty"`
Message string `json:"message"`
EventID string `json:"eventId,omitempty"`
ReleaseID string `json:"releaseId,omitempty"`
SessionID string `json:"sessionId,omitempty"`
ManifestURL string `json:"manifestUrl,omitempty"`
Route string `json:"route,omitempty"`
OccurredAt time.Time `json:"occurredAt"`
ReceivedAt time.Time `json:"receivedAt"`
Details map[string]any `json:"details,omitempty"`
}
type CreateClientDebugLogInput struct {
Source string `json:"source"`
Level string `json:"level"`
Category string `json:"category"`
Message string `json:"message"`
EventID string `json:"eventId"`
ReleaseID string `json:"releaseId"`
SessionID string `json:"sessionId"`
ManifestURL string `json:"manifestUrl"`
Route string `json:"route"`
OccurredAt string `json:"occurredAt"`
Details map[string]any `json:"details"`
}
func NewDevService(appEnv string, store *postgres.Store) *DevService {
return &DevService{
appEnv: appEnv,
store: store,
}
}
func (s *DevService) Enabled() bool {
return s.appEnv != "production"
}
func (s *DevService) BootstrapDemo(ctx context.Context) (*postgres.DemoBootstrapSummary, error) {
if !s.Enabled() {
return nil, apperr.New(http.StatusNotFound, "not_found", "dev bootstrap is disabled")
}
return s.store.EnsureDemoData(ctx)
}
func (s *DevService) AddClientDebugLog(_ context.Context, input CreateClientDebugLogInput) (*ClientDebugLogEntry, error) {
if !s.Enabled() {
return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
}
if input.Message == "" {
return nil, apperr.New(http.StatusBadRequest, "invalid_request", "message is required")
}
if input.Source == "" {
input.Source = "unknown"
}
if input.Level == "" {
input.Level = "info"
}
occurredAt := time.Now().UTC()
if input.OccurredAt != "" {
parsed, err := time.Parse(time.RFC3339, input.OccurredAt)
if err != nil {
return nil, apperr.New(http.StatusBadRequest, "invalid_request", "occurredAt must be RFC3339")
}
occurredAt = parsed.UTC()
}
entry := ClientDebugLogEntry{
Source: input.Source,
Level: input.Level,
Category: input.Category,
Message: input.Message,
EventID: input.EventID,
ReleaseID: input.ReleaseID,
SessionID: input.SessionID,
ManifestURL: input.ManifestURL,
Route: input.Route,
OccurredAt: occurredAt,
ReceivedAt: time.Now().UTC(),
Details: input.Details,
}
s.mu.Lock()
defer s.mu.Unlock()
s.logSeq++
entry.ID = s.logSeq
s.logs = append(s.logs, entry)
if len(s.logs) > 200 {
s.logs = append([]ClientDebugLogEntry(nil), s.logs[len(s.logs)-200:]...)
}
copyEntry := entry
return &copyEntry, nil
}
func (s *DevService) ListClientDebugLogs(_ context.Context, limit int) ([]ClientDebugLogEntry, error) {
if !s.Enabled() {
return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
}
if limit <= 0 || limit > 200 {
limit = 50
}
s.mu.Lock()
defer s.mu.Unlock()
items := append([]ClientDebugLogEntry(nil), s.logs...)
sort.Slice(items, func(i, j int) bool {
return items[i].ID > items[j].ID
})
if len(items) > limit {
items = items[:limit]
}
return items, nil
}
func (s *DevService) ClearClientDebugLogs(_ context.Context) error {
if !s.Enabled() {
return apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
}
s.mu.Lock()
defer s.mu.Unlock()
s.logs = nil
return nil
}