diff --git a/Dockerfile b/Dockerfile index e57d2f7..e4e3eba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,6 +50,9 @@ COPY --from=go-builder /app/.env* ./ # Copy the React build from frontend-builder stage COPY --from=frontend-builder /app/frontend/build ./frontend/build +# Create log directory with proper permissions +RUN mkdir -p /app/logs && chmod 755 /app/logs + # Expose port EXPOSE 8080 diff --git a/_.env.development b/_.env.development index 5d286aa..8da925f 100644 --- a/_.env.development +++ b/_.env.development @@ -3,5 +3,8 @@ ENVIRONMENT=development DATABASE_URL=postgres://postgres:password@postgres:5432/counter_db?sslmode=disable JWT_SECRET=dev-secret-key-change-in-production PORT=8080 +METRICS_PORT=9090 GIN_MODE=debug LOG_LEVEL=debug +LOG_DIR=/app/logs +LOG_VOLUME=counter_logs diff --git a/config.go b/config.go index 47c18f9..498c21d 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,8 @@ type Config struct { GinMode string LogLevel string Debug bool + LogDir string + LogVolume string } // LoadConfig loads configuration with proper environment file precedence @@ -46,6 +48,8 @@ func LoadConfig() *Config { GinMode: getGinMode(env), LogLevel: getLogLevel(env), Debug: env == Development, + LogDir: getEnv("LOG_DIR", "/app/logs"), + LogVolume: getEnv("LOG_VOLUME", "counter_logs"), } // Log configuration (without sensitive data) @@ -183,6 +187,8 @@ func logConfig(config *Config) { log.Printf("║ 📊 LOG LEVEL: %-15s ║", config.LogLevel) log.Printf("║ 🌐 PORT: %-20s ║", config.Port) log.Printf("║ 📈 METRICS PORT: %-15s ║", config.MetricsPort) + log.Printf("║ 📝 LOG DIR: %-20s ║", config.LogDir) + log.Printf("║ 📦 LOG VOLUME: %-18s ║", config.LogVolume) log.Printf("║ ║") log.Printf("║ 📁 Configuration Files Loaded: ║") log.Printf("║ • .env (base configuration) ║") diff --git a/docker-compose.yml b/docker-compose.yml index 57b8d60..4860579 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,11 @@ services: - postgres: - image: postgres:15-alpine - environment: - POSTGRES_DB: counter_db - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - app: build: . ports: - "8080:8080" environment: - ENVIRONMENT=development + - LOG_VOLUME=${LOG_VOLUME:-counter_logs} env_file: - .env.development depends_on: @@ -28,6 +13,4 @@ services: condition: service_healthy volumes: - ./frontend/build:/app/frontend/build - -volumes: - postgres_data: + - ${LOG_VOLUME:-counter_logs}:/app/logs diff --git a/env.example b/env.example index ec9dc63..59575ac 100644 --- a/env.example +++ b/env.example @@ -6,7 +6,13 @@ JWT_SECRET=your-super-secret-jwt-key-change-in-production # Server Configuration PORT=8080 +METRICS_PORT=9090 GIN_MODE=release +# Logging Configuration +LOG_LEVEL=info +LOG_DIR=/app/logs +LOG_VOLUME=counter_logs + # Frontend Configuration (for development) REACT_APP_API_URL=http://localhost:8080/api/v1 diff --git a/go.mod b/go.mod index 7bc25ff..3276500 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/go.sum b/go.sum index cf7b44c..ed4059c 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -104,6 +106,7 @@ 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/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..a8455f6 --- /dev/null +++ b/logger.go @@ -0,0 +1,105 @@ +package main + +import ( + "io" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// Logger is the global logger instance +var Logger *logrus.Logger + +// InitLogger initializes the structured logger with file output +func InitLogger(config *Config) error { + Logger = logrus.New() + + // Set log level based on configuration + level, err := logrus.ParseLevel(config.LogLevel) + if err != nil { + level = logrus.InfoLevel + } + Logger.SetLevel(level) + + // Set JSON formatter for structured logging + Logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + FieldMap: logrus.FieldMap{ + logrus.FieldKeyTime: "timestamp", + logrus.FieldKeyLevel: "level", + logrus.FieldKeyMsg: "message", + }, + }) + + // Create log directory if it doesn't exist + if err := os.MkdirAll(config.LogDir, 0755); err != nil { + return err + } + + // Create log file with timestamp + logFile := filepath.Join(config.LogDir, "app.log") + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return err + } + + // Set output to both file and stdout + multiWriter := io.MultiWriter(os.Stdout, file) + Logger.SetOutput(multiWriter) + + // Add default fields + Logger = Logger.WithFields(logrus.Fields{ + "service": "counter-app", + "environment": string(config.Environment), + "version": "1.0.0", + }).Logger + + // Log initialization + Logger.Info("Logger initialized successfully") + + return nil +} + +// GetLogger returns the global logger instance +func GetLogger() *logrus.Logger { + if Logger == nil { + // Fallback to standard logger if not initialized + logrus.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: time.RFC3339, + }) + return logrus.StandardLogger() + } + return Logger +} + +// LoggingMiddleware creates a Gin middleware for HTTP request logging +func LoggingMiddleware() gin.HandlerFunc { + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + // Create structured log entry + entry := Logger.WithFields(logrus.Fields{ + "method": param.Method, + "path": param.Path, + "status": param.StatusCode, + "latency": param.Latency.String(), + "client_ip": param.ClientIP, + "user_agent": param.Request.UserAgent(), + "timestamp": param.TimeStamp.Format(time.RFC3339), + }) + + // Set log level based on status code + switch { + case param.StatusCode >= 500: + entry.Error("HTTP Request") + case param.StatusCode >= 400: + entry.Warn("HTTP Request") + default: + entry.Info("HTTP Request") + } + + // Return empty string since we're handling logging ourselves + return "" + }) +} diff --git a/main.go b/main.go index c6142f0..bc2c65c 100644 --- a/main.go +++ b/main.go @@ -5,12 +5,18 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" ) func main() { // Load configuration with environment file precedence config := LoadConfig() + // Initialize structured logger + if err := InitLogger(config); err != nil { + log.Fatal("Failed to initialize logger:", err) + } + // Set Gin mode based on configuration gin.SetMode(config.GinMode) @@ -19,12 +25,12 @@ func main() { // Initialize database with configuration if err := InitDBWithConfig(config); err != nil { - log.Fatal("Failed to initialize database :", err) + Logger.WithError(err).Fatal("Failed to initialize database") } // Create tables if err := CreateTables(); err != nil { - log.Fatal("Failed to create tables:", err) + Logger.WithError(err).Fatal("Failed to create tables") } // Initialize Prometheus metrics @@ -47,6 +53,9 @@ func main() { // Add metrics middleware r.Use(MetricsMiddleware()) + // Add logging middleware + r.Use(LoggingMiddleware()) + // Health check endpoint r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) @@ -88,16 +97,19 @@ func main() { // Start server port := config.Port - log.Printf("") - log.Printf("🚀 Starting Counter Application Server...") - log.Printf(" 🌐 Listening on: http://localhost:%s", port) - log.Printf(" 📊 Health check: http://localhost:%s/health", port) - log.Printf(" 🔗 API endpoint: http://localhost:%s/api/v1", port) - log.Printf(" 🎨 Frontend: http://localhost:%s/", port) - log.Printf(" 📈 Metrics: http://localhost:%s/metrics", config.MetricsPort) - log.Printf("") - log.Printf("✅ Server is ready and accepting connections!") - log.Printf("") + Logger.WithFields(logrus.Fields{ + "port": port, + "metrics_port": config.MetricsPort, + "log_dir": config.LogDir, + "log_volume": config.LogVolume, + }).Info("🚀 Starting Counter Application Server") - log.Fatal(r.Run(":" + port)) + Logger.WithFields(logrus.Fields{ + "health_url": "http://localhost:" + port + "/health", + "api_url": "http://localhost:" + port + "/api/v1", + "frontend_url": "http://localhost:" + port + "/", + "metrics_url": "http://localhost:" + config.MetricsPort + "/metrics", + }).Info("✅ Server is ready and accepting connections") + + Logger.Fatal(r.Run(":" + port)) }