feat(server): скелет Go-сервера с healthz, readyz и api/v1/meta
- internal/config: конфигурация из env с валидацией и дефолтами (§5.1, §9.1) - internal/httpx: middleware (request id, slog-логи, recover, security-заголовки), единый формат ошибок §8.5 и лимит тела запроса - internal/database: SQLite WAL + busy_timeout + foreign_keys, один writer и пул чтения, goose-миграции embedded, PRAGMA optimize по расписанию - internal/meta: полезная нагрузка /api/v1/meta (версия, фичи, лимиты) - internal/server: маршруты, SPA-раздача собранного клиента, OpenAPI 3.1 - cmd/glchat: точка входа с graceful shutdown и JSON-логами - тесты: конфиг, миграции и идемпотентность, HTTP-эндпоинты, статика, 404/405
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"glchat/internal/config"
|
||||
"glchat/internal/database"
|
||||
"glchat/internal/server"
|
||||
)
|
||||
|
||||
// Build metadata is injected with -ldflags "-X main.version=... -X main.commit=... -X main.buildDate=...".
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
buildDate = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
slog.Error("glchat exited with error", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if v := os.Getenv("APP_VERSION"); v != "" {
|
||||
version = v
|
||||
}
|
||||
if c := os.Getenv("APP_COMMIT"); c != "" {
|
||||
commit = c
|
||||
}
|
||||
if b := os.Getenv("APP_BUILD_DATE"); b != "" {
|
||||
buildDate = b
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load configuration: %w", err)
|
||||
}
|
||||
cfg.Version = version
|
||||
cfg.Commit = commit
|
||||
cfg.BuildDate = buildDate
|
||||
|
||||
logger := newLogger(cfg)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
db, err := database.Open(ctx, database.Options{
|
||||
Path: cfg.DatabasePath,
|
||||
ReadPool: cfg.ReadPoolSize,
|
||||
CacheSize: cfg.SQLiteCache,
|
||||
MmapSize: cfg.SQLiteMmap,
|
||||
Migrate: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
logger.Error("close database", slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
|
||||
schemaVersion, err := db.SchemaVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
journalMode, err := db.JournalMode(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("database ready",
|
||||
slog.String("path", db.Path()),
|
||||
slog.Int64("schema_version", schemaVersion),
|
||||
slog.String("journal_mode", journalMode),
|
||||
)
|
||||
|
||||
stopMaintenance := db.RunMaintenance(ctx, logger, cfg.Maintenance)
|
||||
defer stopMaintenance()
|
||||
|
||||
srv := server.New(cfg, db, logger)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
logger.Info("http server listening",
|
||||
slog.String("addr", cfg.ListenAddr),
|
||||
slog.String("version", cfg.Version),
|
||||
slog.String("domain", cfg.Domain),
|
||||
)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return fmt.Errorf("http server: %w", err)
|
||||
case <-ctx.Done():
|
||||
logger.Info("shutdown requested")
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTTL)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("graceful shutdown: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLogger(cfg config.Config) *slog.Logger {
|
||||
opts := &slog.HandlerOptions{Level: slog.Level(cfg.LogLevelNumeric())}
|
||||
var handler slog.Handler
|
||||
if cfg.LogFormat == "text" {
|
||||
handler = slog.NewTextHandler(os.Stdout, opts)
|
||||
} else {
|
||||
handler = slog.NewJSONHandler(os.Stdout, opts)
|
||||
}
|
||||
return slog.New(handler).With(
|
||||
slog.String("service", "glchat"),
|
||||
slog.String("version", cfg.Version),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user