diff --git a/api-service/controller/mcp_proxy.go b/api-service/controller/mcp_proxy.go index 8d23bab..a2921ba 100644 --- a/api-service/controller/mcp_proxy.go +++ b/api-service/controller/mcp_proxy.go @@ -19,12 +19,12 @@ package controller import ( "api-service/constants" "io" - "log" "net/http" "net/http/httputil" "net/url" "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) // MCP gateway constant definitions @@ -182,7 +182,7 @@ func (g *MCPGateway) handleMCPSSEWithHeader(c *gin.Context) { // Create request to MCP server req, err := http.NewRequest(MethodGET, targetURL.String(), nil) if err != nil { - log.Printf("Failed to create request: %v", err) + log.Errorf("Failed to create request: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"}) return } @@ -198,19 +198,19 @@ func (g *MCPGateway) handleMCPSSEWithHeader(c *gin.Context) { client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Printf("Failed to connect to MCP server (%s): %v", mcpServerURL, err) + log.Errorf("Failed to connect to MCP server (%s): %v", mcpServerURL, err) c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to connect to MCP server"}) return } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() // Check response status if resp.StatusCode != http.StatusOK { - log.Printf("MCP server (%s) returned status: %d", mcpServerURL, resp.StatusCode) + log.Warnf("MCP server (%s) returned status: %d", mcpServerURL, resp.StatusCode) c.JSON(resp.StatusCode, gin.H{"error": "MCP server error"}) return } @@ -233,7 +233,7 @@ func (g *MCPGateway) handleMCPSSEWithHeader(c *gin.Context) { n, err := resp.Body.Read(buf) if err != nil { if err != io.EOF { - log.Printf("Error reading from MCP server (%s): %v", mcpServerURL, err) + log.Errorf("Error reading from MCP server (%s): %v", mcpServerURL, err) } break } @@ -241,7 +241,7 @@ func (g *MCPGateway) handleMCPSSEWithHeader(c *gin.Context) { if n > 0 { _, writeErr := c.Writer.Write(buf[:n]) if writeErr != nil { - log.Printf("Error writing to client: %v", writeErr) + log.Errorf("Error writing to client: %v", writeErr) break } @@ -285,7 +285,7 @@ func (g *MCPGateway) handleMCPHTTPWithHeader(c *gin.Context) { // Error handling proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - log.Printf("Proxy error for server %s: %v", mcpServerURL, err) + log.Errorf("Proxy error for server %s: %v", mcpServerURL, err) c.JSON(http.StatusBadGateway, gin.H{ "error": "Failed to forward request to MCP server", "details": err.Error(), diff --git a/api-service/go.mod b/api-service/go.mod index f5c6a3b..079c9c0 100644 --- a/api-service/go.mod +++ b/api-service/go.mod @@ -11,7 +11,6 @@ require ( github.com/prometheus/client_golang v1.14.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.6-0.20200504143853-81378bbcd8a1 - go.uber.org/zap v1.27.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -47,7 +46,6 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.13.0 // indirect golang.org/x/net v0.15.0 // indirect diff --git a/api-service/main.go b/api-service/main.go index a697e7c..3d8e0f0 100644 --- a/api-service/main.go +++ b/api-service/main.go @@ -18,11 +18,12 @@ limitations under the License. package main import ( - "log" "net/http" "runtime" "time" + log "github.com/sirupsen/logrus" + "api-service/controller" "api-service/metrics" "api-service/middleware" @@ -76,14 +77,9 @@ func main() { // Register global metrics middleware mainRouter.Use(middleware.MetricsMiddleware()) - // Initialize logger - logger := middleware.InitLogger("") - defer func() { - if err := logger.Sync(); err != nil { - log.Printf("Failed to sync logger: %v", err) - } - }() - mainRouter.Use(middleware.LoggingMiddleware(logger)) + // Initialize logger (logrus + lumberjack) + middleware.InitLogger("", "info") + mainRouter.Use(middleware.LoggingMiddleware()) // Main route configuration var redisClient *service.RedisClient = nil if redisAddr != "" { @@ -138,10 +134,12 @@ func main() { mainRouter.GET("/metrics", gin.WrapH(promhttp.Handler())) // MCP dedicated routing engine - mcpLogger := middleware.InitLogger("/home/admin/logs/api-service-mcp.log") + // Note: MCP uses the same logrus global logger (writes to same log file) + // since logrus is a global singleton. For separate MCP log files, + // use a dedicated logrus instance in the future. mcpRouter := gin.Default() mcpRouter.Use(middleware.MetricsMiddleware()) - mcpRouter.Use(middleware.LoggingMiddleware(mcpLogger)) + mcpRouter.Use(middleware.LoggingMiddleware()) mcpGroup := mcpRouter.Group("/") controller.NewMCPGateway(mcpGroup) diff --git a/api-service/middleware/logging.go b/api-service/middleware/logging.go index a16480e..f2f1a10 100644 --- a/api-service/middleware/logging.go +++ b/api-service/middleware/logging.go @@ -20,44 +20,41 @@ import ( "api-service/constants" "bytes" "io" - "net/http" - "os" "time" "github.com/gin-gonic/gin" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" + log "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" ) -// InitLogger initializes zap logger with log rotation -func InitLogger(logPath string) *zap.Logger { +const maxBodyLogSize = 2048 // 2KB body truncation limit + +// InitLogger initializes logrus with lumberjack log rotation. +// logPath: log file path, empty means default /home/admin/logs/api-service.log +// logLevel: log level string (debug, info, warn, error), empty means info +func InitLogger(logPath, logLevel string) { if logPath == "" { logPath = "/home/admin/logs/api-service.log" } - // Console encoder - consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) - // File encoder (JSON format) - fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - // Lumberjack log rotation configuration - logWriter := zapcore.AddSync(&lumberjack.Logger{ - Filename: logPath, // Log file path - MaxSize: 100, // Maximum size of each log file (MB) - MaxBackups: 30, // Maximum number of old files to retain - MaxAge: 0, // Maximum age of old files in days (0 means permanent) - Compress: false, // Whether to compress old files + + lv, err := log.ParseLevel(logLevel) + if err != nil { + lv = log.InfoLevel + } + log.SetLevel(lv) + + log.SetFormatter(&log.TextFormatter{ + TimestampFormat: "2006-01-02 15:04:05.000", + FullTimestamp: true, + }) + + log.SetOutput(&lumberjack.Logger{ + Filename: logPath, + MaxSize: 500, // megabytes + MaxBackups: 10, + MaxAge: 7, // days + Compress: false, }) - // Console output (stdout) - consoleDebugging := zapcore.Lock(os.Stdout) - consoleEncoderConfig := zap.NewDevelopmentEncoderConfig() - consoleEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder - consoleCore := zapcore.NewCore(consoleEncoder, consoleDebugging, zapcore.DebugLevel) - // File output - fileCore := zapcore.NewCore(fileEncoder, logWriter, zapcore.DebugLevel) - // Merge multiple cores (dual write) - core := zapcore.NewTee(consoleCore, fileCore) - logger := zap.New(core, zap.AddCaller(), zap.Development()) - return logger } // ResponseWriter is a custom ResponseWriter for capturing response body @@ -78,8 +75,8 @@ func (w ResponseWriter) WriteString(s string) (int, error) { return w.ResponseWriter.WriteString(s) } -// LoggingMiddleware creates logging middleware -func LoggingMiddleware(logger *zap.Logger) gin.HandlerFunc { +// LoggingMiddleware creates logging middleware using logrus +func LoggingMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Filter health check path, don't log @@ -114,42 +111,49 @@ func LoggingMiddleware(logger *zap.Logger) gin.HandlerFunc { // Get response status code statusCode := c.Writer.Status() - // log redirect forward pod name and ip - safeHeaders := http.Header{} - safeHeaders.Set(constants.HeaderMCPServerURL, c.Request.Header.Get(constants.HeaderMCPServerURL)) - safeHeaders.Set(constants.HeaderEnvInstanceID, c.Request.Header.Get(constants.HeaderEnvInstanceID)) - // Log - fields := []zap.Field{ - zap.String("method", c.Request.Method), - zap.String("path", c.Request.URL.Path), - zap.Any("header", safeHeaders), - zap.Int("status", statusCode), - zap.Duration("latency", latency), - zap.String("client_ip", c.ClientIP()), + // Build log fields + fields := log.Fields{ + "method": c.Request.Method, + "path": c.Request.URL.Path, + "status": statusCode, + "latency": latency.String(), + "client_ip": c.ClientIP(), + "mcp_url": c.Request.Header.Get(constants.HeaderMCPServerURL), + "inst_id": c.Request.Header.Get(constants.HeaderEnvInstanceID), } - // Add request body (if exists) + // Add request body (truncated) if len(reqBody) > 0 { - fields = append(fields, zap.String("request_body", string(reqBody))) + fields["request_body"] = truncateString(string(reqBody), maxBodyLogSize) } - // Add response body (if exists) + // Add response body (truncated) if blw.body.Len() > 0 { - fields = append(fields, zap.String("response_body", blw.body.String())) + fields["response_body"] = truncateString(blw.body.String(), maxBodyLogSize) } // Log error information (if any) if len(c.Errors) > 0 { - fields = append(fields, zap.String("error", c.Errors.ByType(gin.ErrorTypePrivate).String())) + fields["error"] = c.Errors.ByType(gin.ErrorTypePrivate).String() } + entry := log.WithFields(fields) + // Determine log level based on status code if statusCode >= 500 { - logger.Error("API Error", fields...) + entry.Error("API Error") } else if statusCode >= 400 { - logger.Warn("API Warning", fields...) + entry.Warn("API Warning") } else { - logger.Info("API Access", fields...) + entry.Info("API Access") } } } + +// truncateString truncates a string to maxLen bytes +func truncateString(s string, maxLen int) string { + if len(s) > maxLen { + return s[:maxLen] + "...(truncated)" + } + return s +} diff --git a/api-service/service/backend_client.go b/api-service/service/backend_client.go index c8abcac..af952e8 100644 --- a/api-service/service/backend_client.go +++ b/api-service/service/backend_client.go @@ -133,7 +133,7 @@ func (c *BackendClient) GetEnvByVersion(name, version string) (*backendmodel.Env } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -183,7 +183,7 @@ func (c *BackendClient) ValidateToken(token string) (*backendmodel.Token, error) } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -226,7 +226,7 @@ func (c *BackendClient) SearchDatasource(scenario, key string) (string, error) { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() diff --git a/api-service/service/cleanup_service.go b/api-service/service/cleanup_service.go index db957f4..5af8614 100644 --- a/api-service/service/cleanup_service.go +++ b/api-service/service/cleanup_service.go @@ -19,8 +19,9 @@ package service import ( "api-service/models" "context" - "log" "time" + + log "github.com/sirupsen/logrus" ) type AEnvCleanManager struct { @@ -60,7 +61,7 @@ func (cm *AEnvCleanManager) WithMetrics(incrementSuccess, incrementFailure func( // Start starts the cleanup service func (cm *AEnvCleanManager) Start() { - log.Printf("Starting cleanup service with interval: %v", cm.interval) + log.Infof("Starting cleanup service with interval: %v", cm.interval) // Execute cleanup immediately cm.performCleanup() @@ -73,7 +74,7 @@ func (cm *AEnvCleanManager) Start() { case <-ticker.C: cm.performCleanup() case <-cm.ctx.Done(): - log.Println("Cleanup service stopped") + log.Info("Cleanup service stopped") return } } @@ -82,17 +83,17 @@ func (cm *AEnvCleanManager) Start() { // performCleanup performs the actual cleanup task by checking TTL expiration func (cm *AEnvCleanManager) performCleanup() { - log.Println("Starting TTL-based cleanup task...") + log.Debug("Starting TTL-based cleanup task...") // Get all environment instances envInstances, err := cm.envInstanceService.ListEnvInstances("") if err != nil { - log.Printf("Failed to list environment instances: %v", err) + log.Errorf("Failed to list environment instances: %v", err) return } if len(envInstances) == 0 { - log.Println("No environment instances found") + log.Debug("No environment instances found") return } @@ -107,20 +108,20 @@ func (cm *AEnvCleanManager) performCleanup() { // Check if TTL is set and has expired if cm.isExpired(instance) { - log.Printf("Instance %s has expired (TTL: %s), deleting...", instance.ID, instance.TTL) + log.Infof("Instance %s has expired (TTL: %s), deleting...", instance.ID, instance.TTL) err := cm.envInstanceService.DeleteEnvInstance(instance.ID) if err != nil { - log.Printf("Failed to delete expired instance %s: %v", instance.ID, err) + log.Errorf("Failed to delete expired instance %s: %v", instance.ID, err) cm.incrementCleanupFailure() continue } deletedCount++ cm.incrementCleanupSuccess() - log.Printf("Successfully deleted expired instance %s", instance.ID) + log.Infof("Successfully deleted expired instance %s", instance.ID) } } - log.Printf("TTL-based cleanup task completed. Deleted %d expired instances", deletedCount) + log.Infof("TTL-based cleanup task completed. Deleted %d expired instances", deletedCount) } // isExpired checks if an environment instance has expired based on its TTL and creation time @@ -133,7 +134,7 @@ func (cm *AEnvCleanManager) isExpired(instance *models.EnvInstance) bool { // Parse TTL duration ttlDuration, err := time.ParseDuration(instance.TTL) if err != nil { - log.Printf("Failed to parse TTL '%s' for instance %s: %v", instance.TTL, instance.ID, err) + log.Warnf("Failed to parse TTL '%s' for instance %s: %v", instance.TTL, instance.ID, err) return false } @@ -143,7 +144,7 @@ func (cm *AEnvCleanManager) isExpired(instance *models.EnvInstance) bool { // Fallback to RFC3339 if DateTime parsing fails createdAt, err = time.Parse(time.RFC3339, instance.CreatedAt) if err != nil { - log.Printf("Failed to parse creation time '%s' for instance %s: %v", instance.CreatedAt, instance.ID, err) + log.Warnf("Failed to parse creation time '%s' for instance %s: %v", instance.CreatedAt, instance.ID, err) return false } } diff --git a/api-service/service/env_instance.go b/api-service/service/env_instance.go index 18be8a4..5de87cf 100644 --- a/api-service/service/env_instance.go +++ b/api-service/service/env_instance.go @@ -1,15 +1,17 @@ package service import ( - "api-service/models" "bytes" "encoding/json" - backend "envhub/models" "fmt" "io" - "log" "net/http" "time" + + "api-service/models" + backend "envhub/models" + + log "github.com/sirupsen/logrus" ) const AEnvOpenAPIPrefix = "aenvironment/" @@ -66,7 +68,7 @@ func (c *EnvInstanceClient) CreateEnvInstance(req *backend.Env) (*models.EnvInst } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -118,7 +120,7 @@ func (c *EnvInstanceClient) GetEnvInstance(id string) (*models.EnvInstance, erro } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -169,7 +171,7 @@ func (c *EnvInstanceClient) DeleteEnvInstance(id string) error { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -221,7 +223,7 @@ func (c *EnvInstanceClient) ListEnvInstances(envName string) ([]*models.EnvInsta } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -272,7 +274,7 @@ func (c *EnvInstanceClient) Warmup(req *backend.Env) error { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -323,7 +325,7 @@ func (c *EnvInstanceClient) Cleanup() error { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() diff --git a/api-service/service/redis.go b/api-service/service/redis.go index e0b4910..82ee5ef 100644 --- a/api-service/service/redis.go +++ b/api-service/service/redis.go @@ -22,9 +22,10 @@ import ( "context" "encoding/json" "fmt" - "log" "strings" + log "github.com/sirupsen/logrus" + "github.com/go-redis/redis/v8" ) @@ -51,7 +52,7 @@ func InitRedis(addr, password string) *RedisClient { log.Fatal("Failed to connect to Redis:", err) } - log.Println("Connected to Redis") + log.Info("Connected to Redis") // Assign to global RedisClient (struct) RedisClientInstance = &RedisClient{ @@ -163,23 +164,23 @@ func (r *RedisClient) ListEnvInstancesFromRedis(token string, envInstance *model for i, key := range keys { val := values[i] if val == nil { - log.Printf("Key %s has nil value, skipping", key) + log.Warnf("Key %s has nil value, skipping", key) continue } valueStr, ok := val.(string) if !ok { - log.Printf("Key %s value is not string type, skipping", key) + log.Warnf("Key %s value is not string type, skipping", key) continue } var instance models.EnvInstance if err := json.Unmarshal([]byte(valueStr), &instance); err != nil { - log.Printf("Failed to unmarshal value for key %s: %v", key, err) + log.Errorf("Failed to unmarshal value for key %s: %v", key, err) continue } instances = append(instances, instance) } - log.Printf("Found %d EnvInstance(s) matching pattern: %s", len(instances), pattern) + log.Debugf("Found %d EnvInstance(s) matching pattern: %s", len(instances), pattern) return instances, nil } @@ -197,10 +198,10 @@ func (r *RedisClient) deleteKeysByScan(prefix, suffix string) error { if len(keys) > 0 { if _, err := r.client.Del(r.ctx, keys...).Result(); err != nil { - log.Printf("Failed to delete some keys: %v", err) + log.Errorf("Failed to delete some keys: %v", err) } else { deletedCount += int64(len(keys)) - log.Printf("Deleted keys: %v", keys) + log.Debugf("Deleted keys: %v", keys) } } @@ -209,6 +210,6 @@ func (r *RedisClient) deleteKeysByScan(prefix, suffix string) error { } } - log.Printf("Total deleted keys matching '*%s': %d", suffix, deletedCount) + log.Debugf("Total deleted keys matching '*%s': %d", suffix, deletedCount) return nil } diff --git a/api-service/service/schedule_client.go b/api-service/service/schedule_client.go index 327f28c..02e7417 100644 --- a/api-service/service/schedule_client.go +++ b/api-service/service/schedule_client.go @@ -66,7 +66,7 @@ func (c *ScheduleClient) CreatePod(req *backend.Env) (*models.EnvInstance, error } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -106,7 +106,7 @@ func (c *ScheduleClient) GetPod(podName string) (*models.EnvInstance, error) { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -146,7 +146,7 @@ func (c *ScheduleClient) DeletePod(podName string) (bool, error) { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -185,7 +185,7 @@ func (c *ScheduleClient) FilterPods() (*[]models.EnvInstance, error) { } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -236,7 +236,7 @@ func (c *ScheduleClient) CreateService(req *backend.Env) (*models.EnvService, er } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -276,7 +276,7 @@ func (c *ScheduleClient) GetService(serviceName string) (*models.EnvService, err } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -319,7 +319,7 @@ func (c *ScheduleClient) DeleteService(serviceName string, deleteStorage bool) ( } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -370,7 +370,7 @@ func (c *ScheduleClient) UpdateService(serviceName string, updateReq *UpdateServ } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -436,7 +436,7 @@ func (c *ScheduleClient) ListServices(envName string) ([]*models.EnvService, err } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }() @@ -554,7 +554,7 @@ func (c *ScheduleClient) ListEnvInstances(envName string) ([]*models.EnvInstance } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { - log.Printf("failed to close response body: %v", closeErr) + log.Warnf("failed to close response body: %v", closeErr) } }()