add monitoring #4

Open
alexandvvvvv wants to merge 13 commits from feature/mntr into main
9 changed files with 154 additions and 32 deletions
Showing only changes of commit 4ae5bf6a81 - Show all commits

View File

@@ -50,6 +50,9 @@ COPY --from=go-builder /app/.env* ./
# Copy the React build from frontend-builder stage # Copy the React build from frontend-builder stage
COPY --from=frontend-builder /app/frontend/build ./frontend/build 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 port
EXPOSE 8080 EXPOSE 8080

View File

@@ -3,5 +3,8 @@ ENVIRONMENT=development
DATABASE_URL=postgres://postgres:password@postgres:5432/counter_db?sslmode=disable DATABASE_URL=postgres://postgres:password@postgres:5432/counter_db?sslmode=disable
JWT_SECRET=dev-secret-key-change-in-production JWT_SECRET=dev-secret-key-change-in-production
PORT=8080 PORT=8080
METRICS_PORT=9090
GIN_MODE=debug GIN_MODE=debug
LOG_LEVEL=debug LOG_LEVEL=debug
LOG_DIR=/app/logs
LOG_VOLUME=counter_logs

View File

@@ -26,6 +26,8 @@ type Config struct {
GinMode string GinMode string
LogLevel string LogLevel string
Debug bool Debug bool
LogDir string
LogVolume string
} }
// LoadConfig loads configuration with proper environment file precedence // LoadConfig loads configuration with proper environment file precedence
@@ -46,6 +48,8 @@ func LoadConfig() *Config {
GinMode: getGinMode(env), GinMode: getGinMode(env),
LogLevel: getLogLevel(env), LogLevel: getLogLevel(env),
Debug: env == Development, Debug: env == Development,
LogDir: getEnv("LOG_DIR", "/app/logs"),
LogVolume: getEnv("LOG_VOLUME", "counter_logs"),
} }
// Log configuration (without sensitive data) // Log configuration (without sensitive data)
@@ -183,6 +187,8 @@ func logConfig(config *Config) {
log.Printf("║ 📊 LOG LEVEL: %-15s ║", config.LogLevel) log.Printf("║ 📊 LOG LEVEL: %-15s ║", config.LogLevel)
log.Printf("║ 🌐 PORT: %-20s ║", config.Port) log.Printf("║ 🌐 PORT: %-20s ║", config.Port)
log.Printf("║ 📈 METRICS PORT: %-15s ║", config.MetricsPort) 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("║ ║")
log.Printf("║ 📁 Configuration Files Loaded: ║") log.Printf("║ 📁 Configuration Files Loaded: ║")
log.Printf("║ • .env (base configuration) ║") log.Printf("║ • .env (base configuration) ║")

View File

@@ -1,26 +1,11 @@
services: 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: app:
build: . build: .
ports: ports:
- "8080:8080" - "8080:8080"
environment: environment:
- ENVIRONMENT=development - ENVIRONMENT=development
- LOG_VOLUME=${LOG_VOLUME:-counter_logs}
env_file: env_file:
- .env.development - .env.development
depends_on: depends_on:
@@ -28,6 +13,4 @@ services:
condition: service_healthy condition: service_healthy
volumes: volumes:
- ./frontend/build:/app/frontend/build - ./frontend/build:/app/frontend/build
- ${LOG_VOLUME:-counter_logs}:/app/logs
volumes:
postgres_data:

View File

@@ -6,7 +6,13 @@ JWT_SECRET=your-super-secret-jwt-key-change-in-production
# Server Configuration # Server Configuration
PORT=8080 PORT=8080
METRICS_PORT=9090
GIN_MODE=release GIN_MODE=release
# Logging Configuration
LOG_LEVEL=info
LOG_DIR=/app/logs
LOG_VOLUME=counter_logs
# Frontend Configuration (for development) # Frontend Configuration (for development)
REACT_APP_API_URL=http://localhost:8080/api/v1 REACT_APP_API_URL=http://localhost:8080/api/v1

1
go.mod
View File

@@ -38,6 +38,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect

3
go.sum
View File

@@ -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/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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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/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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=

105
logger.go Normal file
View 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
View File

@@ -5,12 +5,18 @@ import (
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
) )
func main() { func main() {
// Load configuration with environment file precedence // Load configuration with environment file precedence
config := LoadConfig() config := LoadConfig()
// Initialize structured logger
if err := InitLogger(config); err != nil {
log.Fatal("Failed to initialize logger:", err)
}
// Set Gin mode based on configuration // Set Gin mode based on configuration
gin.SetMode(config.GinMode) gin.SetMode(config.GinMode)
@@ -19,12 +25,12 @@ func main() {
// Initialize database with configuration // Initialize database with configuration
if err := InitDBWithConfig(config); err != nil { if err := InitDBWithConfig(config); err != nil {
log.Fatal("Failed to initialize database :", err) Logger.WithError(err).Fatal("Failed to initialize database")
} }
// Create tables // Create tables
if err := CreateTables(); err != nil { if err := CreateTables(); err != nil {
log.Fatal("Failed to create tables:", err) Logger.WithError(err).Fatal("Failed to create tables")
} }
// Initialize Prometheus metrics // Initialize Prometheus metrics
@@ -47,6 +53,9 @@ func main() {
// Add metrics middleware // Add metrics middleware
r.Use(MetricsMiddleware()) r.Use(MetricsMiddleware())
// Add logging middleware
r.Use(LoggingMiddleware())
// Health check endpoint // Health check endpoint
r.GET("/health", func(c *gin.Context) { r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"}) c.JSON(200, gin.H{"status": "ok"})
@@ -88,16 +97,19 @@ func main() {
// Start server // Start server
port := config.Port port := config.Port
log.Printf("") Logger.WithFields(logrus.Fields{
log.Printf("🚀 Starting Counter Application Server...") "port": port,
log.Printf(" 🌐 Listening on: http://localhost:%s", port) "metrics_port": config.MetricsPort,
log.Printf(" 📊 Health check: http://localhost:%s/health", port) "log_dir": config.LogDir,
log.Printf(" 🔗 API endpoint: http://localhost:%s/api/v1", port) "log_volume": config.LogVolume,
log.Printf(" 🎨 Frontend: http://localhost:%s/", port) }).Info("🚀 Starting Counter Application Server")
log.Printf(" 📈 Metrics: http://localhost:%s/metrics", config.MetricsPort)
log.Printf("")
log.Printf("✅ Server is ready and accepting connections!")
log.Printf("")
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))
} }