package main import ( "context" "log/slog" "net/http" "os" "runtime" "time" "github.com/gin-gonic/gin" ) const version = "1.0.34" // SystemInfo holds system information type SystemInfo struct { GoVersion string `json:"go_version"` OS string `json:"os"` Architecture string `json:"architecture"` NumCPU int `json:"num_cpu"` StartTime string `json:"start_time"` } var ( startTime = time.Now() logger *slog.Logger ) // setupLogger configures structured logging with different levels for dev/prod func setupLogger() { var logLevel slog.Level var logFormat string // Determine log level from environment switch os.Getenv("LOG_LEVEL") { case "DEBUG": logLevel = slog.LevelDebug case "INFO": logLevel = slog.LevelInfo case "WARN": logLevel = slog.LevelWarn case "ERROR": logLevel = slog.LevelError default: logLevel = slog.LevelInfo } // Determine log format from environment logFormat = os.Getenv("LOG_FORMAT") if logFormat == "" { logFormat = "json" // Default to JSON for production } // Configure logger based on format var handler slog.Handler opts := &slog.HandlerOptions{ Level: logLevel, AddSource: true, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { // Customize timestamp format if a.Key == slog.TimeKey { return slog.Attr{ Key: slog.TimeKey, Value: slog.StringValue(a.Value.Time().Format("2006-01-02T15:04:05.000Z07:00")), } } return a }, } if logFormat == "text" { handler = slog.NewTextHandler(os.Stdout, opts) } else { handler = slog.NewJSONHandler(os.Stdout, opts) } logger = slog.New(handler) slog.SetDefault(logger) } // logMiddleware creates a structured logging middleware for Gin func logMiddleware() gin.HandlerFunc { return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { // Create structured log entry logger.Info("HTTP Request", "method", param.Method, "path", param.Path, "status", param.StatusCode, "latency", param.Latency, "client_ip", param.ClientIP, "user_agent", param.Request.UserAgent(), "timestamp", param.TimeStamp.Format("2006-01-02T15:04:05.000Z07:00"), ) return "" // Return empty string as we handle logging ourselves }) } // errorLogger logs errors with structured context func ErrorLogger(err error, context ...any) { logger.Error("Application Error", append([]any{"error", err.Error()}, context...)..., ) } func main() { // Setup structured logging setupLogger() logger.Info("Starting application", "version", version, "go_version", runtime.Version(), "os", runtime.GOOS, "architecture", runtime.GOARCH, "num_cpu", runtime.NumCPU(), ) // Set Gin mode gin.SetMode(gin.ReleaseMode) // Create router with custom logger r := gin.New() r.Use(logMiddleware()) r.Use(gin.Recovery()) // Add middleware for CORS r.Use(func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) return } c.Next() }) // Health check endpoint r.GET("/healthz", func(c *gin.Context) { logger.Debug("Health check requested", "client_ip", c.ClientIP()) c.JSON(http.StatusOK, gin.H{ "status": "ok", "version": version, }) }) // Main endpoint r.GET("/", func(c *gin.Context) { logger.Info("Root endpoint accessed", "client_ip", c.ClientIP(), "user_agent", c.Request.UserAgent(), ) c.JSON(http.StatusOK, gin.H{ "message": "Hello, World!", "version": version, }) }) // API endpoints api := r.Group("/api/v1") { api.GET("/info", func(c *gin.Context) { logger.Debug("API info requested", "client_ip", c.ClientIP()) c.JSON(http.StatusOK, gin.H{ "service": "hello-api", "version": version, "status": "running", }) }) api.POST("/echo", func(c *gin.Context) { var request struct { Message string `json:"message"` } if err := c.ShouldBindJSON(&request); err != nil { logger.Warn("Invalid JSON in echo request", "client_ip", c.ClientIP(), "error", err.Error(), ) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid JSON", }) return } logger.Info("Echo request processed", "client_ip", c.ClientIP(), "message_length", len(request.Message), ) c.JSON(http.StatusOK, gin.H{ "echo": request.Message, "version": version, }) }) // Configuration endpoint with detailed logging api.GET("/config", func(c *gin.Context) { logger.Debug("Configuration requested", "client_ip", c.ClientIP()) systemInfo := SystemInfo{ GoVersion: runtime.Version(), OS: runtime.GOOS, Architecture: runtime.GOARCH, NumCPU: runtime.NumCPU(), StartTime: startTime.Format(time.RFC3339), } // Get environment variables (excluding sensitive ones) envVars := make(map[string]string) sensitiveCount := 0 for _, env := range os.Environ() { // Skip sensitive environment variables if !isSensitiveEnvVar(env) { envVars[env] = os.Getenv(env) } else { sensitiveCount++ } } logger.Info("Configuration accessed", "client_ip", c.ClientIP(), "env_vars_count", len(envVars), "sensitive_vars_filtered", sensitiveCount, "uptime", time.Since(startTime).String(), ) c.JSON(http.StatusOK, gin.H{ "version": version, "system_info": systemInfo, "environment": envVars, "uptime": time.Since(startTime).String(), }) }) // New logging endpoint to test log levels api.POST("/log-test", func(c *gin.Context) { var request struct { Level string `json:"level"` Message string `json:"message"` } if err := c.ShouldBindJSON(&request); err != nil { logger.Warn("Invalid JSON in log-test request", "client_ip", c.ClientIP(), "error", err.Error(), ) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid JSON", }) return } // Log based on requested level switch request.Level { case "debug": logger.Debug(request.Message, "client_ip", c.ClientIP()) case "info": logger.Info(request.Message, "client_ip", c.ClientIP()) case "warn": logger.Warn(request.Message, "client_ip", c.ClientIP()) case "error": logger.Error(request.Message, "client_ip", c.ClientIP()) default: logger.Info(request.Message, "client_ip", c.ClientIP(), "level", request.Level) } c.JSON(http.StatusOK, gin.H{ "status": "logged", "level": request.Level, "message": request.Message, }) }) } // Get port from environment or use default port := os.Getenv("PORT") if port == "" { port = "8080" } // Start server with structured logging logger.Info("Server starting", "port", port, "mode", gin.Mode(), ) ctx := context.Background() go func() { // Graceful shutdown handling <-ctx.Done() logger.Info("Shutdown signal received, stopping server...") }() err := r.Run(":" + port) if err != nil { logger.Error("Server failed to start", "error", err.Error()) os.Exit(1) } } // isSensitiveEnvVar checks if an environment variable contains sensitive information func isSensitiveEnvVar(env string) bool { sensitivePrefixes := []string{ "PASSWORD", "SECRET", "KEY", "TOKEN", "CREDENTIAL", "AWS_", "GITHUB_", "DOCKER_", "KUBERNETES_", } for _, prefix := range sensitivePrefixes { if len(env) >= len(prefix) && env[:len(prefix)] == prefix { return true } } return false }