Files
iu 6361e48b20 feat(observability): configurable logging, request ids and error logs
- internal/logging: LOG_LEVEL/LOG_FORMAT parsing and slog handler
- request_id middleware (X-Request-ID or generated, echoed) + debug access log
- httpx registers errors; middleware logs internal causes with request_id
- health excluded, objects/sse debug-only, no query/body/secrets in logs
- new observability capability spec; README/.env.example docs
2026-10-11 13:40:11 +03:00

185 lines
4.4 KiB
Go

package main
import (
"bufio"
"context"
"errors"
"io/fs"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
assets "galo"
"galo/internal/app"
"galo/internal/auth"
"galo/internal/config"
"galo/internal/db"
"galo/internal/logging"
"galo/internal/settings"
"galo/internal/sse"
"galo/internal/storage"
"galo/internal/store"
)
// version is set at build time via -ldflags "-X main.version=..."
var version = "dev"
func main() {
loadDotEnv(".env")
loadDotEnv("../.env")
cfg, err := config.Load()
if err != nil {
slog.Error("config error", "error", err)
os.Exit(1)
}
level, err := logging.ParseLevel(cfg.Log.Level)
if err != nil {
slog.Error("invalid log level", "error", err)
os.Exit(1)
}
handler, err := logging.NewHandler(cfg.Log.Format, level)
if err != nil {
slog.Error("invalid log format", "error", err)
os.Exit(1)
}
slog.SetDefault(slog.New(handler))
slog.Info("starting galo", "version", version)
switch cfg.Database.Backend {
case "sqlite":
slog.Info("metadata backend", "backend", "sqlite", "path", cfg.Database.SQLitePath)
default:
slog.Info("metadata backend", "backend", "postgres",
"host", cfg.Postgres.Host, "database", cfg.Postgres.Database)
}
switch cfg.Storage.Backend {
case "fs":
slog.Info("storage backend", "backend", "fs", "root", cfg.Storage.FSRoot)
default:
slog.Info("storage backend", "backend", "s3",
"endpoint", cfg.S3.Endpoint, "bucket", cfg.S3.Bucket)
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
var (
repos store.Repos
ping func(ctx context.Context) error
closer func()
)
switch cfg.Database.Backend {
case "sqlite":
migFS, err := fs.Sub(assets.Migrations, "migrations/sqlite")
if err != nil {
slog.Error("migrations fs", "error", err)
os.Exit(1)
}
if err := db.MigrateSQLite(cfg.Database.SQLitePath, migFS); err != nil {
slog.Error("migration failed", "error", err)
os.Exit(1)
}
database, err := db.OpenSQLite(cfg.Database.SQLitePath)
if err != nil {
slog.Error("database open failed", "error", err)
os.Exit(1)
}
repos = store.NewSQLite(database)
ping = database.PingContext
closer = func() { _ = database.Close() }
default:
migFS, err := fs.Sub(assets.Migrations, "migrations")
if err != nil {
slog.Error("migrations fs", "error", err)
os.Exit(1)
}
if err := db.Migrate(cfg.Postgres.DSN(), migFS); err != nil {
slog.Error("migration failed", "error", err)
os.Exit(1)
}
pool, err := db.Connect(ctx, cfg.Postgres.DSN())
if err != nil {
slog.Error("database connect failed", "error", err)
os.Exit(1)
}
repos = store.NewPostgres(pool)
ping = pool.Ping
closer = pool.Close
}
defer closer()
slog.Info("migrations applied", "db_backend", cfg.Database.Backend)
secret, err := repos.Settings.Ensure(ctx, settings.JWTSecretKey, settings.GenerateSecret)
if err != nil {
slog.Error("secret init failed", "error", err)
os.Exit(1)
}
tm := auth.NewTokenManager(secret, cfg.Auth.AccessTTL, cfg.Auth.RefreshTTL)
blob, err := storage.Open(ctx, cfg, secret)
if err != nil {
slog.Error("storage init failed", "error", err)
os.Exit(1)
}
if err := blob.EnsureBucket(ctx); err != nil {
slog.Warn("could not ensure bucket", "error", err)
} else {
slog.Info("storage ready", "location", blob.Bucket())
}
hub := sse.NewHub(cfg.SSE.ClientBuffer)
server := app.New(cfg, repos, ping, tm, blob, hub)
go server.StartWorker(ctx)
httpServer := &http.Server{
Addr: cfg.Server.Addr,
Handler: server.Router(),
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
slog.Info("listening", "addr", cfg.Server.Addr)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server error", "error", err)
stop()
}
}()
<-ctx.Done()
slog.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
}
func loadDotEnv(path string) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if _, exists := os.LookupEnv(key); !exists {
_ = os.Setenv(key, value)
}
}
}