Files
kandev/apps/backend/internal/startup/progress.go
T

138 lines
3.7 KiB
Go

// Package startup reports process initialization without exposing private diagnostics.
package startup
import (
"context"
cryptorand "crypto/rand"
"encoding/binary"
"sync"
"time"
"github.com/kandev/kandev/internal/common/logger"
"go.uber.org/zap"
)
type Phase string
const (
OpeningDatabase Phase = "opening_database"
BackingUpDatabase Phase = "backing_up_database"
ApplyingMigrations Phase = "applying_migrations"
InitializingServices Phase = "initializing_services"
RecoveringSessions Phase = "recovering_sessions"
Ready Phase = "ready"
)
// Label accepts only protocol-owned phase names, never arbitrary server text.
func (p Phase) Label() string {
switch p {
case OpeningDatabase:
return "Opening database"
case BackingUpDatabase:
return "Backing up database"
case ApplyingMigrations:
return "Applying migrations"
case InitializingServices:
return "Initializing services"
case RecoveringSessions:
return "Recovering sessions"
case Ready:
return "Ready"
default:
return ""
}
}
type Snapshot struct {
Phase Phase `json:"phase"`
Boot int64 `json:"boot"`
Seq uint64 `json:"seq"`
ElapsedMS int64 `json:"elapsed_ms"`
PhaseElapsedMS int64 `json:"phase_elapsed_ms"`
Step *StepSnapshot `json:"step,omitempty"`
}
type Reporter struct {
mu sync.Mutex
started, phaseStarted time.Time
phase Phase
log *logger.Logger
boot int64
seq uint64
active *stepActivation
unregisteredWarned map[StepID]bool
mismatchWarned map[mismatchKey]bool
}
type contextKey struct{}
func New(log *logger.Logger) *Reporter {
now := time.Now()
return &Reporter{started: now, phaseStarted: now, phase: OpeningDatabase, log: log, boot: newBoot()}
}
// newBoot draws a positive, non-guessable, per-process identifier below 2^53
// so every value decodes exactly when a consumer reads JSON numbers as
// doubles. crypto/rand.Read never errors on the Go version this module
// requires, which is what lets New keep its error-free signature.
func newBoot() int64 {
var buf [8]byte
for {
_, _ = cryptorand.Read(buf[:])
v := int64(binary.BigEndian.Uint64(buf[:]) & ((1 << 53) - 1))
if v != 0 {
return v
}
}
}
func WithReporter(ctx context.Context, r *Reporter) context.Context {
return context.WithValue(ctx, contextKey{}, r)
}
func FromContext(ctx context.Context) *Reporter {
r, _ := ctx.Value(contextKey{}).(*Reporter)
return r
}
func SetPhase(ctx context.Context, phase Phase) {
if r := FromContext(ctx); r != nil {
r.Set(phase)
}
}
func (r *Reporter) Snapshot() Snapshot {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
snap := Snapshot{
Phase: r.phase,
Boot: r.boot,
Seq: r.seq,
ElapsedMS: now.Sub(r.started).Milliseconds(),
PhaseElapsedMS: now.Sub(r.phaseStarted).Milliseconds(),
}
if r.active != nil {
snap.Step = r.active.snapshot(now)
}
return snap
}
func (r *Reporter) Set(phase Phase) {
r.mu.Lock()
defer r.mu.Unlock()
if r.phase == phase {
return
}
if r.log != nil {
r.log.Info("Startup phase completed", zap.String("phase", string(r.phase)), zap.Duration("duration", time.Since(r.phaseStarted)), zap.Duration("elapsed", time.Since(r.started)))
}
r.phase = phase
r.phaseStarted = time.Now()
if r.log != nil {
r.log.Info("Startup phase started", zap.String("phase", string(phase)), zap.Duration("elapsed", time.Since(r.started)))
}
}
func (r *Reporter) Report() {
s := r.Snapshot()
if r.log != nil && s.Phase != Ready {
r.log.Info("Startup in progress", zap.String("phase", string(s.Phase)), zap.Int64("elapsed_ms", s.ElapsedMS), zap.Int64("phase_elapsed_ms", s.PhaseElapsedMS))
}
}