165 lines
4.3 KiB
Go
165 lines
4.3 KiB
Go
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):
|
|
}
|
|
}
|
|
}
|