package main import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "strconv" "sync/atomic" "syscall" "time" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.34.0" ) var appVersion = "dev" type config struct { port string database string chaosRate float64 logLevel slog.Level traceReady bool } func main() { if err := run(); err != nil { slog.Error("startup failed", "error", err) os.Exit(1) } } func run() error { cfg, err := loadConfig() if err != nil { return err } logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.logLevel})) slog.SetDefault(logger) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() shutdownTrace, err := configureTracing(ctx, cfg.traceReady) if err != nil { return err } defer func() { if err := shutdownTrace(context.Background()); err != nil { logger.Error("trace shutdown failed", "error", err) } }() registry := prometheus.NewRegistry() m := newMetrics(registry) db, err := newPostgresStore(ctx, cfg.database, m) if err != nil { return err } defer db.Close() var migrated atomic.Bool go migrateUntilReady(ctx, db, &migrated, logger) application := &api{store: db, metrics: m, logger: logger, version: appVersion, chaosRate: cfg.chaosRate, migrated: &migrated, prometheus: registry} server := &http.Server{ Addr: ":" + cfg.port, Handler: application.routes(), ReadHeaderTimeout: 5 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20, } errCh := make(chan error, 1) go func() { errCh <- server.ListenAndServe() }() logger.Info("server started", "port", cfg.port, "version", appVersion) select { case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("shutdown server: %w", err) } return nil case err := <-errCh: if errors.Is(err, http.ErrServerClosed) { return nil } return fmt.Errorf("serve HTTP: %w", err) } } func loadConfig() (config, error) { cfg := config{port: envOr("PORT", "8080"), database: os.Getenv("DATABASE_URL"), traceReady: os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != ""} if cfg.database == "" { return config{}, errors.New("DATABASE_URL is required") } chaos, err := strconv.ParseFloat(envOr("CHAOS_ERROR_RATE", "0.0"), 64) if err != nil || chaos < 0 || chaos > 1 { return config{}, errors.New("CHAOS_ERROR_RATE must be between 0.0 and 1.0") } cfg.chaosRate = chaos switch envOr("LOG_LEVEL", "info") { case "debug": cfg.logLevel = slog.LevelDebug case "info": cfg.logLevel = slog.LevelInfo case "warn": cfg.logLevel = slog.LevelWarn case "error": cfg.logLevel = slog.LevelError default: return config{}, errors.New("LOG_LEVEL must be debug, info, warn, or error") } return cfg, nil } func envOr(name, fallback string) string { if value := os.Getenv(name); value != "" { return value } return fallback } func configureTracing(ctx context.Context, enabled bool) (func(context.Context) error, error) { if !enabled { return func(context.Context) error { return nil }, nil } exporter, err := otlptracegrpc.New(ctx) if err != nil { return nil, fmt.Errorf("create trace exporter: %w", err) } res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName("nereus-api"))) if err != nil { return nil, fmt.Errorf("create trace resource: %w", err) } provider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter), sdktrace.WithResource(res)) otel.SetTracerProvider(provider) return provider.Shutdown, nil } func migrateUntilReady(ctx context.Context, db store, ready *atomic.Bool, logger *slog.Logger) { for { attempt, cancel := context.WithTimeout(ctx, 5*time.Second) err := db.Migrate(attempt) cancel() if err == nil { ready.Store(true) logger.Info("database migrations applied") return } logger.Warn("database unavailable; migration will retry", "error", err) select { case <-ctx.Done(): return case <-time.After(5 * time.Second): } } }