|
|
@@ -2,8 +2,12 @@ package main
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
- "log"
|
|
|
+ "log/slog"
|
|
|
"net/http"
|
|
|
+ "os"
|
|
|
+ "os/signal"
|
|
|
+ "syscall"
|
|
|
+ "time"
|
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
|
|
@@ -12,6 +16,7 @@ import (
|
|
|
|
|
|
"github.com/photoplaces/backend/internal/config"
|
|
|
"github.com/photoplaces/backend/internal/handlers"
|
|
|
+ "github.com/photoplaces/backend/internal/log"
|
|
|
"github.com/photoplaces/backend/internal/middleware"
|
|
|
"github.com/photoplaces/backend/internal/repository"
|
|
|
"github.com/photoplaces/backend/internal/services"
|
|
|
@@ -21,12 +26,26 @@ func main() {
|
|
|
cfg := config.Load()
|
|
|
ctx := context.Background()
|
|
|
|
|
|
+ // Инициализация структурированного логгера
|
|
|
+ logger := log.Init(cfg.AppEnv)
|
|
|
+ ctx = log.WithContext(ctx, logger)
|
|
|
+
|
|
|
+ logger.InfoContext(ctx, "starting application",
|
|
|
+ slog.String("env", cfg.AppEnv),
|
|
|
+ slog.String("port", cfg.ServerPort),
|
|
|
+ )
|
|
|
+
|
|
|
+ // Подключение к базе данных
|
|
|
pool, err := repository.NewPool(ctx, cfg.DatabaseURL)
|
|
|
if err != nil {
|
|
|
- log.Fatalf("database connection: %v", err)
|
|
|
+ logger.ErrorContext(ctx, "database connection failed", log.WithError(err))
|
|
|
+ os.Exit(1)
|
|
|
}
|
|
|
defer pool.Close()
|
|
|
|
|
|
+ logger.InfoContext(ctx, "database connected")
|
|
|
+
|
|
|
+ // Репозитории
|
|
|
userRepo := repository.NewUserRepo(pool)
|
|
|
refreshTokenRepo := repository.NewRefreshTokenRepo(pool)
|
|
|
placeRepo := repository.NewPlaceRepo(pool)
|
|
|
@@ -36,9 +55,11 @@ func main() {
|
|
|
tagRepo := repository.NewTagRepo(pool)
|
|
|
featureRepo := repository.NewFeatureRepo(pool)
|
|
|
|
|
|
+ // Сервисы
|
|
|
authSvc := services.NewAuthService(userRepo, refreshTokenRepo, cfg.JWTSecret, cfg.JWTRefreshSecret)
|
|
|
placeSvc := services.NewPlaceService(placeRepo)
|
|
|
|
|
|
+ // Хендлеры
|
|
|
authHandler := handlers.NewAuthHandler(authSvc, cfg.AppEnv)
|
|
|
userHandler := handlers.NewUserHandler(userRepo)
|
|
|
placeHandler := handlers.NewPlaceHandler(placeSvc)
|
|
|
@@ -49,18 +70,23 @@ func main() {
|
|
|
wsHub := handlers.NewWSHub()
|
|
|
uploadHandler, err := handlers.NewUploadHandler(cfg.S3Endpoint, cfg.S3PublicEndpoint, cfg.S3AccessKey, cfg.S3SecretKey, cfg.S3Bucket, false)
|
|
|
if err != nil {
|
|
|
- log.Fatalf("upload handler: %v", err)
|
|
|
+ logger.ErrorContext(ctx, "upload handler init failed", log.WithError(err))
|
|
|
+ os.Exit(1)
|
|
|
}
|
|
|
|
|
|
+ // Redis для rate limiting
|
|
|
redisOpts, err := redis.ParseURL(cfg.RedisURL)
|
|
|
if err != nil {
|
|
|
- log.Fatalf("parse redis url: %v", err)
|
|
|
+ logger.ErrorContext(ctx, "parse redis url failed", log.WithError(err))
|
|
|
+ os.Exit(1)
|
|
|
}
|
|
|
redisClient := redis.NewClient(redisOpts)
|
|
|
defer redisClient.Close()
|
|
|
|
|
|
if err := redisClient.Ping(ctx).Err(); err != nil {
|
|
|
- log.Printf("Warning: Redis connection failed, rate limiting disabled: %v", err)
|
|
|
+ logger.WarnContext(ctx, "redis connection failed, rate limiting disabled", log.WithError(err))
|
|
|
+ } else {
|
|
|
+ logger.InfoContext(ctx, "redis connected")
|
|
|
}
|
|
|
|
|
|
authLimiter, _ := middleware.RateLimitAuthEndpoints(redisClient)
|
|
|
@@ -68,9 +94,11 @@ func main() {
|
|
|
apiWriteLimiter, _ := middleware.RateLimitAPIWrite(redisClient)
|
|
|
adminLimiter, _ := middleware.RateLimitAdmin(redisClient)
|
|
|
|
|
|
+ // Роутер
|
|
|
r := chi.NewRouter()
|
|
|
|
|
|
- r.Use(chimiddleware.Logger)
|
|
|
+ // Middleware для логирования запросов
|
|
|
+ r.Use(requestLoggerMiddleware(logger))
|
|
|
r.Use(chimiddleware.Recoverer)
|
|
|
r.Use(chimiddleware.RequestID)
|
|
|
r.Use(cors.Handler(cors.Options{
|
|
|
@@ -166,9 +194,67 @@ func main() {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- port := cfg.ServerPort
|
|
|
- log.Printf("Starting server on :%s", port)
|
|
|
- if err := http.ListenAndServe(":"+port, r); err != nil {
|
|
|
- log.Fatal(err)
|
|
|
+ // HTTP сервер с graceful shutdown
|
|
|
+ server := &http.Server{
|
|
|
+ Addr: ":" + cfg.ServerPort,
|
|
|
+ Handler: r,
|
|
|
+ ReadTimeout: 15 * time.Second,
|
|
|
+ WriteTimeout: 15 * time.Second,
|
|
|
+ IdleTimeout: 60 * time.Second,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Запуск сервера в горутине
|
|
|
+ go func() {
|
|
|
+ logger.InfoContext(ctx, "starting http server", slog.String("addr", server.Addr))
|
|
|
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
+ logger.ErrorContext(ctx, "http server error", log.WithError(err))
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ // Ожидание сигнала завершения
|
|
|
+ quit := make(chan os.Signal, 1)
|
|
|
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
|
+ <-quit
|
|
|
+
|
|
|
+ logger.InfoContext(ctx, "shutdown signal received, gracefully stopping...")
|
|
|
+
|
|
|
+ // Graceful shutdown с таймаутом
|
|
|
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
+ defer cancel()
|
|
|
+
|
|
|
+ if err := server.Shutdown(shutdownCtx); err != nil {
|
|
|
+ logger.ErrorContext(ctx, "server shutdown failed", log.WithError(err))
|
|
|
+ os.Exit(1)
|
|
|
}
|
|
|
+
|
|
|
+ logger.InfoContext(ctx, "server stopped gracefully")
|
|
|
}
|
|
|
+
|
|
|
+// requestLoggerMiddleware логирует входящие HTTP запросы
|
|
|
+func requestLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
|
|
|
+ return func(next http.Handler) http.Handler {
|
|
|
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ start := time.Now()
|
|
|
+ ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
|
+
|
|
|
+ requestID := chimiddleware.GetReqID(r.Context())
|
|
|
+ ctx := log.WithContext(r.Context(), logger.With("request_id", requestID))
|
|
|
+
|
|
|
+ logger.InfoContext(ctx, "request started",
|
|
|
+ slog.String("method", r.Method),
|
|
|
+ slog.String("path", r.URL.Path),
|
|
|
+ slog.String("remote_addr", r.RemoteAddr),
|
|
|
+ slog.String("user_agent", r.UserAgent()),
|
|
|
+ )
|
|
|
+
|
|
|
+ next.ServeHTTP(ww, r.WithContext(ctx))
|
|
|
+
|
|
|
+ logger.InfoContext(ctx, "request completed",
|
|
|
+ slog.Int("status", ww.Status()),
|
|
|
+ slog.Int("bytes_written", ww.BytesWritten()),
|
|
|
+ slog.Duration("duration", time.Since(start)),
|
|
|
+ )
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|