This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ║")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
go.mod
1
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
|
||||
|
||||
3
go.sum
3
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=
|
||||
|
||||
105
logger.go
Normal file
105
logger.go
Normal file
@@ -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 ""
|
||||
})
|
||||
}
|
||||
38
main.go
38
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user