319 lines
7.5 KiB
Go
319 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const version = "1.0.32"
|
|
|
|
// 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
|
|
}
|