diff --git a/README.md b/README.md index 7d104f4..5e7a3ef 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ Hosting by ValitovGaziz team on docker compose -create REST API on Golang 1.25.1 \ No newline at end of file +create REST API on Golang 1.25.1 + +### zap logger внедрить \ No newline at end of file diff --git a/serv_nginx/api_bb/.env b/serv_nginx/api_bb/.env index e65872d..c5d5e5f 100644 --- a/serv_nginx/api_bb/.env +++ b/serv_nginx/api_bb/.env @@ -5,4 +5,12 @@ DB_USER=postgres DB_PASSWORD=postgres DB_NAME=bb_db DB_SSLMODE=disable -JWT_SECRET=your-super-secret-jwt-key-change-in-production \ No newline at end of file +JWT_SECRET=your-super-secret-jwt-key-change-in-production + + +# .env +LOG_LEVEL=debug +ENVIRONMENT=development + +# app +REST_API_VERSION=1.0.0 \ No newline at end of file diff --git a/serv_nginx/api_bb/cmd/main.go b/serv_nginx/api_bb/cmd/main.go index 95312d6..991364d 100644 --- a/serv_nginx/api_bb/cmd/main.go +++ b/serv_nginx/api_bb/cmd/main.go @@ -1,37 +1,92 @@ -// main.go +// main.go с graceful shutdown package main import ( + "context" "log" "net/http" + "os" + "os/signal" + "syscall" + "time" + "go.uber.org/zap" "gorm.io/driver/postgres" "gorm.io/gorm" "api_bb/internal/config" "api_bb/internal/models" "api_bb/internal/routes" + "api_bb/pkg/logger" ) func main() { + // Загрузка конфигурации cfg := config.Load() + // Инициализация логгера + if err := logger.Init( + os.Getenv("LOG_LEVEL"), + os.Getenv("ENVIRONMENT"), + ); err != nil { + log.Printf("Failed to initialize logger: %v", err) + os.Exit(1) + } + defer logger.Sync() + + zapLogger := logger.Get() + + // Логируем начало работы + logger.LogApplicationStart(os.Getenv("REST_API_VERSION"), os.Getenv("ENVIRONMENT"), "") + + // Подключение к базе данных db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{}) if err != nil { - log.Fatal("Failed to connect to database:", err) + zapLogger.Fatal("failed to connect to database", zap.Error(err)) } - + // Автомиграция if err := db.AutoMigrate(&models.User{}); err != nil { - log.Fatal("Failed to migrate database:", err) + zapLogger.Fatal("database migration failed", zap.Error(err)) } - + // Настройка роутера router := routes.SetupRouter(db, cfg) - - // Запуск сервера - log.Printf("Server starting on port %s", cfg.Port) - log.Fatal(http.ListenAndServe(":"+cfg.Port, router)) + + // Настройка HTTP сервера + server := &http.Server{ + Addr: ":" + cfg.Port, + Handler: router, + } + + // Канал для graceful shutdown + done := make(chan bool, 1) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + + // Запуск сервера в горутине + go func() { + zapLogger.Info("starting HTTP server", zap.String("port", cfg.Port)) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + zapLogger.Fatal("failed to start server", zap.Error(err)) + } + }() + + // Ожидание сигнала shutdown + <-quit + zapLogger.Info("shutdown signal received") + + // Graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + server.SetKeepAlivesEnabled(false) + if err := server.Shutdown(ctx); err != nil { + zapLogger.Fatal("could not gracefully shutdown the server", zap.Error(err)) + } + + logger.LogApplicationShutdown("graceful shutdown") + close(done) } \ No newline at end of file diff --git a/serv_nginx/api_bb/go.mod b/serv_nginx/api_bb/go.mod index 636c5a7..2d7211d 100644 --- a/serv_nginx/api_bb/go.mod +++ b/serv_nginx/api_bb/go.mod @@ -11,6 +11,8 @@ require ( gorm.io/gorm v1.31.0 ) +require go.uber.org/multierr v1.10.0 // indirect + require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -18,6 +20,8 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 + go.uber.org/zap v1.27.0 golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.30.0 // indirect ) diff --git a/serv_nginx/api_bb/go.sum b/serv_nginx/api_bb/go.sum index d94e5fa..d526fb6 100644 --- a/serv_nginx/api_bb/go.sum +++ b/serv_nginx/api_bb/go.sum @@ -19,6 +19,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -26,6 +28,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= diff --git a/serv_nginx/api_bb/internal/config/config.go b/serv_nginx/api_bb/internal/config/config.go index cde2f46..c6f4f05 100644 --- a/serv_nginx/api_bb/internal/config/config.go +++ b/serv_nginx/api_bb/internal/config/config.go @@ -4,6 +4,8 @@ package config import ( "fmt" "os" + + "github.com/joho/godotenv" ) type Config struct { @@ -13,6 +15,7 @@ type Config struct { } func Load() *Config { + _ = godotenv.Load(".env") port := getEnv("PORT", "8080") jwtSecret := getEnv("JWT_SECRET", "your-secret-key") diff --git a/serv_nginx/api_bb/pkg/logger/helpers.go b/serv_nginx/api_bb/pkg/logger/helpers.go new file mode 100644 index 0000000..b440ee4 --- /dev/null +++ b/serv_nginx/api_bb/pkg/logger/helpers.go @@ -0,0 +1,35 @@ +// pkg/logger/helpers.go +package logger + +import ( + "time" + + "go.uber.org/zap" +) + +// LogApplicationStart логирует запуск приложения +func LogApplicationStart(version, environment, port string) { + Get().Info("application starting", + zap.String("version", version), + zap.String("environment", environment), + zap.String("port", port), + zap.Time("start_time", time.Now()), + ) +} + +// LogApplicationShutdown логирует graceful shutdown +func LogApplicationShutdown(reason string) { + Get().Info("application shutting down", + zap.String("reason", reason), + zap.Time("shutdown_time", time.Now()), + ) +} + +// LogDatabaseStats логирует статистику базы данных +func LogDatabaseStats(stats map[string]interface{}) { + fields := make([]zap.Field, 0, len(stats)) + for key, value := range stats { + fields = append(fields, zap.Any(key, value)) + } + Get().Info("database statistics", fields...) +} \ No newline at end of file diff --git a/serv_nginx/api_bb/pkg/logger/interface.go b/serv_nginx/api_bb/pkg/logger/interface.go new file mode 100644 index 0000000..4316c77 --- /dev/null +++ b/serv_nginx/api_bb/pkg/logger/interface.go @@ -0,0 +1,75 @@ +// pkg/logger/interface.go +package logger + +import "go.uber.org/zap" + +// Interface определяет контракт для логгера +type Interface interface { + Debug(msg string, fields ...zap.Field) + Info(msg string, fields ...zap.Field) + Warn(msg string, fields ...zap.Field) + Error(msg string, fields ...zap.Field) + Fatal(msg string, fields ...zap.Field) + + Debugf(template string, args ...interface{}) + Infof(template string, args ...interface{}) + Warnf(template string, args ...interface{}) + Errorf(template string, args ...interface{}) + Fatalf(template string, args ...interface{}) + + With(fields ...zap.Field) Interface +} + +// wrapper обертка для zap.Logger +type wrapper struct { + logger *zap.Logger +} + +// NewWrapper создает новую обертку +func NewWrapper(logger *zap.Logger) Interface { + return &wrapper{logger: logger} +} + +func (w *wrapper) Debug(msg string, fields ...zap.Field) { + w.logger.Debug(msg, fields...) +} + +func (w *wrapper) Info(msg string, fields ...zap.Field) { + w.logger.Info(msg, fields...) +} + +func (w *wrapper) Warn(msg string, fields ...zap.Field) { + w.logger.Warn(msg, fields...) +} + +func (w *wrapper) Error(msg string, fields ...zap.Field) { + w.logger.Error(msg, fields...) +} + +func (w *wrapper) Fatal(msg string, fields ...zap.Field) { + w.logger.Fatal(msg, fields...) +} + +func (w *wrapper) Debugf(template string, args ...interface{}) { + w.logger.Sugar().Debugf(template, args...) +} + +func (w *wrapper) Infof(template string, args ...interface{}) { + w.logger.Sugar().Infof(template, args...) +} + +func (w *wrapper) Warnf(template string, args ...interface{}) { + w.logger.Sugar().Warnf(template, args...) +} + +func (w *wrapper) Errorf(template string, args ...interface{}) { + w.logger.Sugar().Errorf(template, args...) +} + +func (w *wrapper) Fatalf(template string, args ...interface{}) { + w.logger.Sugar().Fatalf(template, args...) +} + +func (w *wrapper) With(fields ...zap.Field) Interface { + return &wrapper{logger: w.logger.With(fields...)} +} \ No newline at end of file diff --git a/serv_nginx/api_bb/pkg/logger/logger.go b/serv_nginx/api_bb/pkg/logger/logger.go new file mode 100644 index 0000000..a263e86 --- /dev/null +++ b/serv_nginx/api_bb/pkg/logger/logger.go @@ -0,0 +1,65 @@ +// pkg/logger/logger.go +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var globalLogger *zap.Logger + +// Init инициализирует глобальный логгер +func Init(level string, environment string) error { + var config zap.Config + + if environment == "production" { + config = zap.NewProductionConfig() + } else { + config = zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + } + + // Устанавливаем уровень логирования + switch level { + case "debug": + config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + case "info": + config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) + case "warn": + config.Level = zap.NewAtomicLevelAt(zap.WarnLevel) + case "error": + config.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) + default: + config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) + } + + logger, err := config.Build() + if err != nil { + return err + } + + globalLogger = logger + return nil +} + +// Get возвращает глобальный логгер +func Get() *zap.Logger { + if globalLogger == nil { + // Fallback на стандартный логгер если не инициализирован + logger, _ := zap.NewProduction() + return logger + } + return globalLogger +} + +// Sync синхронизирует буферы логгера +func Sync() { + if globalLogger != nil { + globalLogger.Sync() + } +} + +// Sugar возвращает SugaredLogger +func Sugar() *zap.SugaredLogger { + return Get().Sugar() +} \ No newline at end of file diff --git a/serv_nginx/api_bb/pkg/middleware/logger.go b/serv_nginx/api_bb/pkg/middleware/logger.go new file mode 100644 index 0000000..0644efe --- /dev/null +++ b/serv_nginx/api_bb/pkg/middleware/logger.go @@ -0,0 +1,46 @@ +// pkg/middleware/logger.go +package middleware + +import ( + "net/http" + "time" + + "api_bb/pkg/logger" + + "github.com/go-chi/chi/v5/middleware" + "go.uber.org/zap" +) + +// Logger middleware для логирования HTTP запросов +func Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Получаем request ID + reqID := middleware.GetReqID(r.Context()) + + // Создаем логгер с контекстом запроса + requestLogger := logger.Get().With( + zap.String("method", r.Method), + zap.String("path", r.URL.Path), + zap.String("remote_addr", r.RemoteAddr), + zap.String("user_agent", r.UserAgent()), + zap.String("request_id", reqID), + ) + + // Обертываем ResponseWriter для получения статуса + wrappedWriter := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + // Обрабатываем запрос + next.ServeHTTP(wrappedWriter, r) + + // Логируем результат + duration := time.Since(start) + + requestLogger.Info("request completed", + zap.Int("status", wrappedWriter.Status()), + zap.Int("bytes", wrappedWriter.BytesWritten()), + zap.Duration("duration", duration), + ) + }) +} \ No newline at end of file