From ed355ee60d4cec0eb967ea8141e3c14514c9dc1c Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 11 Nov 2025 05:58:36 +0500 Subject: [PATCH] modified: main_dc/docker-compose.yml modified: main_dc/yalarba/api_es/.env modified: main_dc/yalarba/api_es/cmd/main.go modified: main_dc/yalarba/api_es/go.mod modified: main_dc/yalarba/api_es/go.sum new file: main_dc/yalarba/api_es/internal/config/config.go new file: main_dc/yalarba/api_es/internal/database/psql_db.go new file: main_dc/yalarba/api_es/pkg/logger/helpers.go new file: main_dc/yalarba/api_es/pkg/logger/interface.go new file: main_dc/yalarba/api_es/pkg/logger/logger.go new file: main_dc/yalarba/api_es/pkg/logger/route_logger.go add new User model for api_es add global zapLogger api_es add configs dotenv api_es sipmplify main api_es --- main_dc/docker-compose.yml | 2 + main_dc/yalarba/api_es/.env | 7 +- main_dc/yalarba/api_es/cmd/main.go | 219 ++---------------- main_dc/yalarba/api_es/go.mod | 3 + main_dc/yalarba/api_es/go.sum | 4 + .../yalarba/api_es/internal/config/config.go | 42 ++++ .../api_es/internal/database/psql_db.go | 46 ++++ main_dc/yalarba/api_es/pkg/logger/helpers.go | 35 +++ .../yalarba/api_es/pkg/logger/interface.go | 75 ++++++ main_dc/yalarba/api_es/pkg/logger/logger.go | 66 ++++++ .../yalarba/api_es/pkg/logger/route_logger.go | 100 ++++++++ 11 files changed, 403 insertions(+), 196 deletions(-) create mode 100644 main_dc/yalarba/api_es/internal/config/config.go create mode 100644 main_dc/yalarba/api_es/internal/database/psql_db.go create mode 100644 main_dc/yalarba/api_es/pkg/logger/helpers.go create mode 100644 main_dc/yalarba/api_es/pkg/logger/interface.go create mode 100644 main_dc/yalarba/api_es/pkg/logger/logger.go create mode 100644 main_dc/yalarba/api_es/pkg/logger/route_logger.go diff --git a/main_dc/docker-compose.yml b/main_dc/docker-compose.yml index b2ae705..ee0c2d8 100644 --- a/main_dc/docker-compose.yml +++ b/main_dc/docker-compose.yml @@ -212,6 +212,8 @@ services: dockerfile: Dockerfile container_name: api_es restart: unless-stopped + env_file: + - ./yalarba/api_es/.env depends_on: db: condition: service_healthy diff --git a/main_dc/yalarba/api_es/.env b/main_dc/yalarba/api_es/.env index 78090e2..bff6bc7 100644 --- a/main_dc/yalarba/api_es/.env +++ b/main_dc/yalarba/api_es/.env @@ -4,4 +4,9 @@ DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres DB_NAME=mydb -APP_PORT=8080 \ No newline at end of file +APP_PORT=8080 +JWT_SECRET=secret +UPLOAD_PATH=./storage/uploads +ENVIRONMENT=development +LOG_LEVEL=debug +APP_PORT=8081 \ No newline at end of file diff --git a/main_dc/yalarba/api_es/cmd/main.go b/main_dc/yalarba/api_es/cmd/main.go index cc9323b..f58b287 100644 --- a/main_dc/yalarba/api_es/cmd/main.go +++ b/main_dc/yalarba/api_es/cmd/main.go @@ -2,81 +2,46 @@ package main import ( "encoding/json" - "fmt" "log" "net/http" - "os" - "time" + + "api_es/internal/config" + "api_es/internal/database" + "api_es/pkg/logger" "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "gorm.io/driver/postgres" + "go.uber.org/zap" "gorm.io/gorm" - "gorm.io/gorm/logger" ) -// Модели для БД -type User struct { - ID uint `json:"id" gorm:"primarykey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"` - - Name string `json:"name" gorm:"size:100;not null"` - Email string `json:"email" gorm:"size:255;uniqueIndex;not null"` - Password string `json:"-" gorm:"size:255;not null"` // Пароль не возвращаем в JSON -} - -type Config struct { - DBHost string - DBPort string - DBUser string - DBPassword string - DBName string - AppPort string -} - var db *gorm.DB -var config Config func main() { // Загрузка конфигурации - config = Config{ - DBHost: getEnv("DB_HOST", "db"), - DBPort: getEnv("DB_PORT", "5432"), - DBUser: getEnv("DB_USER", "postgres"), - DBPassword: getEnv("DB_PASSWORD", "postgres"), - DBName: getEnv("DB_NAME", "mydb"), - AppPort: getEnv("APP_PORT", "8081"), - } + cfg := config.Load() + + logger.Init(cfg.LogLevel, cfg.Environment) + zapLogger := logger.Get() + zapLogger.Info("Start api_es REST API on stack Golang (gorm, chi) and PostgresDB connect") // Инициализация БД - if err := initDB(); err != nil { - log.Fatal("Failed to connect to database:", err) + db, err := database.NewPostgresConnection(cfg) + if err != nil { + zapLogger.Panic("Failed to connect to database:", zap.Error(err)) } - // Автомиграция - if err := db.AutoMigrate(&User{}); err != nil { - log.Fatal("Failed to migrate database:", err) + sqlDB, err := db.DB() + if err != nil { + zapLogger.Error("failed to get database instance", zap.Error(err)) } + if err := sqlDB.Ping(); err != nil { + zapLogger.Error("database ping failed", zap.Error(err)) + } + + zapLogger.Info("database ping successful") r := chi.NewRouter() - // Стандартные middleware - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - r.Use(middleware.Timeout(60 * time.Second)) - - // Маршруты API - r.Route("/api", func(r chi.Router) { - r.Get("/", handleRoot) - r.Get("/users", getUsers) - r.Post("/users", createUser) - r.Get("/users/{id}", getUser) - r.Put("/users/{id}", updateUser) - r.Delete("/users/{id}", deleteUser) - }) - // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -84,145 +49,9 @@ func main() { }) // Запуск сервера - log.Printf("Server starting on port %s", config.AppPort) - if err := http.ListenAndServe(":"+config.AppPort, r); err != nil { + zapLogger.Info("Server starting on port %s", zap.String("AppPort", cfg.AppPort)) + log.Printf("Server starting on port %s", cfg.AppPort) + if err := http.ListenAndServe(":"+cfg.AppPort, r); err != nil { log.Fatal("Failed to start server:", err) } } - -func initDB() error { - dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", - config.DBHost, config.DBUser, config.DBPassword, config.DBName, config.DBPort) - - var err error - db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), - }) - if err != nil { - return err - } - - sqlDB, err := db.DB() - if err != nil { - return err - } - - // Настройка пула соединений - sqlDB.SetMaxIdleConns(10) - sqlDB.SetMaxOpenConns(100) - sqlDB.SetConnMaxLifetime(time.Hour) - - log.Println("Successfully connected to database") - return nil -} - -func getEnv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -// Обработчики API -func handleRoot(w http.ResponseWriter, r *http.Request) { - response := map[string]string{ - "message": "EasySite REST API Server", - "version": "1.0.0", - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -func getUsers(w http.ResponseWriter, r *http.Request) { - var users []User - if err := db.Find(&users).Error; err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(users) -} - -func createUser(w http.ResponseWriter, r *http.Request) { - var user User - if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if err := db.Create(&user).Error; err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(user) -} - -func getUser(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - var user User - - if err := db.First(&user, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - http.Error(w, "User not found", http.StatusNotFound) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -func updateUser(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - var user User - - if err := db.First(&user, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - http.Error(w, "User not found", http.StatusNotFound) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - var updateData User - if err := json.NewDecoder(r.Body).Decode(&updateData); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if err := db.Model(&user).Updates(updateData).Error; err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -func deleteUser(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - var user User - - if err := db.First(&user, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - http.Error(w, "User not found", http.StatusNotFound) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - if err := db.Delete(&user).Error; err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"message": "User deleted successfully"}) -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/go.mod b/main_dc/yalarba/api_es/go.mod index b7e2cef..8009eff 100644 --- a/main_dc/yalarba/api_es/go.mod +++ b/main_dc/yalarba/api_es/go.mod @@ -8,6 +8,8 @@ require ( gorm.io/gorm v1.25.10 ) +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 @@ -15,6 +17,7 @@ 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 + go.uber.org/zap v1.27.0 golang.org/x/crypto v0.31.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/main_dc/yalarba/api_es/go.sum b/main_dc/yalarba/api_es/go.sum index 778008f..23b1350 100644 --- a/main_dc/yalarba/api_es/go.sum +++ b/main_dc/yalarba/api_es/go.sum @@ -22,6 +22,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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= diff --git a/main_dc/yalarba/api_es/internal/config/config.go b/main_dc/yalarba/api_es/internal/config/config.go new file mode 100644 index 0000000..ca23f8c --- /dev/null +++ b/main_dc/yalarba/api_es/internal/config/config.go @@ -0,0 +1,42 @@ +package config + +import ( + "os" +) + +type Config struct { + DBHost string + DBPort string + DBUser string + DBPassword string + DBName string + JWTSecret string + ServerPort string + UploadPath string + LogLevel string + Environment string + AppPort string +} + +func Load() *Config { + return &Config{ + DBHost: getEnv("DB_HOST", "localhost"), + DBPort: getEnv("DB_PORT", "5432"), + DBUser: getEnv("DB_USER", "postgres"), + DBPassword: getEnv("DB_PASSWORD", "postgres"), + DBName: getEnv("DB_NAME", "mydb"), + JWTSecret: getEnv("JWT_SECRET", "secret"), + ServerPort: getEnv("SERVER_PORT", "8080"), + UploadPath: getEnv("UPLOAD_PATH", "./storage/uploads"), + LogLevel: getEnv("LOG_LEVEL", "debug"), + Environment: getEnv("ENVIRONMENT", "development"), + AppPort: getEnv("APP_PORT", "8081"), + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/main_dc/yalarba/api_es/internal/database/psql_db.go b/main_dc/yalarba/api_es/internal/database/psql_db.go new file mode 100644 index 0000000..884f49c --- /dev/null +++ b/main_dc/yalarba/api_es/internal/database/psql_db.go @@ -0,0 +1,46 @@ +package database + +import ( + "api_es/internal/config" + "api_es/internal/models" + "api_es/pkg/logger" + "fmt" + "log" + + "go.uber.org/zap" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) { + zapLogger := logger.Get() + zapLogger.Info("Start connect to Postgres DB") + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", + cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort) + zapLogger.Info("dsn = %s", zap.String("dsn", dsn)) + + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + zapLogger.Info("AutoMigrate models") + // Автомиграция + if err := autoMigrate(db); err != nil { + return nil, fmt.Errorf("can't migrate models, error = %s", err) + } + zapLogger.Info("Migrate complite successfully") + + log.Println("Successfully connected to database") + return db, nil +} + +func autoMigrate(db *gorm.DB) error { + // автоматические миграции GORM + return db.AutoMigrate( + &models.User{}, + // другие модели... + ) +} + diff --git a/main_dc/yalarba/api_es/pkg/logger/helpers.go b/main_dc/yalarba/api_es/pkg/logger/helpers.go new file mode 100644 index 0000000..b440ee4 --- /dev/null +++ b/main_dc/yalarba/api_es/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/main_dc/yalarba/api_es/pkg/logger/interface.go b/main_dc/yalarba/api_es/pkg/logger/interface.go new file mode 100644 index 0000000..1a41a5c --- /dev/null +++ b/main_dc/yalarba/api_es/pkg/logger/interface.go @@ -0,0 +1,75 @@ +// pkg/logger/interface.go +package logger + +import "go.uber.org/zap" + +// LoggerInterface определяет контракт для логгера +type LoggerInterface 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) LoggerInterface +} + +// wrapper обертка для zap.Logger +type wrapper struct { + logger *zap.Logger +} + +// NewWrapper создает новую обертку +func NewWrapper(logger *zap.Logger) LoggerInterface { + 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) LoggerInterface { + return &wrapper{logger: w.logger.With(fields...)} +} diff --git a/main_dc/yalarba/api_es/pkg/logger/logger.go b/main_dc/yalarba/api_es/pkg/logger/logger.go new file mode 100644 index 0000000..4602535 --- /dev/null +++ b/main_dc/yalarba/api_es/pkg/logger/logger.go @@ -0,0 +1,66 @@ +// 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/main_dc/yalarba/api_es/pkg/logger/route_logger.go b/main_dc/yalarba/api_es/pkg/logger/route_logger.go new file mode 100644 index 0000000..7b67a06 --- /dev/null +++ b/main_dc/yalarba/api_es/pkg/logger/route_logger.go @@ -0,0 +1,100 @@ +package logger + +import ( + "net/http" + "sort" + "strings" + + "github.com/go-chi/chi/v5" + "go.uber.org/zap" +) + +type RouteLogger struct { + logger LoggerInterface +} + +func NewRouteLogger(log LoggerInterface) *RouteLogger { + return &RouteLogger{ + logger: log, + } +} + +func (rl *RouteLogger) LogRoutes(router *chi.Mux) { + routes := rl.extractRoutes(router) + rl.printFormattedRoutes(routes) +} + +func (rl *RouteLogger) extractRoutes(router *chi.Mux) []RouteInfo { + var routes []RouteInfo + + walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + if route != "" { + routes = append(routes, RouteInfo{ + Method: method, + Path: route, + }) + } + return nil + } + + if err := chi.Walk(router, walkFunc); err != nil { + rl.logger.Error("Failed to walk routes", zap.Error(err)) + } + + return routes +} + +func (rl *RouteLogger) printFormattedRoutes(routes []RouteInfo) { + if len(routes) == 0 { + rl.logger.Info("No routes found") + return + } + + // Группируем по пути + routesByPath := make(map[string][]string) + for _, route := range routes { + routesByPath[route.Path] = append(routesByPath[route.Path], route.Method) + } + + // Сортируем пути + var paths []string + for path := range routesByPath { + paths = append(paths, path) + } + sort.Strings(paths) + + rl.logger.Info("📋 Registered API Routes:") + rl.logger.Info("┌──────────────────────────────────────────────────────────────┐") + + for _, path := range paths { + methods := routesByPath[path] + sort.Strings(methods) + methodsStr := strings.Join(methods, ", ") + + if len(methodsStr) > 12 { + methodsStr = methodsStr[:9] + "..." + } + + methodField := methodsStr + if len(methodField) < 12 { + methodField = methodField + strings.Repeat(" ", 12-len(methodField)) + } + + pathField := path + if len(pathField) > 45 { + pathField = pathField[:42] + "..." + } else { + pathField = pathField + strings.Repeat(" ", 45-len(pathField)) + } + + rl.logger.Info("│ " + methodField + " " + pathField + " │") + } + + rl.logger.Info("└──────────────────────────────────────────────────────────────┘") + rl.logger.Info("Total routes registered: %d", zap.Int("count", len(routes))) +} + +type RouteInfo struct { + Method string + Path string +} \ No newline at end of file