feat(logging): Implement comprehensive logging system with file rotation and container log management
- Introduce a logging configuration structure in the config file to enable file-based logging with customizable settings. - Implement a log manager to handle log collection for individual containers, including starting and stopping log collection. - Enhance the logging setup to support different log levels and file rotation using the lumberjack package. - Update the application to initialize logging based on the configuration and ensure secure permissions for log files. - Add logging examples in configuration files for development, production, and minimal setups. - Improve request logging in middleware to utilize the new logging system for better traceability.
- commit
- 33eb2a5acf72ac108fa5eed338d9d234f1ce7bfc
- author
- bamen · · 2025-06-08 09:57 UTC
- parent
- 57617f5
- signature
- unsigned
14 files · +812 -46
M cmd/start.go +19 -7
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "gordon/internal/config" | |
| 14 | 14 | "gordon/internal/container" | |
| 15 | 15 | "gordon/internal/events" | |
| 16 | + | "gordon/internal/logging" | |
| 16 | 17 | "gordon/internal/proxy" | |
| 17 | 18 | "gordon/internal/registry" | |
| 18 | 19 | ||
| @@ -35,15 +36,21 @@ func init() { | |||
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | 38 | func runStart(cmd *cobra.Command, args []string) { | |
| 38 | - | // Configure zerolog | |
| 39 | - | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |
| 40 | - | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |
| 41 | - | ||
| 39 | + | // Load configuration first | |
| 42 | 40 | cfg, err := config.Load() | |
| 43 | 41 | if err != nil { | |
| 42 | + | // Use basic console logging for config errors | |
| 43 | + | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |
| 44 | + | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |
| 44 | 45 | log.Fatal().Err(err).Msg("Failed to load config") | |
| 45 | 46 | } | |
| 46 | 47 | ||
| 48 | + | // Initialize logging system | |
| 49 | + | if err := logging.Setup(cfg); err != nil { | |
| 50 | + | log.Fatal().Err(err).Msg("Failed to initialize logging") | |
| 51 | + | } | |
| 52 | + | defer logging.Close() | |
| 53 | + | ||
| 47 | 54 | log.Info().Msg("Starting Gordon server...") | |
| 48 | 55 | log.Info().Int("registry_port", cfg.Server.RegistryPort).Msg("Registry server") | |
| 49 | 56 | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | |
| @@ -123,6 +130,11 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 123 | 130 | // Update the configuration reference | |
| 124 | 131 | cfg = newCfg | |
| 125 | 132 | ||
| 133 | + | // Reinitialize logging with new config | |
| 134 | + | if err := logging.Setup(cfg); err != nil { | |
| 135 | + | log.Error().Err(err).Msg("Failed to reinitialize logging after config reload") | |
| 136 | + | } | |
| 137 | + | ||
| 126 | 138 | // Update proxy server config | |
| 127 | 139 | proxyServer.UpdateConfig(cfg) | |
| 128 | 140 | ||
| @@ -237,9 +249,9 @@ shutdown: | |||
| 237 | 249 | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| 238 | 250 | defer shutdownCancel() | |
| 239 | 251 | ||
| 240 | - | log.Info().Msg("Stopping all managed containers...") | |
| 241 | - | if err := manager.StopAllManagedContainers(shutdownCtx); err != nil { | |
| 242 | - | log.Error().Err(err).Msg("Failed to stop managed containers") | |
| 252 | + | log.Info().Msg("Shutting down container manager...") | |
| 253 | + | if err := manager.Shutdown(shutdownCtx); err != nil { | |
| 254 | + | log.Error().Err(err).Msg("Failed to shutdown container manager") | |
| 243 | 255 | } | |
| 244 | 256 | ||
| 245 | 257 | // Stop event bus | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "gordon/internal/config" | 13 | "gordon/internal/config" | ||
| 14 | "gordon/internal/container" | 14 | "gordon/internal/container" | ||
| 15 | "gordon/internal/events" | 15 | "gordon/internal/events" | ||
| 16 | + | "gordon/internal/logging" | |||
| 16 | "gordon/internal/proxy" | 17 | "gordon/internal/proxy" | ||
| 17 | "gordon/internal/registry" | 18 | "gordon/internal/registry" | ||
| 18 | 19 | ||||
| @@ -35,15 +36,21 @@ func init() { | |||||
| 35 | } | 36 | } | ||
| 36 | 37 | ||||
| 37 | func runStart(cmd *cobra.Command, args []string) { | 38 | func runStart(cmd *cobra.Command, args []string) { | ||
| 38 | - | // Configure zerolog | 39 | + | // Load configuration first |
| 39 | - | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |||
| 40 | - | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |||
| 41 | - | ||||
| 42 | cfg, err := config.Load() | 40 | cfg, err := config.Load() | ||
| 43 | if err != nil { | 41 | if err != nil { | ||
| 42 | + | // Use basic console logging for config errors | |||
| 43 | + | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |||
| 44 | + | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |||
| 44 | log.Fatal().Err(err).Msg("Failed to load config") | 45 | log.Fatal().Err(err).Msg("Failed to load config") | ||
| 45 | } | 46 | } | ||
| 46 | 47 | ||||
| 48 | + | // Initialize logging system | |||
| 49 | + | if err := logging.Setup(cfg); err != nil { | |||
| 50 | + | log.Fatal().Err(err).Msg("Failed to initialize logging") | |||
| 51 | + | } | |||
| 52 | + | defer logging.Close() | |||
| 53 | + | ||||
| 47 | log.Info().Msg("Starting Gordon server...") | 54 | log.Info().Msg("Starting Gordon server...") | ||
| 48 | log.Info().Int("registry_port", cfg.Server.RegistryPort).Msg("Registry server") | 55 | log.Info().Int("registry_port", cfg.Server.RegistryPort).Msg("Registry server") | ||
| 49 | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | 56 | log.Info().Int("proxy_port", cfg.Server.Port).Msg("Proxy server") | ||
| @@ -123,6 +130,11 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 123 | // Update the configuration reference | 130 | // Update the configuration reference | ||
| 124 | cfg = newCfg | 131 | cfg = newCfg | ||
| 125 | 132 | ||||
| 133 | + | // Reinitialize logging with new config | |||
| 134 | + | if err := logging.Setup(cfg); err != nil { | |||
| 135 | + | log.Error().Err(err).Msg("Failed to reinitialize logging after config reload") | |||
| 136 | + | } | |||
| 137 | + | ||||
| 126 | // Update proxy server config | 138 | // Update proxy server config | ||
| 127 | proxyServer.UpdateConfig(cfg) | 139 | proxyServer.UpdateConfig(cfg) | ||
| 128 | 140 | ||||
| @@ -237,9 +249,9 @@ shutdown: | |||||
| 237 | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) | 249 | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| 238 | defer shutdownCancel() | 250 | defer shutdownCancel() | ||
| 239 | 251 | ||||
| 240 | - | log.Info().Msg("Stopping all managed containers...") | 252 | + | log.Info().Msg("Shutting down container manager...") |
| 241 | - | if err := manager.StopAllManagedContainers(shutdownCtx); err != nil { | 253 | + | if err := manager.Shutdown(shutdownCtx); err != nil { |
| 242 | - | log.Error().Err(err).Msg("Failed to stop managed containers") | 254 | + | log.Error().Err(err).Msg("Failed to shutdown container manager") |
| 243 | } | 255 | } | ||
| 244 | 256 | ||||
| 245 | // Stop event bus | 257 | // Stop event bus | ||
M examples/README.md +27 -0
| @@ -39,6 +39,13 @@ This directory contains configuration examples for different use cases and envir | |||
| 39 | 39 | - Shared application architecture | |
| 40 | 40 | - Enterprise features | |
| 41 | 41 | ||
| 42 | + | ### 📊 [`logging.toml`](logging.toml) | |
| 43 | + | **Comprehensive logging configuration** | |
| 44 | + | - Complete logging setup examples | |
| 45 | + | - Different logging levels and configurations | |
| 46 | + | - Production and development logging strategies | |
| 47 | + | - Log rotation and monitoring examples | |
| 48 | + | ||
| 42 | 49 | ## 🚀 Quick Start | |
| 43 | 50 | ||
| 44 | 51 | 1. **Choose an example** that matches your use case | |
| @@ -96,6 +103,26 @@ export CONTAINER_HOST=tcp://remote-docker:2376 | |||
| 96 | 103 | gordon start | |
| 97 | 104 | ``` | |
| 98 | 105 | ||
| 106 | + | ### Logging Configuration | |
| 107 | + | Enable comprehensive logging to monitor your deployments: | |
| 108 | + | ```toml | |
| 109 | + | [logging] | |
| 110 | + | enabled = true # Enable file-based logging | |
| 111 | + | level = "info" # Log level: trace, debug, info, warn, error | |
| 112 | + | dir = "./logs" # Directory for log files | |
| 113 | + | main_log_file = "gordon.log" # Main application logs | |
| 114 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |
| 115 | + | container_log_dir = "containers" # Container logs subdirectory | |
| 116 | + | ||
| 117 | + | # Log rotation settings | |
| 118 | + | max_size = 100 # Max log file size in MB | |
| 119 | + | max_backups = 5 # Number of old log files to keep | |
| 120 | + | max_age = 30 # Max age in days | |
| 121 | + | compress = true # Compress old log files | |
| 122 | + | ``` | |
| 123 | + | ||
| 124 | + | See [`logging.toml`](logging.toml) for comprehensive logging examples. | |
| 125 | + | ||
| 99 | 126 | ### Environment Variables | |
| 100 | 127 | For production, use environment variables for sensitive data: | |
| 101 | 128 | ```bash | |
| @@ -39,6 +39,13 @@ This directory contains configuration examples for different use cases and envir | |||||
| 39 | - Shared application architecture | 39 | - Shared application architecture | ||
| 40 | - Enterprise features | 40 | - Enterprise features | ||
| 41 | 41 | ||||
| 42 | + | ### 📊 [`logging.toml`](logging.toml) | |||
| 43 | + | **Comprehensive logging configuration** | |||
| 44 | + | - Complete logging setup examples | |||
| 45 | + | - Different logging levels and configurations | |||
| 46 | + | - Production and development logging strategies | |||
| 47 | + | - Log rotation and monitoring examples | |||
| 48 | + | ||||
| 42 | ## 🚀 Quick Start | 49 | ## 🚀 Quick Start | ||
| 43 | 50 | ||||
| 44 | 1. **Choose an example** that matches your use case | 51 | 1. **Choose an example** that matches your use case | ||
| @@ -96,6 +103,26 @@ export CONTAINER_HOST=tcp://remote-docker:2376 | |||||
| 96 | gordon start | 103 | gordon start | ||
| 97 | ``` | 104 | ``` | ||
| 98 | 105 | ||||
| 106 | + | ### Logging Configuration | |||
| 107 | + | Enable comprehensive logging to monitor your deployments: | |||
| 108 | + | ```toml | |||
| 109 | + | [logging] | |||
| 110 | + | enabled = true # Enable file-based logging | |||
| 111 | + | level = "info" # Log level: trace, debug, info, warn, error | |||
| 112 | + | dir = "./logs" # Directory for log files | |||
| 113 | + | main_log_file = "gordon.log" # Main application logs | |||
| 114 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |||
| 115 | + | container_log_dir = "containers" # Container logs subdirectory | |||
| 116 | + | ||||
| 117 | + | # Log rotation settings | |||
| 118 | + | max_size = 100 # Max log file size in MB | |||
| 119 | + | max_backups = 5 # Number of old log files to keep | |||
| 120 | + | max_age = 30 # Max age in days | |||
| 121 | + | compress = true # Compress old log files | |||
| 122 | + | ``` | |||
| 123 | + | ||||
| 124 | + | See [`logging.toml`](logging.toml) for comprehensive logging examples. | |||
| 125 | + | ||||
| 99 | ### Environment Variables | 126 | ### Environment Variables | ||
| 100 | For production, use environment variables for sensitive data: | 127 | For production, use environment variables for sensitive data: | ||
| 101 | ```bash | 128 | ```bash | ||
M examples/development.toml +15 -0
| @@ -15,6 +15,21 @@ enabled = false # Disable auth for easier local development | |||
| 15 | 15 | [auto_route] | |
| 16 | 16 | enabled = true # Enable automatic route creation from image names | |
| 17 | 17 | ||
| 18 | + | # Logging configuration for development | |
| 19 | + | [logging] | |
| 20 | + | enabled = true # Enable file-based logging | |
| 21 | + | level = "debug" # Verbose logging for development | |
| 22 | + | dir = "./dev-data/logs" # Store logs in dev data directory | |
| 23 | + | main_log_file = "gordon.log" # Main application logs | |
| 24 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |
| 25 | + | container_log_dir = "containers" # Container logs subdirectory | |
| 26 | + | ||
| 27 | + | # Log rotation settings - smaller for development | |
| 28 | + | max_size = 10 # Max 10MB per log file | |
| 29 | + | max_backups = 3 # Keep 3 old files | |
| 30 | + | max_age = 7 # Keep logs for 1 week | |
| 31 | + | compress = true # Compress old logs | |
| 32 | + | ||
| 18 | 33 | # Environment variables configuration | |
| 19 | 34 | [env] | |
| 20 | 35 | # Directory where .env files are stored for each route | |
| @@ -15,6 +15,21 @@ enabled = false # Disable auth for easier local development | |||||
| 15 | [auto_route] | 15 | [auto_route] | ||
| 16 | enabled = true # Enable automatic route creation from image names | 16 | enabled = true # Enable automatic route creation from image names | ||
| 17 | 17 | ||||
| 18 | + | # Logging configuration for development | |||
| 19 | + | [logging] | |||
| 20 | + | enabled = true # Enable file-based logging | |||
| 21 | + | level = "debug" # Verbose logging for development | |||
| 22 | + | dir = "./dev-data/logs" # Store logs in dev data directory | |||
| 23 | + | main_log_file = "gordon.log" # Main application logs | |||
| 24 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |||
| 25 | + | container_log_dir = "containers" # Container logs subdirectory | |||
| 26 | + | ||||
| 27 | + | # Log rotation settings - smaller for development | |||
| 28 | + | max_size = 10 # Max 10MB per log file | |||
| 29 | + | max_backups = 3 # Keep 3 old files | |||
| 30 | + | max_age = 7 # Keep logs for 1 week | |||
| 31 | + | compress = true # Compress old logs | |||
| 32 | + | ||||
| 18 | # Environment variables configuration | 33 | # Environment variables configuration | ||
| 19 | [env] | 34 | [env] | ||
| 20 | # Directory where .env files are stored for each route | 35 | # Directory where .env files are stored for each route | ||
A examples/logging.toml +180 -0
| @@ -0,0 +1,180 @@ | |||
| 1 | + | # Gordon v2 - Comprehensive Logging Configuration Example | |
| 2 | + | # Demonstrates all logging features and configurations | |
| 3 | + | ||
| 4 | + | [server] | |
| 5 | + | port = 8080 | |
| 6 | + | registry_port = 5000 | |
| 7 | + | registry_domain = "registry.yourdomain.com" | |
| 8 | + | runtime = "auto" | |
| 9 | + | data_dir = "./data" | |
| 10 | + | ||
| 11 | + | [registry_auth] | |
| 12 | + | enabled = true | |
| 13 | + | username = "admin" | |
| 14 | + | password = "secure-password" | |
| 15 | + | ||
| 16 | + | # ============================================================================ | |
| 17 | + | # LOGGING CONFIGURATION EXAMPLES | |
| 18 | + | # ============================================================================ | |
| 19 | + | ||
| 20 | + | # Basic logging setup - good for most users | |
| 21 | + | [logging] | |
| 22 | + | enabled = true # Enable file-based logging (default: false) | |
| 23 | + | level = "info" # Log level: trace, debug, info, warn, error (default: info) | |
| 24 | + | dir = "./logs" # Directory for log files (default: ./logs) | |
| 25 | + | main_log_file = "gordon.log" # Main application log file (default: gordon.log) | |
| 26 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic log file (default: proxy.log) | |
| 27 | + | container_log_dir = "containers" # Container logs subdirectory (default: containers) | |
| 28 | + | ||
| 29 | + | # Log rotation settings | |
| 30 | + | max_size = 100 # Max log file size in MB before rotation (default: 100) | |
| 31 | + | max_backups = 5 # Number of old log files to keep (default: 5) | |
| 32 | + | max_age = 30 # Max age in days to keep old log files (default: 30) | |
| 33 | + | compress = true # Compress old log files with gzip (default: true) | |
| 34 | + | ||
| 35 | + | # ============================================================================ | |
| 36 | + | # ALTERNATIVE LOGGING CONFIGURATIONS | |
| 37 | + | # ============================================================================ | |
| 38 | + | ||
| 39 | + | # Development logging configuration (uncomment to use) | |
| 40 | + | # [logging] | |
| 41 | + | # enabled = true | |
| 42 | + | # level = "debug" # More verbose for debugging | |
| 43 | + | # dir = "./dev-logs" | |
| 44 | + | # main_log_file = "gordon-dev.log" | |
| 45 | + | # proxy_log_file = "proxy-dev.log" | |
| 46 | + | # container_log_dir = "containers" | |
| 47 | + | # max_size = 10 # Smaller files for development | |
| 48 | + | # max_backups = 3 # Less history needed | |
| 49 | + | # max_age = 7 # Keep logs for 1 week | |
| 50 | + | # compress = false # No compression for faster access | |
| 51 | + | ||
| 52 | + | # Production logging configuration (uncomment to use) | |
| 53 | + | # [logging] | |
| 54 | + | # enabled = true | |
| 55 | + | # level = "warn" # Only warnings and errors in production | |
| 56 | + | # dir = "/var/log/gordon" # System log directory | |
| 57 | + | # main_log_file = "gordon.log" | |
| 58 | + | # proxy_log_file = "proxy.log" | |
| 59 | + | # container_log_dir = "containers" | |
| 60 | + | # max_size = 200 # Larger files for production | |
| 61 | + | # max_backups = 15 # More history for analysis | |
| 62 | + | # max_age = 90 # Keep logs for 3 months | |
| 63 | + | # compress = true # Save disk space | |
| 64 | + | ||
| 65 | + | # High-traffic logging configuration (uncomment to use) | |
| 66 | + | # [logging] | |
| 67 | + | # enabled = true | |
| 68 | + | # level = "error" # Only errors to reduce log volume | |
| 69 | + | # dir = "/var/log/gordon" | |
| 70 | + | # main_log_file = "gordon.log" | |
| 71 | + | # proxy_log_file = "proxy.log" | |
| 72 | + | # container_log_dir = "containers" | |
| 73 | + | # max_size = 500 # Very large files for high traffic | |
| 74 | + | # max_backups = 20 # Extensive history | |
| 75 | + | # max_age = 180 # Keep logs for 6 months | |
| 76 | + | # compress = true # Essential for large files | |
| 77 | + | ||
| 78 | + | # Minimal logging configuration (uncomment to use) | |
| 79 | + | # [logging] | |
| 80 | + | # enabled = true | |
| 81 | + | # level = "error" # Only log errors | |
| 82 | + | # dir = "./logs" | |
| 83 | + | # max_size = 50 # Small files | |
| 84 | + | # max_backups = 2 # Minimal history | |
| 85 | + | # max_age = 14 # 2 weeks retention | |
| 86 | + | # compress = true | |
| 87 | + | ||
| 88 | + | # Disable logging (console output only) - uncomment to use | |
| 89 | + | # [logging] | |
| 90 | + | # enabled = false # Only console output, no files | |
| 91 | + | ||
| 92 | + | # ============================================================================ | |
| 93 | + | # LOG STRUCTURE CREATED BY GORDON | |
| 94 | + | # ============================================================================ | |
| 95 | + | # | |
| 96 | + | # When logging is enabled, Gordon creates this structure: | |
| 97 | + | # | |
| 98 | + | # ./logs/ # Main log directory (permissions: 0700) | |
| 99 | + | # ├── gordon.log # Main application logs (permissions: 0600) | |
| 100 | + | # ├── gordon.log.1.gz # Rotated logs (compressed) | |
| 101 | + | # ├── gordon.log.2.gz | |
| 102 | + | # ├── proxy.log # HTTP proxy traffic logs (permissions: 0600) | |
| 103 | + | # ├── proxy.log.1.gz # Rotated proxy logs | |
| 104 | + | # └── containers/ # Container logs directory (permissions: 0700) | |
| 105 | + | # ├── abc123def456.log # Container logs by ID (permissions: 0600) | |
| 106 | + | # ├── def456ghi789.log | |
| 107 | + | # ├── myapp_yourdomain_com.log # Symlink to container ID for easier access | |
| 108 | + | # └── api_yourdomain_com.log # Symlink to container ID for easier access | |
| 109 | + | # | |
| 110 | + | # ============================================================================ | |
| 111 | + | # WHAT GETS LOGGED | |
| 112 | + | # ============================================================================ | |
| 113 | + | # | |
| 114 | + | # Main Application Logs (gordon.log): | |
| 115 | + | # - Server startup and shutdown events | |
| 116 | + | # - Container lifecycle events (deploy, start, stop, remove) | |
| 117 | + | # - Configuration changes and hot reloads | |
| 118 | + | # - Registry operations (push/pull events) | |
| 119 | + | # - Error conditions and warnings | |
| 120 | + | # - Authentication events | |
| 121 | + | # - Auto-route creation events | |
| 122 | + | # | |
| 123 | + | # Proxy Logs (proxy.log): | |
| 124 | + | # - All HTTP/HTTPS requests through the reverse proxy | |
| 125 | + | # - Request method, URL, response code, response time | |
| 126 | + | # - Client IP addresses and user agents | |
| 127 | + | # - SSL/TLS connection information | |
| 128 | + | # - Upstream container routing decisions | |
| 129 | + | # | |
| 130 | + | # Container Logs (containers/*.log): | |
| 131 | + | # - Real-time stdout/stderr from each container | |
| 132 | + | # - Timestamped application output | |
| 133 | + | # - Container startup and shutdown messages | |
| 134 | + | # - Application errors and debug output | |
| 135 | + | # - Custom application logging | |
| 136 | + | # | |
| 137 | + | # ============================================================================ | |
| 138 | + | # SECURITY FEATURES | |
| 139 | + | # ============================================================================ | |
| 140 | + | # | |
| 141 | + | # - Log directories created with 0700 permissions (owner-only access) | |
| 142 | + | # - Log files created with 0600 permissions (owner read/write only) | |
| 143 | + | # - Automatic log rotation prevents disk space exhaustion | |
| 144 | + | # - Sensitive information (passwords, tokens) is automatically redacted | |
| 145 | + | # - Container logs are isolated per container for security analysis | |
| 146 | + | # | |
| 147 | + | # ============================================================================ | |
| 148 | + | # LOG MONITORING COMMANDS | |
| 149 | + | # ============================================================================ | |
| 150 | + | # | |
| 151 | + | # Real-time monitoring: | |
| 152 | + | # tail -f logs/gordon.log # Watch main application logs | |
| 153 | + | # tail -f logs/proxy.log # Monitor HTTP traffic | |
| 154 | + | # tail -f logs/containers/*.log # Follow all container logs | |
| 155 | + | # | |
| 156 | + | # Log analysis: | |
| 157 | + | # grep -i error logs/gordon.log # Find errors in main logs | |
| 158 | + | # grep "deploy" logs/gordon.log # Find deployment events | |
| 159 | + | # awk '{print $9}' logs/proxy.log | sort | uniq -c # HTTP status code analysis | |
| 160 | + | # grep "container" logs/gordon.log | tail -20 # Recent container events | |
| 161 | + | # | |
| 162 | + | # ============================================================================ | |
| 163 | + | ||
| 164 | + | [routes] | |
| 165 | + | "app.yourdomain.com" = "myapp:latest" | |
| 166 | + | "api.yourdomain.com" = "myapi:latest" | |
| 167 | + | "logs.yourdomain.com" = "logviewer:latest" # Example log viewing service | |
| 168 | + | ||
| 169 | + | # Example environment variables configuration | |
| 170 | + | [env] | |
| 171 | + | dir = "./data/env" | |
| 172 | + | providers = ["pass"] | |
| 173 | + | ||
| 174 | + | # With comprehensive logging enabled, you can: | |
| 175 | + | # 1. Monitor all HTTP traffic to your applications | |
| 176 | + | # 2. Debug container deployment issues | |
| 177 | + | # 3. Analyze application performance | |
| 178 | + | # 4. Track security events and access patterns | |
| 179 | + | # 5. Maintain audit trails for compliance | |
| 180 | + | # 6. Troubleshoot network and routing issues⏎̸ | |
| @@ -0,0 +1,180 @@ | |||||
| 1 | + | # Gordon v2 - Comprehensive Logging Configuration Example | |||
| 2 | + | # Demonstrates all logging features and configurations | |||
| 3 | + | ||||
| 4 | + | [server] | |||
| 5 | + | port = 8080 | |||
| 6 | + | registry_port = 5000 | |||
| 7 | + | registry_domain = "registry.yourdomain.com" | |||
| 8 | + | runtime = "auto" | |||
| 9 | + | data_dir = "./data" | |||
| 10 | + | ||||
| 11 | + | [registry_auth] | |||
| 12 | + | enabled = true | |||
| 13 | + | username = "admin" | |||
| 14 | + | password = "secure-password" | |||
| 15 | + | ||||
| 16 | + | # ============================================================================ | |||
| 17 | + | # LOGGING CONFIGURATION EXAMPLES | |||
| 18 | + | # ============================================================================ | |||
| 19 | + | ||||
| 20 | + | # Basic logging setup - good for most users | |||
| 21 | + | [logging] | |||
| 22 | + | enabled = true # Enable file-based logging (default: false) | |||
| 23 | + | level = "info" # Log level: trace, debug, info, warn, error (default: info) | |||
| 24 | + | dir = "./logs" # Directory for log files (default: ./logs) | |||
| 25 | + | main_log_file = "gordon.log" # Main application log file (default: gordon.log) | |||
| 26 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic log file (default: proxy.log) | |||
| 27 | + | container_log_dir = "containers" # Container logs subdirectory (default: containers) | |||
| 28 | + | ||||
| 29 | + | # Log rotation settings | |||
| 30 | + | max_size = 100 # Max log file size in MB before rotation (default: 100) | |||
| 31 | + | max_backups = 5 # Number of old log files to keep (default: 5) | |||
| 32 | + | max_age = 30 # Max age in days to keep old log files (default: 30) | |||
| 33 | + | compress = true # Compress old log files with gzip (default: true) | |||
| 34 | + | ||||
| 35 | + | # ============================================================================ | |||
| 36 | + | # ALTERNATIVE LOGGING CONFIGURATIONS | |||
| 37 | + | # ============================================================================ | |||
| 38 | + | ||||
| 39 | + | # Development logging configuration (uncomment to use) | |||
| 40 | + | # [logging] | |||
| 41 | + | # enabled = true | |||
| 42 | + | # level = "debug" # More verbose for debugging | |||
| 43 | + | # dir = "./dev-logs" | |||
| 44 | + | # main_log_file = "gordon-dev.log" | |||
| 45 | + | # proxy_log_file = "proxy-dev.log" | |||
| 46 | + | # container_log_dir = "containers" | |||
| 47 | + | # max_size = 10 # Smaller files for development | |||
| 48 | + | # max_backups = 3 # Less history needed | |||
| 49 | + | # max_age = 7 # Keep logs for 1 week | |||
| 50 | + | # compress = false # No compression for faster access | |||
| 51 | + | ||||
| 52 | + | # Production logging configuration (uncomment to use) | |||
| 53 | + | # [logging] | |||
| 54 | + | # enabled = true | |||
| 55 | + | # level = "warn" # Only warnings and errors in production | |||
| 56 | + | # dir = "/var/log/gordon" # System log directory | |||
| 57 | + | # main_log_file = "gordon.log" | |||
| 58 | + | # proxy_log_file = "proxy.log" | |||
| 59 | + | # container_log_dir = "containers" | |||
| 60 | + | # max_size = 200 # Larger files for production | |||
| 61 | + | # max_backups = 15 # More history for analysis | |||
| 62 | + | # max_age = 90 # Keep logs for 3 months | |||
| 63 | + | # compress = true # Save disk space | |||
| 64 | + | ||||
| 65 | + | # High-traffic logging configuration (uncomment to use) | |||
| 66 | + | # [logging] | |||
| 67 | + | # enabled = true | |||
| 68 | + | # level = "error" # Only errors to reduce log volume | |||
| 69 | + | # dir = "/var/log/gordon" | |||
| 70 | + | # main_log_file = "gordon.log" | |||
| 71 | + | # proxy_log_file = "proxy.log" | |||
| 72 | + | # container_log_dir = "containers" | |||
| 73 | + | # max_size = 500 # Very large files for high traffic | |||
| 74 | + | # max_backups = 20 # Extensive history | |||
| 75 | + | # max_age = 180 # Keep logs for 6 months | |||
| 76 | + | # compress = true # Essential for large files | |||
| 77 | + | ||||
| 78 | + | # Minimal logging configuration (uncomment to use) | |||
| 79 | + | # [logging] | |||
| 80 | + | # enabled = true | |||
| 81 | + | # level = "error" # Only log errors | |||
| 82 | + | # dir = "./logs" | |||
| 83 | + | # max_size = 50 # Small files | |||
| 84 | + | # max_backups = 2 # Minimal history | |||
| 85 | + | # max_age = 14 # 2 weeks retention | |||
| 86 | + | # compress = true | |||
| 87 | + | ||||
| 88 | + | # Disable logging (console output only) - uncomment to use | |||
| 89 | + | # [logging] | |||
| 90 | + | # enabled = false # Only console output, no files | |||
| 91 | + | ||||
| 92 | + | # ============================================================================ | |||
| 93 | + | # LOG STRUCTURE CREATED BY GORDON | |||
| 94 | + | # ============================================================================ | |||
| 95 | + | # | |||
| 96 | + | # When logging is enabled, Gordon creates this structure: | |||
| 97 | + | # | |||
| 98 | + | # ./logs/ # Main log directory (permissions: 0700) | |||
| 99 | + | # ├── gordon.log # Main application logs (permissions: 0600) | |||
| 100 | + | # ├── gordon.log.1.gz # Rotated logs (compressed) | |||
| 101 | + | # ├── gordon.log.2.gz | |||
| 102 | + | # ├── proxy.log # HTTP proxy traffic logs (permissions: 0600) | |||
| 103 | + | # ├── proxy.log.1.gz # Rotated proxy logs | |||
| 104 | + | # └── containers/ # Container logs directory (permissions: 0700) | |||
| 105 | + | # ├── abc123def456.log # Container logs by ID (permissions: 0600) | |||
| 106 | + | # ├── def456ghi789.log | |||
| 107 | + | # ├── myapp_yourdomain_com.log # Symlink to container ID for easier access | |||
| 108 | + | # └── api_yourdomain_com.log # Symlink to container ID for easier access | |||
| 109 | + | # | |||
| 110 | + | # ============================================================================ | |||
| 111 | + | # WHAT GETS LOGGED | |||
| 112 | + | # ============================================================================ | |||
| 113 | + | # | |||
| 114 | + | # Main Application Logs (gordon.log): | |||
| 115 | + | # - Server startup and shutdown events | |||
| 116 | + | # - Container lifecycle events (deploy, start, stop, remove) | |||
| 117 | + | # - Configuration changes and hot reloads | |||
| 118 | + | # - Registry operations (push/pull events) | |||
| 119 | + | # - Error conditions and warnings | |||
| 120 | + | # - Authentication events | |||
| 121 | + | # - Auto-route creation events | |||
| 122 | + | # | |||
| 123 | + | # Proxy Logs (proxy.log): | |||
| 124 | + | # - All HTTP/HTTPS requests through the reverse proxy | |||
| 125 | + | # - Request method, URL, response code, response time | |||
| 126 | + | # - Client IP addresses and user agents | |||
| 127 | + | # - SSL/TLS connection information | |||
| 128 | + | # - Upstream container routing decisions | |||
| 129 | + | # | |||
| 130 | + | # Container Logs (containers/*.log): | |||
| 131 | + | # - Real-time stdout/stderr from each container | |||
| 132 | + | # - Timestamped application output | |||
| 133 | + | # - Container startup and shutdown messages | |||
| 134 | + | # - Application errors and debug output | |||
| 135 | + | # - Custom application logging | |||
| 136 | + | # | |||
| 137 | + | # ============================================================================ | |||
| 138 | + | # SECURITY FEATURES | |||
| 139 | + | # ============================================================================ | |||
| 140 | + | # | |||
| 141 | + | # - Log directories created with 0700 permissions (owner-only access) | |||
| 142 | + | # - Log files created with 0600 permissions (owner read/write only) | |||
| 143 | + | # - Automatic log rotation prevents disk space exhaustion | |||
| 144 | + | # - Sensitive information (passwords, tokens) is automatically redacted | |||
| 145 | + | # - Container logs are isolated per container for security analysis | |||
| 146 | + | # | |||
| 147 | + | # ============================================================================ | |||
| 148 | + | # LOG MONITORING COMMANDS | |||
| 149 | + | # ============================================================================ | |||
| 150 | + | # | |||
| 151 | + | # Real-time monitoring: | |||
| 152 | + | # tail -f logs/gordon.log # Watch main application logs | |||
| 153 | + | # tail -f logs/proxy.log # Monitor HTTP traffic | |||
| 154 | + | # tail -f logs/containers/*.log # Follow all container logs | |||
| 155 | + | # | |||
| 156 | + | # Log analysis: | |||
| 157 | + | # grep -i error logs/gordon.log # Find errors in main logs | |||
| 158 | + | # grep "deploy" logs/gordon.log # Find deployment events | |||
| 159 | + | # awk '{print $9}' logs/proxy.log | sort | uniq -c # HTTP status code analysis | |||
| 160 | + | # grep "container" logs/gordon.log | tail -20 # Recent container events | |||
| 161 | + | # | |||
| 162 | + | # ============================================================================ | |||
| 163 | + | ||||
| 164 | + | [routes] | |||
| 165 | + | "app.yourdomain.com" = "myapp:latest" | |||
| 166 | + | "api.yourdomain.com" = "myapi:latest" | |||
| 167 | + | "logs.yourdomain.com" = "logviewer:latest" # Example log viewing service | |||
| 168 | + | ||||
| 169 | + | # Example environment variables configuration | |||
| 170 | + | [env] | |||
| 171 | + | dir = "./data/env" | |||
| 172 | + | providers = ["pass"] | |||
| 173 | + | ||||
| 174 | + | # With comprehensive logging enabled, you can: | |||
| 175 | + | # 1. Monitor all HTTP traffic to your applications | |||
| 176 | + | # 2. Debug container deployment issues | |||
| 177 | + | # 3. Analyze application performance | |||
| 178 | + | # 4. Track security events and access patterns | |||
| 179 | + | # 5. Maintain audit trails for compliance | |||
| 180 | + | # 6. Troubleshoot network and routing issues⏎̸ | |||
M examples/minimal.toml +6 -0
| @@ -9,6 +9,12 @@ registry_domain = "registry.local" | |||
| 9 | 9 | [registry_auth] | |
| 10 | 10 | enabled = false # Disabled for simplicity | |
| 11 | 11 | ||
| 12 | + | # Optional: Enable basic logging | |
| 13 | + | # [logging] | |
| 14 | + | # enabled = true | |
| 15 | + | # level = "info" | |
| 16 | + | # dir = "./logs" | |
| 17 | + | ||
| 12 | 18 | # Environment variables (optional) | |
| 13 | 19 | [env] | |
| 14 | 20 | dir = "./data/env" # Default location | |
| @@ -9,6 +9,12 @@ registry_domain = "registry.local" | |||||
| 9 | [registry_auth] | 9 | [registry_auth] | ||
| 10 | enabled = false # Disabled for simplicity | 10 | enabled = false # Disabled for simplicity | ||
| 11 | 11 | ||||
| 12 | + | # Optional: Enable basic logging | |||
| 13 | + | # [logging] | |||
| 14 | + | # enabled = true | |||
| 15 | + | # level = "info" | |||
| 16 | + | # dir = "./logs" | |||
| 17 | + | ||||
| 12 | # Environment variables (optional) | 18 | # Environment variables (optional) | ||
| 13 | [env] | 19 | [env] | ||
| 14 | dir = "./data/env" # Default location | 20 | dir = "./data/env" # Default location | ||
M examples/production.toml +15 -0
| @@ -15,6 +15,21 @@ enabled = true | |||
| 15 | 15 | username = "deploy-bot" | |
| 16 | 16 | password = "CHANGE-THIS-IN-PRODUCTION" # Use environment variables in production! | |
| 17 | 17 | ||
| 18 | + | # Production logging configuration | |
| 19 | + | [logging] | |
| 20 | + | enabled = true # Enable comprehensive logging | |
| 21 | + | level = "info" # Standard production logging level | |
| 22 | + | dir = "/var/log/gordon" # System log directory | |
| 23 | + | main_log_file = "gordon.log" # Main application logs | |
| 24 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |
| 25 | + | container_log_dir = "containers" # Container logs subdirectory | |
| 26 | + | ||
| 27 | + | # Production log rotation settings | |
| 28 | + | max_size = 100 # Max 100MB per log file | |
| 29 | + | max_backups = 10 # Keep 10 old files for history | |
| 30 | + | max_age = 90 # Keep logs for 3 months | |
| 31 | + | compress = true # Compress old logs to save space | |
| 32 | + | ||
| 18 | 33 | # Environment variables configuration | |
| 19 | 34 | [env] | |
| 20 | 35 | # Directory where .env files are stored for each route | |
| @@ -15,6 +15,21 @@ enabled = true | |||||
| 15 | username = "deploy-bot" | 15 | username = "deploy-bot" | ||
| 16 | password = "CHANGE-THIS-IN-PRODUCTION" # Use environment variables in production! | 16 | password = "CHANGE-THIS-IN-PRODUCTION" # Use environment variables in production! | ||
| 17 | 17 | ||||
| 18 | + | # Production logging configuration | |||
| 19 | + | [logging] | |||
| 20 | + | enabled = true # Enable comprehensive logging | |||
| 21 | + | level = "info" # Standard production logging level | |||
| 22 | + | dir = "/var/log/gordon" # System log directory | |||
| 23 | + | main_log_file = "gordon.log" # Main application logs | |||
| 24 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |||
| 25 | + | container_log_dir = "containers" # Container logs subdirectory | |||
| 26 | + | ||||
| 27 | + | # Production log rotation settings | |||
| 28 | + | max_size = 100 # Max 100MB per log file | |||
| 29 | + | max_backups = 10 # Keep 10 old files for history | |||
| 30 | + | max_age = 90 # Keep logs for 3 months | |||
| 31 | + | compress = true # Compress old logs to save space | |||
| 32 | + | ||||
| 18 | # Environment variables configuration | 33 | # Environment variables configuration | ||
| 19 | [env] | 34 | [env] | ||
| 20 | # Directory where .env files are stored for each route | 35 | # Directory where .env files are stored for each route | ||
M examples/staging.toml +15 -0
| @@ -14,6 +14,21 @@ enabled = true | |||
| 14 | 14 | username = "staging-user" | |
| 15 | 15 | password = "staging-pass-123" | |
| 16 | 16 | ||
| 17 | + | # Staging logging configuration | |
| 18 | + | [logging] | |
| 19 | + | enabled = true # Enable logging for staging debugging | |
| 20 | + | level = "debug" # Verbose logging for troubleshooting | |
| 21 | + | dir = "./staging-logs" # Staging-specific log directory | |
| 22 | + | main_log_file = "gordon.log" # Main application logs | |
| 23 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |
| 24 | + | container_log_dir = "containers" # Container logs subdirectory | |
| 25 | + | ||
| 26 | + | # Staging log rotation settings | |
| 27 | + | max_size = 50 # Moderate file sizes for staging | |
| 28 | + | max_backups = 5 # Keep reasonable history | |
| 29 | + | max_age = 30 # Keep logs for 1 month | |
| 30 | + | compress = true # Compress to save space | |
| 31 | + | ||
| 17 | 32 | # Staging environment routes - using branch-based deployments | |
| 18 | 33 | [routes] | |
| 19 | 34 | "staging.company.com" = "company-app:staging" # Main staging branch | |
| @@ -14,6 +14,21 @@ enabled = true | |||||
| 14 | username = "staging-user" | 14 | username = "staging-user" | ||
| 15 | password = "staging-pass-123" | 15 | password = "staging-pass-123" | ||
| 16 | 16 | ||||
| 17 | + | # Staging logging configuration | |||
| 18 | + | [logging] | |||
| 19 | + | enabled = true # Enable logging for staging debugging | |||
| 20 | + | level = "debug" # Verbose logging for troubleshooting | |||
| 21 | + | dir = "./staging-logs" # Staging-specific log directory | |||
| 22 | + | main_log_file = "gordon.log" # Main application logs | |||
| 23 | + | proxy_log_file = "proxy.log" # HTTP proxy traffic logs | |||
| 24 | + | container_log_dir = "containers" # Container logs subdirectory | |||
| 25 | + | ||||
| 26 | + | # Staging log rotation settings | |||
| 27 | + | max_size = 50 # Moderate file sizes for staging | |||
| 28 | + | max_backups = 5 # Keep reasonable history | |||
| 29 | + | max_age = 30 # Keep logs for 1 month | |||
| 30 | + | compress = true # Compress to save space | |||
| 31 | + | ||||
| 17 | # Staging environment routes - using branch-based deployments | 32 | # Staging environment routes - using branch-based deployments | ||
| 18 | [routes] | 33 | [routes] | ||
| 19 | "staging.company.com" = "company-app:staging" # Main staging branch | 34 | "staging.company.com" = "company-app:staging" # Main staging branch | ||
M go.mod +2 -0
| @@ -10,6 +10,8 @@ require ( | |||
| 10 | 10 | github.com/spf13/viper v1.20.1 | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | + | require gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |
| 14 | + | ||
| 13 | 15 | require ( | |
| 14 | 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | |
| 15 | 17 | github.com/containerd/errdefs v1.0.0 // indirect | |
| @@ -10,6 +10,8 @@ require ( | |||||
| 10 | github.com/spf13/viper v1.20.1 | 10 | github.com/spf13/viper v1.20.1 | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | + | require gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect | |||
| 14 | + | ||||
| 13 | require ( | 15 | require ( | ||
| 14 | github.com/Microsoft/go-winio v0.4.14 // indirect | 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | ||
| 15 | github.com/containerd/errdefs v1.0.0 // indirect | 17 | github.com/containerd/errdefs v1.0.0 // indirect | ||
M go.sum +2 -0 generated file: collapsed
M internal/config/config.go +72 -35
| @@ -12,11 +12,12 @@ import ( | |||
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | 14 | type Config struct { | |
| 15 | - | Server ServerConfig `mapstructure:"server"` | |
| 15 | + | Server ServerConfig `mapstructure:"server"` | |
| 16 | 16 | RegistryAuth RegistryAuthConfig `mapstructure:"registry_auth"` | |
| 17 | - | Routes map[string]string `mapstructure:"routes"` | |
| 18 | - | AutoRoute AutoRouteConfig `mapstructure:"auto_route"` | |
| 19 | - | Env EnvConfig `mapstructure:"env"` | |
| 17 | + | Routes map[string]string `mapstructure:"routes"` | |
| 18 | + | AutoRoute AutoRouteConfig `mapstructure:"auto_route"` | |
| 19 | + | Env EnvConfig `mapstructure:"env"` | |
| 20 | + | Logging LoggingConfig `mapstructure:"logging"` | |
| 20 | 21 | } | |
| 21 | 22 | ||
| 22 | 23 | type ServerConfig struct { | |
| @@ -44,6 +45,19 @@ type EnvConfig struct { | |||
| 44 | 45 | Providers []string `mapstructure:"providers"` | |
| 45 | 46 | } | |
| 46 | 47 | ||
| 48 | + | type LoggingConfig struct { | |
| 49 | + | Enabled bool `mapstructure:"enabled"` | |
| 50 | + | Level string `mapstructure:"level"` | |
| 51 | + | Dir string `mapstructure:"dir"` | |
| 52 | + | MainLogFile string `mapstructure:"main_log_file"` | |
| 53 | + | ProxyLogFile string `mapstructure:"proxy_log_file"` | |
| 54 | + | ContainerLogDir string `mapstructure:"container_log_dir"` | |
| 55 | + | MaxSize int `mapstructure:"max_size"` | |
| 56 | + | MaxBackups int `mapstructure:"max_backups"` | |
| 57 | + | MaxAge int `mapstructure:"max_age"` | |
| 58 | + | Compress bool `mapstructure:"compress"` | |
| 59 | + | } | |
| 60 | + | ||
| 47 | 61 | type SecretProvider struct { | |
| 48 | 62 | Type string `mapstructure:"type"` | |
| 49 | 63 | Config map[string]string `mapstructure:"config"` | |
| @@ -57,13 +71,13 @@ type Route struct { | |||
| 57 | 71 | ||
| 58 | 72 | func Load() (*Config, error) { | |
| 59 | 73 | var cfg Config | |
| 60 | - | ||
| 74 | + | ||
| 61 | 75 | // Set defaults | |
| 62 | 76 | viper.SetDefault("server.port", 8080) | |
| 63 | 77 | viper.SetDefault("server.registry_port", 5000) | |
| 64 | 78 | viper.SetDefault("server.runtime", "auto") | |
| 65 | 79 | viper.SetDefault("server.socket_path", "") | |
| 66 | - | ||
| 80 | + | ||
| 67 | 81 | // Set data_dir default based on environment | |
| 68 | 82 | defaultDataDir := getDefaultDataDir() | |
| 69 | 83 | viper.SetDefault("server.data_dir", defaultDataDir) | |
| @@ -72,26 +86,38 @@ func Load() (*Config, error) { | |||
| 72 | 86 | viper.SetDefault("env.dir", filepath.Join(defaultDataDir, "env")) | |
| 73 | 87 | viper.SetDefault("env.providers", []string{"pass", "sops"}) | |
| 74 | 88 | ||
| 89 | + | // Logging defaults | |
| 90 | + | viper.SetDefault("logging.enabled", true) | |
| 91 | + | viper.SetDefault("logging.level", "info") | |
| 92 | + | viper.SetDefault("logging.dir", filepath.Join(defaultDataDir, "logs")) | |
| 93 | + | viper.SetDefault("logging.main_log_file", "gordon.log") | |
| 94 | + | viper.SetDefault("logging.proxy_log_file", "proxy.log") | |
| 95 | + | viper.SetDefault("logging.container_log_dir", "containers") | |
| 96 | + | viper.SetDefault("logging.max_size", 100) // 100MB | |
| 97 | + | viper.SetDefault("logging.max_backups", 3) // Keep 3 old files | |
| 98 | + | viper.SetDefault("logging.max_age", 28) // 28 days | |
| 99 | + | viper.SetDefault("logging.compress", true) | |
| 100 | + | ||
| 75 | 101 | // Handle the routes manually since Viper struggles with domain names | |
| 76 | 102 | cfg.Routes = make(map[string]string) | |
| 77 | - | ||
| 103 | + | ||
| 78 | 104 | // Get server config first | |
| 79 | 105 | if err := viper.UnmarshalKey("server", &cfg.Server); err != nil { | |
| 80 | 106 | return nil, fmt.Errorf("unable to decode server config: %v", err) | |
| 81 | 107 | } | |
| 82 | - | ||
| 108 | + | ||
| 83 | 109 | // If data_dir is empty after loading config, use the default | |
| 84 | 110 | if cfg.Server.DataDir == "" { | |
| 85 | 111 | cfg.Server.DataDir = defaultDataDir | |
| 86 | 112 | log.Debug().Str("data_dir", cfg.Server.DataDir).Msg("Config had empty data_dir, using default") | |
| 87 | 113 | } | |
| 88 | - | ||
| 114 | + | ||
| 89 | 115 | // If registry_port is 0 after loading config, use the default | |
| 90 | 116 | if cfg.Server.RegistryPort == 0 { | |
| 91 | 117 | cfg.Server.RegistryPort = 5000 | |
| 92 | 118 | log.Debug().Int("registry_port", cfg.Server.RegistryPort).Msg("Config had empty registry_port, using default") | |
| 93 | 119 | } | |
| 94 | - | ||
| 120 | + | ||
| 95 | 121 | // Get registry auth config | |
| 96 | 122 | if err := viper.UnmarshalKey("registry_auth", &cfg.RegistryAuth); err != nil { | |
| 97 | 123 | return nil, fmt.Errorf("unable to decode registry auth config: %v", err) | |
| @@ -112,7 +138,18 @@ func Load() (*Config, error) { | |||
| 112 | 138 | cfg.Env.Dir = filepath.Join(cfg.Server.DataDir, "env") | |
| 113 | 139 | log.Debug().Str("env_dir", cfg.Env.Dir).Msg("Config had empty env.dir, using default") | |
| 114 | 140 | } | |
| 115 | - | ||
| 141 | + | ||
| 142 | + | // Get logging config | |
| 143 | + | if err := viper.UnmarshalKey("logging", &cfg.Logging); err != nil { | |
| 144 | + | return nil, fmt.Errorf("unable to decode logging config: %v", err) | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // If logging.dir is empty after loading config, use the default | |
| 148 | + | if cfg.Logging.Dir == "" { | |
| 149 | + | cfg.Logging.Dir = filepath.Join(cfg.Server.DataDir, "logs") | |
| 150 | + | log.Debug().Str("logging_dir", cfg.Logging.Dir).Msg("Config had empty logging.dir, using default") | |
| 151 | + | } | |
| 152 | + | ||
| 116 | 153 | // Get routes manually from the raw config | |
| 117 | 154 | routesRaw := viper.Get("routes") | |
| 118 | 155 | if routesRaw != nil { | |
| @@ -165,13 +202,13 @@ func Load() (*Config, error) { | |||
| 165 | 202 | ||
| 166 | 203 | func (c *Config) GetRoutes() []Route { | |
| 167 | 204 | var routes []Route | |
| 168 | - | ||
| 205 | + | ||
| 169 | 206 | for domain, image := range c.Routes { | |
| 170 | 207 | route := Route{ | |
| 171 | 208 | Image: image, | |
| 172 | 209 | HTTPS: true, // Default to HTTPS | |
| 173 | 210 | } | |
| 174 | - | ||
| 211 | + | ||
| 175 | 212 | // Check if domain has http:// prefix | |
| 176 | 213 | if strings.HasPrefix(domain, "http://") { | |
| 177 | 214 | route.Domain = strings.TrimPrefix(domain, "http://") | |
| @@ -179,10 +216,10 @@ func (c *Config) GetRoutes() []Route { | |||
| 179 | 216 | } else { | |
| 180 | 217 | route.Domain = domain | |
| 181 | 218 | } | |
| 182 | - | ||
| 219 | + | ||
| 183 | 220 | routes = append(routes, route) | |
| 184 | 221 | } | |
| 185 | - | ||
| 222 | + | ||
| 186 | 223 | return routes | |
| 187 | 224 | } | |
| 188 | 225 | ||
| @@ -190,7 +227,7 @@ func (c *Config) GetRoutes() []Route { | |||
| 190 | 227 | func getDefaultDataDir() string { | |
| 191 | 228 | uid := os.Getuid() | |
| 192 | 229 | log.Debug().Int("uid", uid).Msg("Detecting default data directory") | |
| 193 | - | ||
| 230 | + | ||
| 194 | 231 | // Check if we're running in a rootless environment | |
| 195 | 232 | if uid != 0 { | |
| 196 | 233 | // For rootless environments, use user's home directory or current directory | |
| @@ -203,7 +240,7 @@ func getDefaultDataDir() string { | |||
| 203 | 240 | } else { | |
| 204 | 241 | log.Debug().Msg("Running as root, using ./data") | |
| 205 | 242 | } | |
| 206 | - | ||
| 243 | + | ||
| 207 | 244 | // For root or when home directory is not available, use relative path | |
| 208 | 245 | return "./data" | |
| 209 | 246 | } | |
| @@ -213,14 +250,14 @@ func ExtractDomainFromImageName(imageName string) (string, bool) { | |||
| 213 | 250 | // Split by colon to separate image name from tag | |
| 214 | 251 | parts := strings.Split(imageName, ":") | |
| 215 | 252 | imageNamePart := parts[0] | |
| 216 | - | ||
| 253 | + | ||
| 217 | 254 | // Domain pattern: should contain at least one dot and valid domain characters | |
| 218 | 255 | domainPattern := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) | |
| 219 | - | ||
| 256 | + | ||
| 220 | 257 | if domainPattern.MatchString(imageNamePart) { | |
| 221 | 258 | return imageNamePart, true | |
| 222 | 259 | } | |
| 223 | - | ||
| 260 | + | ||
| 224 | 261 | return "", false | |
| 225 | 262 | } | |
| 226 | 263 | ||
| @@ -229,37 +266,37 @@ func (c *Config) AddRoute(domain, image string) error { | |||
| 229 | 266 | if c.Routes == nil { | |
| 230 | 267 | c.Routes = make(map[string]string) | |
| 231 | 268 | } | |
| 232 | - | ||
| 269 | + | ||
| 233 | 270 | c.Routes[domain] = image | |
| 234 | - | ||
| 271 | + | ||
| 235 | 272 | // Read the current config file to preserve formatting | |
| 236 | 273 | configFile := viper.ConfigFileUsed() | |
| 237 | 274 | if configFile == "" { | |
| 238 | 275 | return fmt.Errorf("no config file path available") | |
| 239 | 276 | } | |
| 240 | - | ||
| 277 | + | ||
| 241 | 278 | // Read existing content | |
| 242 | 279 | content, err := os.ReadFile(configFile) | |
| 243 | 280 | if err != nil { | |
| 244 | 281 | return fmt.Errorf("failed to read config file: %w", err) | |
| 245 | 282 | } | |
| 246 | - | ||
| 283 | + | ||
| 247 | 284 | // Parse and update only the routes section while preserving other formatting | |
| 248 | 285 | lines := strings.Split(string(content), "\n") | |
| 249 | 286 | var newLines []string | |
| 250 | 287 | inRoutesSection := false | |
| 251 | 288 | routeAdded := false | |
| 252 | - | ||
| 289 | + | ||
| 253 | 290 | for _, line := range lines { | |
| 254 | 291 | trimmed := strings.TrimSpace(line) | |
| 255 | - | ||
| 292 | + | ||
| 256 | 293 | // Check if we're entering or leaving the routes section | |
| 257 | 294 | if trimmed == "[routes]" { | |
| 258 | 295 | inRoutesSection = true | |
| 259 | 296 | newLines = append(newLines, line) | |
| 260 | 297 | continue | |
| 261 | 298 | } | |
| 262 | - | ||
| 299 | + | ||
| 263 | 300 | // Check if we're entering a new section | |
| 264 | 301 | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | |
| 265 | 302 | // If we were in routes section and haven't added the route yet, add it | |
| @@ -269,7 +306,7 @@ func (c *Config) AddRoute(domain, image string) error { | |||
| 269 | 306 | } | |
| 270 | 307 | inRoutesSection = false | |
| 271 | 308 | } | |
| 272 | - | ||
| 309 | + | ||
| 273 | 310 | // If we're in routes section, check if this route already exists (check both quote styles) | |
| 274 | 311 | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | |
| 275 | 312 | // Update existing route using double quotes for consistency | |
| @@ -279,18 +316,18 @@ func (c *Config) AddRoute(domain, image string) error { | |||
| 279 | 316 | newLines = append(newLines, line) | |
| 280 | 317 | } | |
| 281 | 318 | } | |
| 282 | - | ||
| 319 | + | ||
| 283 | 320 | // If we were in routes section at the end and haven't added the route yet, add it | |
| 284 | 321 | if inRoutesSection && !routeAdded { | |
| 285 | 322 | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | |
| 286 | 323 | } | |
| 287 | - | ||
| 324 | + | ||
| 288 | 325 | // Write the updated content back | |
| 289 | 326 | newContent := strings.Join(newLines, "\n") | |
| 290 | 327 | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | |
| 291 | 328 | return fmt.Errorf("failed to write config file: %w", err) | |
| 292 | 329 | } | |
| 293 | - | ||
| 330 | + | ||
| 294 | 331 | log.Info(). | |
| 295 | 332 | Str("domain", domain). | |
| 296 | 333 | Str("image", image). | |
| @@ -298,7 +335,7 @@ func (c *Config) AddRoute(domain, image string) error { | |||
| 298 | 335 | ||
| 299 | 336 | // Create env file for the new route (if env loader is available) | |
| 300 | 337 | // This will be called from the auto-route handler which has access to the env loader | |
| 301 | - | ||
| 338 | + | ||
| 302 | 339 | return nil | |
| 303 | 340 | } | |
| 304 | 341 | ||
| @@ -326,7 +363,7 @@ func validateEnvConfig(envCfg *EnvConfig) error { | |||
| 326 | 363 | if providerName == "" { | |
| 327 | 364 | return fmt.Errorf("empty provider name in env.providers") | |
| 328 | 365 | } | |
| 329 | - | ||
| 366 | + | ||
| 330 | 367 | isValid := false | |
| 331 | 368 | for _, validType := range validProviders { | |
| 332 | 369 | if providerName == validType { | |
| @@ -335,10 +372,10 @@ func validateEnvConfig(envCfg *EnvConfig) error { | |||
| 335 | 372 | } | |
| 336 | 373 | } | |
| 337 | 374 | if !isValid { | |
| 338 | - | return fmt.Errorf("unsupported provider '%s' in env.providers. Supported providers: %s", | |
| 375 | + | return fmt.Errorf("unsupported provider '%s' in env.providers. Supported providers: %s", | |
| 339 | 376 | providerName, strings.Join(validProviders, ", ")) | |
| 340 | 377 | } | |
| 341 | 378 | } | |
| 342 | 379 | ||
| 343 | 380 | return nil | |
| 344 | - | }⏎̸ | |
| 381 | + | } | |
| @@ -12,11 +12,12 @@ import ( | |||||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | type Config struct { | 14 | type Config struct { | ||
| 15 | - | Server ServerConfig `mapstructure:"server"` | 15 | + | Server ServerConfig `mapstructure:"server"` |
| 16 | RegistryAuth RegistryAuthConfig `mapstructure:"registry_auth"` | 16 | RegistryAuth RegistryAuthConfig `mapstructure:"registry_auth"` | ||
| 17 | - | Routes map[string]string `mapstructure:"routes"` | 17 | + | Routes map[string]string `mapstructure:"routes"` |
| 18 | - | AutoRoute AutoRouteConfig `mapstructure:"auto_route"` | 18 | + | AutoRoute AutoRouteConfig `mapstructure:"auto_route"` |
| 19 | - | Env EnvConfig `mapstructure:"env"` | 19 | + | Env EnvConfig `mapstructure:"env"` |
| 20 | + | Logging LoggingConfig `mapstructure:"logging"` | |||
| 20 | } | 21 | } | ||
| 21 | 22 | ||||
| 22 | type ServerConfig struct { | 23 | type ServerConfig struct { | ||
| @@ -44,6 +45,19 @@ type EnvConfig struct { | |||||
| 44 | Providers []string `mapstructure:"providers"` | 45 | Providers []string `mapstructure:"providers"` | ||
| 45 | } | 46 | } | ||
| 46 | 47 | ||||
| 48 | + | type LoggingConfig struct { | |||
| 49 | + | Enabled bool `mapstructure:"enabled"` | |||
| 50 | + | Level string `mapstructure:"level"` | |||
| 51 | + | Dir string `mapstructure:"dir"` | |||
| 52 | + | MainLogFile string `mapstructure:"main_log_file"` | |||
| 53 | + | ProxyLogFile string `mapstructure:"proxy_log_file"` | |||
| 54 | + | ContainerLogDir string `mapstructure:"container_log_dir"` | |||
| 55 | + | MaxSize int `mapstructure:"max_size"` | |||
| 56 | + | MaxBackups int `mapstructure:"max_backups"` | |||
| 57 | + | MaxAge int `mapstructure:"max_age"` | |||
| 58 | + | Compress bool `mapstructure:"compress"` | |||
| 59 | + | } | |||
| 60 | + | ||||
| 47 | type SecretProvider struct { | 61 | type SecretProvider struct { | ||
| 48 | Type string `mapstructure:"type"` | 62 | Type string `mapstructure:"type"` | ||
| 49 | Config map[string]string `mapstructure:"config"` | 63 | Config map[string]string `mapstructure:"config"` | ||
| @@ -57,13 +71,13 @@ type Route struct { | |||||
| 57 | 71 | ||||
| 58 | func Load() (*Config, error) { | 72 | func Load() (*Config, error) { | ||
| 59 | var cfg Config | 73 | var cfg Config | ||
| 60 | - | 74 | + | ||
| 61 | // Set defaults | 75 | // Set defaults | ||
| 62 | viper.SetDefault("server.port", 8080) | 76 | viper.SetDefault("server.port", 8080) | ||
| 63 | viper.SetDefault("server.registry_port", 5000) | 77 | viper.SetDefault("server.registry_port", 5000) | ||
| 64 | viper.SetDefault("server.runtime", "auto") | 78 | viper.SetDefault("server.runtime", "auto") | ||
| 65 | viper.SetDefault("server.socket_path", "") | 79 | viper.SetDefault("server.socket_path", "") | ||
| 66 | - | 80 | + | ||
| 67 | // Set data_dir default based on environment | 81 | // Set data_dir default based on environment | ||
| 68 | defaultDataDir := getDefaultDataDir() | 82 | defaultDataDir := getDefaultDataDir() | ||
| 69 | viper.SetDefault("server.data_dir", defaultDataDir) | 83 | viper.SetDefault("server.data_dir", defaultDataDir) | ||
| @@ -72,26 +86,38 @@ func Load() (*Config, error) { | |||||
| 72 | viper.SetDefault("env.dir", filepath.Join(defaultDataDir, "env")) | 86 | viper.SetDefault("env.dir", filepath.Join(defaultDataDir, "env")) | ||
| 73 | viper.SetDefault("env.providers", []string{"pass", "sops"}) | 87 | viper.SetDefault("env.providers", []string{"pass", "sops"}) | ||
| 74 | 88 | ||||
| 89 | + | // Logging defaults | |||
| 90 | + | viper.SetDefault("logging.enabled", true) | |||
| 91 | + | viper.SetDefault("logging.level", "info") | |||
| 92 | + | viper.SetDefault("logging.dir", filepath.Join(defaultDataDir, "logs")) | |||
| 93 | + | viper.SetDefault("logging.main_log_file", "gordon.log") | |||
| 94 | + | viper.SetDefault("logging.proxy_log_file", "proxy.log") | |||
| 95 | + | viper.SetDefault("logging.container_log_dir", "containers") | |||
| 96 | + | viper.SetDefault("logging.max_size", 100) // 100MB | |||
| 97 | + | viper.SetDefault("logging.max_backups", 3) // Keep 3 old files | |||
| 98 | + | viper.SetDefault("logging.max_age", 28) // 28 days | |||
| 99 | + | viper.SetDefault("logging.compress", true) | |||
| 100 | + | ||||
| 75 | // Handle the routes manually since Viper struggles with domain names | 101 | // Handle the routes manually since Viper struggles with domain names | ||
| 76 | cfg.Routes = make(map[string]string) | 102 | cfg.Routes = make(map[string]string) | ||
| 77 | - | 103 | + | ||
| 78 | // Get server config first | 104 | // Get server config first | ||
| 79 | if err := viper.UnmarshalKey("server", &cfg.Server); err != nil { | 105 | if err := viper.UnmarshalKey("server", &cfg.Server); err != nil { | ||
| 80 | return nil, fmt.Errorf("unable to decode server config: %v", err) | 106 | return nil, fmt.Errorf("unable to decode server config: %v", err) | ||
| 81 | } | 107 | } | ||
| 82 | - | 108 | + | ||
| 83 | // If data_dir is empty after loading config, use the default | 109 | // If data_dir is empty after loading config, use the default | ||
| 84 | if cfg.Server.DataDir == "" { | 110 | if cfg.Server.DataDir == "" { | ||
| 85 | cfg.Server.DataDir = defaultDataDir | 111 | cfg.Server.DataDir = defaultDataDir | ||
| 86 | log.Debug().Str("data_dir", cfg.Server.DataDir).Msg("Config had empty data_dir, using default") | 112 | log.Debug().Str("data_dir", cfg.Server.DataDir).Msg("Config had empty data_dir, using default") | ||
| 87 | } | 113 | } | ||
| 88 | - | 114 | + | ||
| 89 | // If registry_port is 0 after loading config, use the default | 115 | // If registry_port is 0 after loading config, use the default | ||
| 90 | if cfg.Server.RegistryPort == 0 { | 116 | if cfg.Server.RegistryPort == 0 { | ||
| 91 | cfg.Server.RegistryPort = 5000 | 117 | cfg.Server.RegistryPort = 5000 | ||
| 92 | log.Debug().Int("registry_port", cfg.Server.RegistryPort).Msg("Config had empty registry_port, using default") | 118 | log.Debug().Int("registry_port", cfg.Server.RegistryPort).Msg("Config had empty registry_port, using default") | ||
| 93 | } | 119 | } | ||
| 94 | - | 120 | + | ||
| 95 | // Get registry auth config | 121 | // Get registry auth config | ||
| 96 | if err := viper.UnmarshalKey("registry_auth", &cfg.RegistryAuth); err != nil { | 122 | if err := viper.UnmarshalKey("registry_auth", &cfg.RegistryAuth); err != nil { | ||
| 97 | return nil, fmt.Errorf("unable to decode registry auth config: %v", err) | 123 | return nil, fmt.Errorf("unable to decode registry auth config: %v", err) | ||
| @@ -112,7 +138,18 @@ func Load() (*Config, error) { | |||||
| 112 | cfg.Env.Dir = filepath.Join(cfg.Server.DataDir, "env") | 138 | cfg.Env.Dir = filepath.Join(cfg.Server.DataDir, "env") | ||
| 113 | log.Debug().Str("env_dir", cfg.Env.Dir).Msg("Config had empty env.dir, using default") | 139 | log.Debug().Str("env_dir", cfg.Env.Dir).Msg("Config had empty env.dir, using default") | ||
| 114 | } | 140 | } | ||
| 115 | - | 141 | + | ||
| 142 | + | // Get logging config | |||
| 143 | + | if err := viper.UnmarshalKey("logging", &cfg.Logging); err != nil { | |||
| 144 | + | return nil, fmt.Errorf("unable to decode logging config: %v", err) | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // If logging.dir is empty after loading config, use the default | |||
| 148 | + | if cfg.Logging.Dir == "" { | |||
| 149 | + | cfg.Logging.Dir = filepath.Join(cfg.Server.DataDir, "logs") | |||
| 150 | + | log.Debug().Str("logging_dir", cfg.Logging.Dir).Msg("Config had empty logging.dir, using default") | |||
| 151 | + | } | |||
| 152 | + | ||||
| 116 | // Get routes manually from the raw config | 153 | // Get routes manually from the raw config | ||
| 117 | routesRaw := viper.Get("routes") | 154 | routesRaw := viper.Get("routes") | ||
| 118 | if routesRaw != nil { | 155 | if routesRaw != nil { | ||
| @@ -165,13 +202,13 @@ func Load() (*Config, error) { | |||||
| 165 | 202 | ||||
| 166 | func (c *Config) GetRoutes() []Route { | 203 | func (c *Config) GetRoutes() []Route { | ||
| 167 | var routes []Route | 204 | var routes []Route | ||
| 168 | - | 205 | + | ||
| 169 | for domain, image := range c.Routes { | 206 | for domain, image := range c.Routes { | ||
| 170 | route := Route{ | 207 | route := Route{ | ||
| 171 | Image: image, | 208 | Image: image, | ||
| 172 | HTTPS: true, // Default to HTTPS | 209 | HTTPS: true, // Default to HTTPS | ||
| 173 | } | 210 | } | ||
| 174 | - | 211 | + | ||
| 175 | // Check if domain has http:// prefix | 212 | // Check if domain has http:// prefix | ||
| 176 | if strings.HasPrefix(domain, "http://") { | 213 | if strings.HasPrefix(domain, "http://") { | ||
| 177 | route.Domain = strings.TrimPrefix(domain, "http://") | 214 | route.Domain = strings.TrimPrefix(domain, "http://") | ||
| @@ -179,10 +216,10 @@ func (c *Config) GetRoutes() []Route { | |||||
| 179 | } else { | 216 | } else { | ||
| 180 | route.Domain = domain | 217 | route.Domain = domain | ||
| 181 | } | 218 | } | ||
| 182 | - | 219 | + | ||
| 183 | routes = append(routes, route) | 220 | routes = append(routes, route) | ||
| 184 | } | 221 | } | ||
| 185 | - | 222 | + | ||
| 186 | return routes | 223 | return routes | ||
| 187 | } | 224 | } | ||
| 188 | 225 | ||||
| @@ -190,7 +227,7 @@ func (c *Config) GetRoutes() []Route { | |||||
| 190 | func getDefaultDataDir() string { | 227 | func getDefaultDataDir() string { | ||
| 191 | uid := os.Getuid() | 228 | uid := os.Getuid() | ||
| 192 | log.Debug().Int("uid", uid).Msg("Detecting default data directory") | 229 | log.Debug().Int("uid", uid).Msg("Detecting default data directory") | ||
| 193 | - | 230 | + | ||
| 194 | // Check if we're running in a rootless environment | 231 | // Check if we're running in a rootless environment | ||
| 195 | if uid != 0 { | 232 | if uid != 0 { | ||
| 196 | // For rootless environments, use user's home directory or current directory | 233 | // For rootless environments, use user's home directory or current directory | ||
| @@ -203,7 +240,7 @@ func getDefaultDataDir() string { | |||||
| 203 | } else { | 240 | } else { | ||
| 204 | log.Debug().Msg("Running as root, using ./data") | 241 | log.Debug().Msg("Running as root, using ./data") | ||
| 205 | } | 242 | } | ||
| 206 | - | 243 | + | ||
| 207 | // For root or when home directory is not available, use relative path | 244 | // For root or when home directory is not available, use relative path | ||
| 208 | return "./data" | 245 | return "./data" | ||
| 209 | } | 246 | } | ||
| @@ -213,14 +250,14 @@ func ExtractDomainFromImageName(imageName string) (string, bool) { | |||||
| 213 | // Split by colon to separate image name from tag | 250 | // Split by colon to separate image name from tag | ||
| 214 | parts := strings.Split(imageName, ":") | 251 | parts := strings.Split(imageName, ":") | ||
| 215 | imageNamePart := parts[0] | 252 | imageNamePart := parts[0] | ||
| 216 | - | 253 | + | ||
| 217 | // Domain pattern: should contain at least one dot and valid domain characters | 254 | // Domain pattern: should contain at least one dot and valid domain characters | ||
| 218 | domainPattern := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) | 255 | domainPattern := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) | ||
| 219 | - | 256 | + | ||
| 220 | if domainPattern.MatchString(imageNamePart) { | 257 | if domainPattern.MatchString(imageNamePart) { | ||
| 221 | return imageNamePart, true | 258 | return imageNamePart, true | ||
| 222 | } | 259 | } | ||
| 223 | - | 260 | + | ||
| 224 | return "", false | 261 | return "", false | ||
| 225 | } | 262 | } | ||
| 226 | 263 | ||||
| @@ -229,37 +266,37 @@ func (c *Config) AddRoute(domain, image string) error { | |||||
| 229 | if c.Routes == nil { | 266 | if c.Routes == nil { | ||
| 230 | c.Routes = make(map[string]string) | 267 | c.Routes = make(map[string]string) | ||
| 231 | } | 268 | } | ||
| 232 | - | 269 | + | ||
| 233 | c.Routes[domain] = image | 270 | c.Routes[domain] = image | ||
| 234 | - | 271 | + | ||
| 235 | // Read the current config file to preserve formatting | 272 | // Read the current config file to preserve formatting | ||
| 236 | configFile := viper.ConfigFileUsed() | 273 | configFile := viper.ConfigFileUsed() | ||
| 237 | if configFile == "" { | 274 | if configFile == "" { | ||
| 238 | return fmt.Errorf("no config file path available") | 275 | return fmt.Errorf("no config file path available") | ||
| 239 | } | 276 | } | ||
| 240 | - | 277 | + | ||
| 241 | // Read existing content | 278 | // Read existing content | ||
| 242 | content, err := os.ReadFile(configFile) | 279 | content, err := os.ReadFile(configFile) | ||
| 243 | if err != nil { | 280 | if err != nil { | ||
| 244 | return fmt.Errorf("failed to read config file: %w", err) | 281 | return fmt.Errorf("failed to read config file: %w", err) | ||
| 245 | } | 282 | } | ||
| 246 | - | 283 | + | ||
| 247 | // Parse and update only the routes section while preserving other formatting | 284 | // Parse and update only the routes section while preserving other formatting | ||
| 248 | lines := strings.Split(string(content), "\n") | 285 | lines := strings.Split(string(content), "\n") | ||
| 249 | var newLines []string | 286 | var newLines []string | ||
| 250 | inRoutesSection := false | 287 | inRoutesSection := false | ||
| 251 | routeAdded := false | 288 | routeAdded := false | ||
| 252 | - | 289 | + | ||
| 253 | for _, line := range lines { | 290 | for _, line := range lines { | ||
| 254 | trimmed := strings.TrimSpace(line) | 291 | trimmed := strings.TrimSpace(line) | ||
| 255 | - | 292 | + | ||
| 256 | // Check if we're entering or leaving the routes section | 293 | // Check if we're entering or leaving the routes section | ||
| 257 | if trimmed == "[routes]" { | 294 | if trimmed == "[routes]" { | ||
| 258 | inRoutesSection = true | 295 | inRoutesSection = true | ||
| 259 | newLines = append(newLines, line) | 296 | newLines = append(newLines, line) | ||
| 260 | continue | 297 | continue | ||
| 261 | } | 298 | } | ||
| 262 | - | 299 | + | ||
| 263 | // Check if we're entering a new section | 300 | // Check if we're entering a new section | ||
| 264 | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | 301 | if strings.HasPrefix(trimmed, "[") && trimmed != "[routes]" { | ||
| 265 | // If we were in routes section and haven't added the route yet, add it | 302 | // If we were in routes section and haven't added the route yet, add it | ||
| @@ -269,7 +306,7 @@ func (c *Config) AddRoute(domain, image string) error { | |||||
| 269 | } | 306 | } | ||
| 270 | inRoutesSection = false | 307 | inRoutesSection = false | ||
| 271 | } | 308 | } | ||
| 272 | - | 309 | + | ||
| 273 | // If we're in routes section, check if this route already exists (check both quote styles) | 310 | // If we're in routes section, check if this route already exists (check both quote styles) | ||
| 274 | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | 311 | if inRoutesSection && (strings.Contains(trimmed, fmt.Sprintf("'%s'", domain)) || strings.Contains(trimmed, fmt.Sprintf("\"%s\"", domain))) { | ||
| 275 | // Update existing route using double quotes for consistency | 312 | // Update existing route using double quotes for consistency | ||
| @@ -279,18 +316,18 @@ func (c *Config) AddRoute(domain, image string) error { | |||||
| 279 | newLines = append(newLines, line) | 316 | newLines = append(newLines, line) | ||
| 280 | } | 317 | } | ||
| 281 | } | 318 | } | ||
| 282 | - | 319 | + | ||
| 283 | // If we were in routes section at the end and haven't added the route yet, add it | 320 | // If we were in routes section at the end and haven't added the route yet, add it | ||
| 284 | if inRoutesSection && !routeAdded { | 321 | if inRoutesSection && !routeAdded { | ||
| 285 | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | 322 | newLines = append(newLines, fmt.Sprintf("\"%s\" = \"%s\"", domain, image)) | ||
| 286 | } | 323 | } | ||
| 287 | - | 324 | + | ||
| 288 | // Write the updated content back | 325 | // Write the updated content back | ||
| 289 | newContent := strings.Join(newLines, "\n") | 326 | newContent := strings.Join(newLines, "\n") | ||
| 290 | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | 327 | if err := os.WriteFile(configFile, []byte(newContent), 0644); err != nil { | ||
| 291 | return fmt.Errorf("failed to write config file: %w", err) | 328 | return fmt.Errorf("failed to write config file: %w", err) | ||
| 292 | } | 329 | } | ||
| 293 | - | 330 | + | ||
| 294 | log.Info(). | 331 | log.Info(). | ||
| 295 | Str("domain", domain). | 332 | Str("domain", domain). | ||
| 296 | Str("image", image). | 333 | Str("image", image). | ||
| @@ -298,7 +335,7 @@ func (c *Config) AddRoute(domain, image string) error { | |||||
| 298 | 335 | ||||
| 299 | // Create env file for the new route (if env loader is available) | 336 | // Create env file for the new route (if env loader is available) | ||
| 300 | // This will be called from the auto-route handler which has access to the env loader | 337 | // This will be called from the auto-route handler which has access to the env loader | ||
| 301 | - | 338 | + | ||
| 302 | return nil | 339 | return nil | ||
| 303 | } | 340 | } | ||
| 304 | 341 | ||||
| @@ -326,7 +363,7 @@ func validateEnvConfig(envCfg *EnvConfig) error { | |||||
| 326 | if providerName == "" { | 363 | if providerName == "" { | ||
| 327 | return fmt.Errorf("empty provider name in env.providers") | 364 | return fmt.Errorf("empty provider name in env.providers") | ||
| 328 | } | 365 | } | ||
| 329 | - | 366 | + | ||
| 330 | isValid := false | 367 | isValid := false | ||
| 331 | for _, validType := range validProviders { | 368 | for _, validType := range validProviders { | ||
| 332 | if providerName == validType { | 369 | if providerName == validType { | ||
| @@ -335,10 +372,10 @@ func validateEnvConfig(envCfg *EnvConfig) error { | |||||
| 335 | } | 372 | } | ||
| 336 | } | 373 | } | ||
| 337 | if !isValid { | 374 | if !isValid { | ||
| 338 | - | return fmt.Errorf("unsupported provider '%s' in env.providers. Supported providers: %s", | 375 | + | return fmt.Errorf("unsupported provider '%s' in env.providers. Supported providers: %s", |
| 339 | providerName, strings.Join(validProviders, ", ")) | 376 | providerName, strings.Join(validProviders, ", ")) | ||
| 340 | } | 377 | } | ||
| 341 | } | 378 | } | ||
| 342 | 379 | ||||
| 343 | return nil | 380 | return nil | ||
| 344 | - | }⏎̸ | 381 | + | } |
M internal/container/manager.go +57 -0
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | "gordon/internal/config" | |
| 12 | 12 | "gordon/internal/env" | |
| 13 | + | "gordon/internal/logging" | |
| 13 | 14 | "gordon/pkg/runtime" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| @@ -19,6 +20,7 @@ type Manager struct { | |||
| 19 | 20 | config *config.Config | |
| 20 | 21 | containers map[string]*runtime.Container // map[domain] -> container | |
| 21 | 22 | envLoader *env.Loader | |
| 23 | + | logManager *logging.LogManager | |
| 22 | 24 | mu sync.RWMutex | |
| 23 | 25 | } | |
| 24 | 26 | ||
| @@ -68,11 +70,15 @@ func NewManager(cfg *config.Config) (*Manager, error) { | |||
| 68 | 70 | log.Warn().Err(err).Msg("Failed to create env files for routes") | |
| 69 | 71 | } | |
| 70 | 72 | ||
| 73 | + | // Create log manager | |
| 74 | + | logManager := logging.NewLogManager(cfg, rt) | |
| 75 | + | ||
| 71 | 76 | return &Manager{ | |
| 72 | 77 | runtime: rt, | |
| 73 | 78 | config: cfg, | |
| 74 | 79 | containers: make(map[string]*runtime.Container), | |
| 75 | 80 | envLoader: envLoader, | |
| 81 | + | logManager: logManager, | |
| 76 | 82 | }, nil | |
| 77 | 83 | } | |
| 78 | 84 | ||
| @@ -253,6 +259,19 @@ func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*run | |||
| 253 | 259 | // Store the container mapping | |
| 254 | 260 | m.containers[route.Domain] = container | |
| 255 | 261 | ||
| 262 | + | // Start log collection for the container | |
| 263 | + | containerName := container.Name | |
| 264 | + | if containerName == "" { | |
| 265 | + | containerName = route.Domain | |
| 266 | + | } | |
| 267 | + | if err := m.logManager.StartCollection(container.ID, containerName); err != nil { | |
| 268 | + | log.Warn(). | |
| 269 | + | Err(err). | |
| 270 | + | Str("container", container.ID). | |
| 271 | + | Str("domain", route.Domain). | |
| 272 | + | Msg("Failed to start log collection for container") | |
| 273 | + | } | |
| 274 | + | ||
| 256 | 275 | log.Info(). | |
| 257 | 276 | Str("domain", route.Domain). | |
| 258 | 277 | Str("image", route.Image). | |
| @@ -328,6 +347,9 @@ func (m *Manager) StopContainer(ctx context.Context, containerID string) error { | |||
| 328 | 347 | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |
| 329 | 348 | } | |
| 330 | 349 | ||
| 350 | + | // Stop log collection for the container | |
| 351 | + | m.logManager.StopCollection(containerID) | |
| 352 | + | ||
| 331 | 353 | log.Info().Str("container", containerID).Msg("Container stopped") | |
| 332 | 354 | return nil | |
| 333 | 355 | } | |
| @@ -351,6 +373,9 @@ func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force | |||
| 351 | 373 | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | |
| 352 | 374 | } | |
| 353 | 375 | ||
| 376 | + | // Stop log collection for the container | |
| 377 | + | m.logManager.StopCollection(containerID) | |
| 378 | + | ||
| 354 | 379 | // Remove from our tracking map | |
| 355 | 380 | m.mu.Lock() | |
| 356 | 381 | defer m.mu.Unlock() | |
| @@ -481,6 +506,19 @@ func (m *Manager) AutoStartContainers(ctx context.Context) error { | |||
| 481 | 506 | m.containers[route.Domain] = container | |
| 482 | 507 | m.mu.Unlock() | |
| 483 | 508 | startedCount++ | |
| 509 | + | ||
| 510 | + | // Start log collection for the container | |
| 511 | + | containerName := container.Name | |
| 512 | + | if containerName == "" { | |
| 513 | + | containerName = route.Domain | |
| 514 | + | } | |
| 515 | + | if err := m.logManager.StartCollection(container.ID, containerName); err != nil { | |
| 516 | + | log.Warn(). | |
| 517 | + | Err(err). | |
| 518 | + | Str("container", container.ID). | |
| 519 | + | Str("domain", route.Domain). | |
| 520 | + | Msg("Failed to start log collection for auto-started container") | |
| 521 | + | } | |
| 484 | 522 | ||
| 485 | 523 | log.Info().Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Existing Gordon-managed container started successfully") | |
| 486 | 524 | continue | |
| @@ -581,6 +619,9 @@ func (m *Manager) StopAllManagedContainers(ctx context.Context) error { | |||
| 581 | 619 | continue | |
| 582 | 620 | } | |
| 583 | 621 | ||
| 622 | + | // Stop log collection for the container | |
| 623 | + | m.logManager.StopCollection(container.ID) | |
| 624 | + | ||
| 584 | 625 | // Remove from tracking | |
| 585 | 626 | m.mu.Lock() | |
| 586 | 627 | delete(m.containers, domain) | |
| @@ -655,3 +696,19 @@ func normalizeImageRef(imageRef string) string { | |||
| 655 | 696 | ||
| 656 | 697 | return image + ":" + tag | |
| 657 | 698 | } | |
| 699 | + | ||
| 700 | + | // Shutdown gracefully shuts down the container manager | |
| 701 | + | func (m *Manager) Shutdown(ctx context.Context) error { | |
| 702 | + | log.Info().Msg("Shutting down container manager...") | |
| 703 | + | ||
| 704 | + | // Stop all managed containers (this also stops log collection for each) | |
| 705 | + | if err := m.StopAllManagedContainers(ctx); err != nil { | |
| 706 | + | log.Error().Err(err).Msg("Failed to stop all managed containers during shutdown") | |
| 707 | + | } | |
| 708 | + | ||
| 709 | + | // Stop all remaining log collection | |
| 710 | + | m.logManager.StopAll() | |
| 711 | + | ||
| 712 | + | log.Info().Msg("Container manager shutdown complete") | |
| 713 | + | return nil | |
| 714 | + | } | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | "gordon/internal/config" | 11 | "gordon/internal/config" | ||
| 12 | "gordon/internal/env" | 12 | "gordon/internal/env" | ||
| 13 | + | "gordon/internal/logging" | |||
| 13 | "gordon/pkg/runtime" | 14 | "gordon/pkg/runtime" | ||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| @@ -19,6 +20,7 @@ type Manager struct { | |||||
| 19 | config *config.Config | 20 | config *config.Config | ||
| 20 | containers map[string]*runtime.Container // map[domain] -> container | 21 | containers map[string]*runtime.Container // map[domain] -> container | ||
| 21 | envLoader *env.Loader | 22 | envLoader *env.Loader | ||
| 23 | + | logManager *logging.LogManager | |||
| 22 | mu sync.RWMutex | 24 | mu sync.RWMutex | ||
| 23 | } | 25 | } | ||
| 24 | 26 | ||||
| @@ -68,11 +70,15 @@ func NewManager(cfg *config.Config) (*Manager, error) { | |||||
| 68 | log.Warn().Err(err).Msg("Failed to create env files for routes") | 70 | log.Warn().Err(err).Msg("Failed to create env files for routes") | ||
| 69 | } | 71 | } | ||
| 70 | 72 | ||||
| 73 | + | // Create log manager | |||
| 74 | + | logManager := logging.NewLogManager(cfg, rt) | |||
| 75 | + | ||||
| 71 | return &Manager{ | 76 | return &Manager{ | ||
| 72 | runtime: rt, | 77 | runtime: rt, | ||
| 73 | config: cfg, | 78 | config: cfg, | ||
| 74 | containers: make(map[string]*runtime.Container), | 79 | containers: make(map[string]*runtime.Container), | ||
| 75 | envLoader: envLoader, | 80 | envLoader: envLoader, | ||
| 81 | + | logManager: logManager, | |||
| 76 | }, nil | 82 | }, nil | ||
| 77 | } | 83 | } | ||
| 78 | 84 | ||||
| @@ -253,6 +259,19 @@ func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*run | |||||
| 253 | // Store the container mapping | 259 | // Store the container mapping | ||
| 254 | m.containers[route.Domain] = container | 260 | m.containers[route.Domain] = container | ||
| 255 | 261 | ||||
| 262 | + | // Start log collection for the container | |||
| 263 | + | containerName := container.Name | |||
| 264 | + | if containerName == "" { | |||
| 265 | + | containerName = route.Domain | |||
| 266 | + | } | |||
| 267 | + | if err := m.logManager.StartCollection(container.ID, containerName); err != nil { | |||
| 268 | + | log.Warn(). | |||
| 269 | + | Err(err). | |||
| 270 | + | Str("container", container.ID). | |||
| 271 | + | Str("domain", route.Domain). | |||
| 272 | + | Msg("Failed to start log collection for container") | |||
| 273 | + | } | |||
| 274 | + | ||||
| 256 | log.Info(). | 275 | log.Info(). | ||
| 257 | Str("domain", route.Domain). | 276 | Str("domain", route.Domain). | ||
| 258 | Str("image", route.Image). | 277 | Str("image", route.Image). | ||
| @@ -328,6 +347,9 @@ func (m *Manager) StopContainer(ctx context.Context, containerID string) error { | |||||
| 328 | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | 347 | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | ||
| 329 | } | 348 | } | ||
| 330 | 349 | ||||
| 350 | + | // Stop log collection for the container | |||
| 351 | + | m.logManager.StopCollection(containerID) | |||
| 352 | + | ||||
| 331 | log.Info().Str("container", containerID).Msg("Container stopped") | 353 | log.Info().Str("container", containerID).Msg("Container stopped") | ||
| 332 | return nil | 354 | return nil | ||
| 333 | } | 355 | } | ||
| @@ -351,6 +373,9 @@ func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force | |||||
| 351 | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | 373 | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | ||
| 352 | } | 374 | } | ||
| 353 | 375 | ||||
| 376 | + | // Stop log collection for the container | |||
| 377 | + | m.logManager.StopCollection(containerID) | |||
| 378 | + | ||||
| 354 | // Remove from our tracking map | 379 | // Remove from our tracking map | ||
| 355 | m.mu.Lock() | 380 | m.mu.Lock() | ||
| 356 | defer m.mu.Unlock() | 381 | defer m.mu.Unlock() | ||
| @@ -481,6 +506,19 @@ func (m *Manager) AutoStartContainers(ctx context.Context) error { | |||||
| 481 | m.containers[route.Domain] = container | 506 | m.containers[route.Domain] = container | ||
| 482 | m.mu.Unlock() | 507 | m.mu.Unlock() | ||
| 483 | startedCount++ | 508 | startedCount++ | ||
| 509 | + | ||||
| 510 | + | // Start log collection for the container | |||
| 511 | + | containerName := container.Name | |||
| 512 | + | if containerName == "" { | |||
| 513 | + | containerName = route.Domain | |||
| 514 | + | } | |||
| 515 | + | if err := m.logManager.StartCollection(container.ID, containerName); err != nil { | |||
| 516 | + | log.Warn(). | |||
| 517 | + | Err(err). | |||
| 518 | + | Str("container", container.ID). | |||
| 519 | + | Str("domain", route.Domain). | |||
| 520 | + | Msg("Failed to start log collection for auto-started container") | |||
| 521 | + | } | |||
| 484 | 522 | ||||
| 485 | log.Info().Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Existing Gordon-managed container started successfully") | 523 | log.Info().Str("domain", route.Domain).Str("container", existingContainer.ID).Msg("Existing Gordon-managed container started successfully") | ||
| 486 | continue | 524 | continue | ||
| @@ -581,6 +619,9 @@ func (m *Manager) StopAllManagedContainers(ctx context.Context) error { | |||||
| 581 | continue | 619 | continue | ||
| 582 | } | 620 | } | ||
| 583 | 621 | ||||
| 622 | + | // Stop log collection for the container | |||
| 623 | + | m.logManager.StopCollection(container.ID) | |||
| 624 | + | ||||
| 584 | // Remove from tracking | 625 | // Remove from tracking | ||
| 585 | m.mu.Lock() | 626 | m.mu.Lock() | ||
| 586 | delete(m.containers, domain) | 627 | delete(m.containers, domain) | ||
| @@ -655,3 +696,19 @@ func normalizeImageRef(imageRef string) string { | |||||
| 655 | 696 | ||||
| 656 | return image + ":" + tag | 697 | return image + ":" + tag | ||
| 657 | } | 698 | } | ||
| 699 | + | ||||
| 700 | + | // Shutdown gracefully shuts down the container manager | |||
| 701 | + | func (m *Manager) Shutdown(ctx context.Context) error { | |||
| 702 | + | log.Info().Msg("Shutting down container manager...") | |||
| 703 | + | ||||
| 704 | + | // Stop all managed containers (this also stops log collection for each) | |||
| 705 | + | if err := m.StopAllManagedContainers(ctx); err != nil { | |||
| 706 | + | log.Error().Err(err).Msg("Failed to stop all managed containers during shutdown") | |||
| 707 | + | } | |||
| 708 | + | ||||
| 709 | + | // Stop all remaining log collection | |||
| 710 | + | m.logManager.StopAll() | |||
| 711 | + | ||||
| 712 | + | log.Info().Msg("Container manager shutdown complete") | |||
| 713 | + | return nil | |||
| 714 | + | } | |||
A internal/logging/collector.go +236 -0
| @@ -0,0 +1,236 @@ | |||
| 1 | + | package logging | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bufio" | |
| 5 | + | "context" | |
| 6 | + | "fmt" | |
| 7 | + | "sync" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "gordon/internal/config" | |
| 11 | + | "gordon/pkg/runtime" | |
| 12 | + | ||
| 13 | + | "github.com/rs/zerolog/log" | |
| 14 | + | "gopkg.in/natefinch/lumberjack.v2" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // LogCollector manages log collection for a single container | |
| 18 | + | type LogCollector struct { | |
| 19 | + | containerID string | |
| 20 | + | containerName string | |
| 21 | + | runtime runtime.Runtime | |
| 22 | + | logWriter *lumberjack.Logger | |
| 23 | + | ctx context.Context | |
| 24 | + | cancel context.CancelFunc | |
| 25 | + | wg sync.WaitGroup | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // LogManager manages all container log collectors | |
| 29 | + | type LogManager struct { | |
| 30 | + | collectors map[string]*LogCollector | |
| 31 | + | config *config.Config | |
| 32 | + | runtime runtime.Runtime | |
| 33 | + | mu sync.RWMutex | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // NewLogManager creates a new log manager | |
| 37 | + | func NewLogManager(cfg *config.Config, rt runtime.Runtime) *LogManager { | |
| 38 | + | return &LogManager{ | |
| 39 | + | collectors: make(map[string]*LogCollector), | |
| 40 | + | config: cfg, | |
| 41 | + | runtime: rt, | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // StartCollection starts log collection for a container | |
| 46 | + | func (lm *LogManager) StartCollection(containerID, containerName string) error { | |
| 47 | + | if !lm.config.Logging.Enabled { | |
| 48 | + | return nil // Logging disabled, nothing to do | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | lm.mu.Lock() | |
| 52 | + | defer lm.mu.Unlock() | |
| 53 | + | ||
| 54 | + | // Stop existing collector if it exists | |
| 55 | + | if existing, exists := lm.collectors[containerID]; exists { | |
| 56 | + | log.Debug().Str("container_id", containerID).Msg("Stopping existing log collector") | |
| 57 | + | existing.Stop() | |
| 58 | + | delete(lm.collectors, containerID) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Create log writer for this container | |
| 62 | + | logWriter, err := GetContainerLogWriter(lm.config, containerID, containerName) | |
| 63 | + | if err != nil { | |
| 64 | + | return fmt.Errorf("failed to create log writer for container %s: %w", containerID, err) | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Create collector context | |
| 68 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 69 | + | ||
| 70 | + | collector := &LogCollector{ | |
| 71 | + | containerID: containerID, | |
| 72 | + | containerName: containerName, | |
| 73 | + | runtime: lm.runtime, | |
| 74 | + | logWriter: logWriter, | |
| 75 | + | ctx: ctx, | |
| 76 | + | cancel: cancel, | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Start collection goroutine | |
| 80 | + | collector.wg.Add(1) | |
| 81 | + | go collector.collectLogs() | |
| 82 | + | ||
| 83 | + | // Store collector | |
| 84 | + | lm.collectors[containerID] = collector | |
| 85 | + | ||
| 86 | + | log.Info(). | |
| 87 | + | Str("container_id", containerID). | |
| 88 | + | Str("container_name", containerName). | |
| 89 | + | Msg("Started container log collection") | |
| 90 | + | ||
| 91 | + | return nil | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // StopCollection stops log collection for a container | |
| 95 | + | func (lm *LogManager) StopCollection(containerID string) { | |
| 96 | + | lm.mu.Lock() | |
| 97 | + | defer lm.mu.Unlock() | |
| 98 | + | ||
| 99 | + | if collector, exists := lm.collectors[containerID]; exists { | |
| 100 | + | log.Debug().Str("container_id", containerID).Msg("Stopping log collection") | |
| 101 | + | collector.Stop() | |
| 102 | + | delete(lm.collectors, containerID) | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // StopAll stops all log collection | |
| 107 | + | func (lm *LogManager) StopAll() { | |
| 108 | + | lm.mu.Lock() | |
| 109 | + | defer lm.mu.Unlock() | |
| 110 | + | ||
| 111 | + | for containerID, collector := range lm.collectors { | |
| 112 | + | log.Debug().Str("container_id", containerID).Msg("Stopping log collection") | |
| 113 | + | collector.Stop() | |
| 114 | + | } | |
| 115 | + | lm.collectors = make(map[string]*LogCollector) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // GetActiveCollectors returns the number of active collectors | |
| 119 | + | func (lm *LogManager) GetActiveCollectors() int { | |
| 120 | + | lm.mu.RLock() | |
| 121 | + | defer lm.mu.RUnlock() | |
| 122 | + | return len(lm.collectors) | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // collectLogs runs the log collection loop for a single container | |
| 126 | + | func (lc *LogCollector) collectLogs() { | |
| 127 | + | defer lc.wg.Done() | |
| 128 | + | ||
| 129 | + | log.Debug(). | |
| 130 | + | Str("container_id", lc.containerID). | |
| 131 | + | Str("container_name", lc.containerName). | |
| 132 | + | Msg("Starting log collection goroutine") | |
| 133 | + | ||
| 134 | + | // Retry loop for log collection | |
| 135 | + | retryDelay := time.Second | |
| 136 | + | maxRetryDelay := 30 * time.Second | |
| 137 | + | ||
| 138 | + | for { | |
| 139 | + | select { | |
| 140 | + | case <-lc.ctx.Done(): | |
| 141 | + | log.Debug().Str("container_id", lc.containerID).Msg("Log collection context cancelled") | |
| 142 | + | return | |
| 143 | + | default: | |
| 144 | + | // Attempt to collect logs | |
| 145 | + | if err := lc.collectLogsOnce(); err != nil { | |
| 146 | + | log.Warn(). | |
| 147 | + | Err(err). | |
| 148 | + | Str("container_id", lc.containerID). | |
| 149 | + | Dur("retry_delay", retryDelay). | |
| 150 | + | Msg("Log collection failed, retrying") | |
| 151 | + | ||
| 152 | + | // Wait before retrying, with exponential backoff | |
| 153 | + | select { | |
| 154 | + | case <-lc.ctx.Done(): | |
| 155 | + | return | |
| 156 | + | case <-time.After(retryDelay): | |
| 157 | + | // Increase retry delay, up to maximum | |
| 158 | + | retryDelay *= 2 | |
| 159 | + | if retryDelay > maxRetryDelay { | |
| 160 | + | retryDelay = maxRetryDelay | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | continue | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // Reset retry delay on success | |
| 167 | + | retryDelay = time.Second | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // collectLogsOnce attempts to collect logs once | |
| 173 | + | func (lc *LogCollector) collectLogsOnce() error { | |
| 174 | + | // Get log stream from runtime | |
| 175 | + | logStream, err := lc.runtime.GetContainerLogs(lc.ctx, lc.containerID, true) | |
| 176 | + | if err != nil { | |
| 177 | + | return fmt.Errorf("failed to get log stream: %w", err) | |
| 178 | + | } | |
| 179 | + | defer logStream.Close() | |
| 180 | + | ||
| 181 | + | log.Debug().Str("container_id", lc.containerID).Msg("Started reading container logs") | |
| 182 | + | ||
| 183 | + | // Read logs line by line and write to file | |
| 184 | + | scanner := bufio.NewScanner(logStream) | |
| 185 | + | for scanner.Scan() { | |
| 186 | + | select { | |
| 187 | + | case <-lc.ctx.Done(): | |
| 188 | + | return nil | |
| 189 | + | default: | |
| 190 | + | // Get log line | |
| 191 | + | line := scanner.Text() | |
| 192 | + | ||
| 193 | + | // Add timestamp and write to log file | |
| 194 | + | timestampedLine := fmt.Sprintf("%s %s\n", | |
| 195 | + | time.Now().Format(time.RFC3339), | |
| 196 | + | line) | |
| 197 | + | ||
| 198 | + | if _, err := lc.logWriter.Write([]byte(timestampedLine)); err != nil { | |
| 199 | + | log.Warn(). | |
| 200 | + | Err(err). | |
| 201 | + | Str("container_id", lc.containerID). | |
| 202 | + | Msg("Failed to write container log line") | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | // Check for scanner errors | |
| 208 | + | if err := scanner.Err(); err != nil { | |
| 209 | + | return fmt.Errorf("error reading log stream: %w", err) | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | return nil | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // Stop gracefully stops the log collector | |
| 216 | + | func (lc *LogCollector) Stop() { | |
| 217 | + | log.Debug().Str("container_id", lc.containerID).Msg("Stopping log collector") | |
| 218 | + | ||
| 219 | + | // Cancel context to stop goroutines | |
| 220 | + | lc.cancel() | |
| 221 | + | ||
| 222 | + | // Wait for collection goroutine to finish | |
| 223 | + | lc.wg.Wait() | |
| 224 | + | ||
| 225 | + | // Close log writer | |
| 226 | + | if lc.logWriter != nil { | |
| 227 | + | if err := lc.logWriter.Close(); err != nil { | |
| 228 | + | log.Warn(). | |
| 229 | + | Err(err). | |
| 230 | + | Str("container_id", lc.containerID). | |
| 231 | + | Msg("Failed to close log writer") | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | log.Debug().Str("container_id", lc.containerID).Msg("Log collector stopped") | |
| 236 | + | } | |
| @@ -0,0 +1,236 @@ | |||||
| 1 | + | package logging | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bufio" | |||
| 5 | + | "context" | |||
| 6 | + | "fmt" | |||
| 7 | + | "sync" | |||
| 8 | + | "time" | |||
| 9 | + | ||||
| 10 | + | "gordon/internal/config" | |||
| 11 | + | "gordon/pkg/runtime" | |||
| 12 | + | ||||
| 13 | + | "github.com/rs/zerolog/log" | |||
| 14 | + | "gopkg.in/natefinch/lumberjack.v2" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // LogCollector manages log collection for a single container | |||
| 18 | + | type LogCollector struct { | |||
| 19 | + | containerID string | |||
| 20 | + | containerName string | |||
| 21 | + | runtime runtime.Runtime | |||
| 22 | + | logWriter *lumberjack.Logger | |||
| 23 | + | ctx context.Context | |||
| 24 | + | cancel context.CancelFunc | |||
| 25 | + | wg sync.WaitGroup | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // LogManager manages all container log collectors | |||
| 29 | + | type LogManager struct { | |||
| 30 | + | collectors map[string]*LogCollector | |||
| 31 | + | config *config.Config | |||
| 32 | + | runtime runtime.Runtime | |||
| 33 | + | mu sync.RWMutex | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // NewLogManager creates a new log manager | |||
| 37 | + | func NewLogManager(cfg *config.Config, rt runtime.Runtime) *LogManager { | |||
| 38 | + | return &LogManager{ | |||
| 39 | + | collectors: make(map[string]*LogCollector), | |||
| 40 | + | config: cfg, | |||
| 41 | + | runtime: rt, | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // StartCollection starts log collection for a container | |||
| 46 | + | func (lm *LogManager) StartCollection(containerID, containerName string) error { | |||
| 47 | + | if !lm.config.Logging.Enabled { | |||
| 48 | + | return nil // Logging disabled, nothing to do | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | lm.mu.Lock() | |||
| 52 | + | defer lm.mu.Unlock() | |||
| 53 | + | ||||
| 54 | + | // Stop existing collector if it exists | |||
| 55 | + | if existing, exists := lm.collectors[containerID]; exists { | |||
| 56 | + | log.Debug().Str("container_id", containerID).Msg("Stopping existing log collector") | |||
| 57 | + | existing.Stop() | |||
| 58 | + | delete(lm.collectors, containerID) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Create log writer for this container | |||
| 62 | + | logWriter, err := GetContainerLogWriter(lm.config, containerID, containerName) | |||
| 63 | + | if err != nil { | |||
| 64 | + | return fmt.Errorf("failed to create log writer for container %s: %w", containerID, err) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Create collector context | |||
| 68 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 69 | + | ||||
| 70 | + | collector := &LogCollector{ | |||
| 71 | + | containerID: containerID, | |||
| 72 | + | containerName: containerName, | |||
| 73 | + | runtime: lm.runtime, | |||
| 74 | + | logWriter: logWriter, | |||
| 75 | + | ctx: ctx, | |||
| 76 | + | cancel: cancel, | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // Start collection goroutine | |||
| 80 | + | collector.wg.Add(1) | |||
| 81 | + | go collector.collectLogs() | |||
| 82 | + | ||||
| 83 | + | // Store collector | |||
| 84 | + | lm.collectors[containerID] = collector | |||
| 85 | + | ||||
| 86 | + | log.Info(). | |||
| 87 | + | Str("container_id", containerID). | |||
| 88 | + | Str("container_name", containerName). | |||
| 89 | + | Msg("Started container log collection") | |||
| 90 | + | ||||
| 91 | + | return nil | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // StopCollection stops log collection for a container | |||
| 95 | + | func (lm *LogManager) StopCollection(containerID string) { | |||
| 96 | + | lm.mu.Lock() | |||
| 97 | + | defer lm.mu.Unlock() | |||
| 98 | + | ||||
| 99 | + | if collector, exists := lm.collectors[containerID]; exists { | |||
| 100 | + | log.Debug().Str("container_id", containerID).Msg("Stopping log collection") | |||
| 101 | + | collector.Stop() | |||
| 102 | + | delete(lm.collectors, containerID) | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // StopAll stops all log collection | |||
| 107 | + | func (lm *LogManager) StopAll() { | |||
| 108 | + | lm.mu.Lock() | |||
| 109 | + | defer lm.mu.Unlock() | |||
| 110 | + | ||||
| 111 | + | for containerID, collector := range lm.collectors { | |||
| 112 | + | log.Debug().Str("container_id", containerID).Msg("Stopping log collection") | |||
| 113 | + | collector.Stop() | |||
| 114 | + | } | |||
| 115 | + | lm.collectors = make(map[string]*LogCollector) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // GetActiveCollectors returns the number of active collectors | |||
| 119 | + | func (lm *LogManager) GetActiveCollectors() int { | |||
| 120 | + | lm.mu.RLock() | |||
| 121 | + | defer lm.mu.RUnlock() | |||
| 122 | + | return len(lm.collectors) | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // collectLogs runs the log collection loop for a single container | |||
| 126 | + | func (lc *LogCollector) collectLogs() { | |||
| 127 | + | defer lc.wg.Done() | |||
| 128 | + | ||||
| 129 | + | log.Debug(). | |||
| 130 | + | Str("container_id", lc.containerID). | |||
| 131 | + | Str("container_name", lc.containerName). | |||
| 132 | + | Msg("Starting log collection goroutine") | |||
| 133 | + | ||||
| 134 | + | // Retry loop for log collection | |||
| 135 | + | retryDelay := time.Second | |||
| 136 | + | maxRetryDelay := 30 * time.Second | |||
| 137 | + | ||||
| 138 | + | for { | |||
| 139 | + | select { | |||
| 140 | + | case <-lc.ctx.Done(): | |||
| 141 | + | log.Debug().Str("container_id", lc.containerID).Msg("Log collection context cancelled") | |||
| 142 | + | return | |||
| 143 | + | default: | |||
| 144 | + | // Attempt to collect logs | |||
| 145 | + | if err := lc.collectLogsOnce(); err != nil { | |||
| 146 | + | log.Warn(). | |||
| 147 | + | Err(err). | |||
| 148 | + | Str("container_id", lc.containerID). | |||
| 149 | + | Dur("retry_delay", retryDelay). | |||
| 150 | + | Msg("Log collection failed, retrying") | |||
| 151 | + | ||||
| 152 | + | // Wait before retrying, with exponential backoff | |||
| 153 | + | select { | |||
| 154 | + | case <-lc.ctx.Done(): | |||
| 155 | + | return | |||
| 156 | + | case <-time.After(retryDelay): | |||
| 157 | + | // Increase retry delay, up to maximum | |||
| 158 | + | retryDelay *= 2 | |||
| 159 | + | if retryDelay > maxRetryDelay { | |||
| 160 | + | retryDelay = maxRetryDelay | |||
| 161 | + | } | |||
| 162 | + | } | |||
| 163 | + | continue | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // Reset retry delay on success | |||
| 167 | + | retryDelay = time.Second | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // collectLogsOnce attempts to collect logs once | |||
| 173 | + | func (lc *LogCollector) collectLogsOnce() error { | |||
| 174 | + | // Get log stream from runtime | |||
| 175 | + | logStream, err := lc.runtime.GetContainerLogs(lc.ctx, lc.containerID, true) | |||
| 176 | + | if err != nil { | |||
| 177 | + | return fmt.Errorf("failed to get log stream: %w", err) | |||
| 178 | + | } | |||
| 179 | + | defer logStream.Close() | |||
| 180 | + | ||||
| 181 | + | log.Debug().Str("container_id", lc.containerID).Msg("Started reading container logs") | |||
| 182 | + | ||||
| 183 | + | // Read logs line by line and write to file | |||
| 184 | + | scanner := bufio.NewScanner(logStream) | |||
| 185 | + | for scanner.Scan() { | |||
| 186 | + | select { | |||
| 187 | + | case <-lc.ctx.Done(): | |||
| 188 | + | return nil | |||
| 189 | + | default: | |||
| 190 | + | // Get log line | |||
| 191 | + | line := scanner.Text() | |||
| 192 | + | ||||
| 193 | + | // Add timestamp and write to log file | |||
| 194 | + | timestampedLine := fmt.Sprintf("%s %s\n", | |||
| 195 | + | time.Now().Format(time.RFC3339), | |||
| 196 | + | line) | |||
| 197 | + | ||||
| 198 | + | if _, err := lc.logWriter.Write([]byte(timestampedLine)); err != nil { | |||
| 199 | + | log.Warn(). | |||
| 200 | + | Err(err). | |||
| 201 | + | Str("container_id", lc.containerID). | |||
| 202 | + | Msg("Failed to write container log line") | |||
| 203 | + | } | |||
| 204 | + | } | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | // Check for scanner errors | |||
| 208 | + | if err := scanner.Err(); err != nil { | |||
| 209 | + | return fmt.Errorf("error reading log stream: %w", err) | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | return nil | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // Stop gracefully stops the log collector | |||
| 216 | + | func (lc *LogCollector) Stop() { | |||
| 217 | + | log.Debug().Str("container_id", lc.containerID).Msg("Stopping log collector") | |||
| 218 | + | ||||
| 219 | + | // Cancel context to stop goroutines | |||
| 220 | + | lc.cancel() | |||
| 221 | + | ||||
| 222 | + | // Wait for collection goroutine to finish | |||
| 223 | + | lc.wg.Wait() | |||
| 224 | + | ||||
| 225 | + | // Close log writer | |||
| 226 | + | if lc.logWriter != nil { | |||
| 227 | + | if err := lc.logWriter.Close(); err != nil { | |||
| 228 | + | log.Warn(). | |||
| 229 | + | Err(err). | |||
| 230 | + | Str("container_id", lc.containerID). | |||
| 231 | + | Msg("Failed to close log writer") | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | log.Debug().Str("container_id", lc.containerID).Msg("Log collector stopped") | |||
| 236 | + | } | |||
A internal/logging/setup.go +161 -0
| @@ -0,0 +1,161 @@ | |||
| 1 | + | package logging | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "io" | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | ||
| 9 | + | "github.com/rs/zerolog" | |
| 10 | + | "github.com/rs/zerolog/log" | |
| 11 | + | "gopkg.in/natefinch/lumberjack.v2" | |
| 12 | + | "gordon/internal/config" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | var ( | |
| 16 | + | MainLogger zerolog.Logger | |
| 17 | + | ProxyLogger zerolog.Logger | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // Setup initializes the logging system based on the configuration | |
| 21 | + | func Setup(cfg *config.Config) error { | |
| 22 | + | if !cfg.Logging.Enabled { | |
| 23 | + | // Keep console logging only | |
| 24 | + | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |
| 25 | + | MainLogger = log.Logger | |
| 26 | + | ProxyLogger = log.Logger | |
| 27 | + | return nil | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // Create logs directory with secure permissions (0700 - owner only) | |
| 31 | + | if err := os.MkdirAll(cfg.Logging.Dir, 0700); err != nil { | |
| 32 | + | return fmt.Errorf("failed to create logs directory: %w", err) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Create container logs directory with secure permissions | |
| 36 | + | containerLogDir := filepath.Join(cfg.Logging.Dir, cfg.Logging.ContainerLogDir) | |
| 37 | + | if err := os.MkdirAll(containerLogDir, 0700); err != nil { | |
| 38 | + | return fmt.Errorf("failed to create container logs directory: %w", err) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Set log level | |
| 42 | + | level, err := zerolog.ParseLevel(cfg.Logging.Level) | |
| 43 | + | if err != nil { | |
| 44 | + | level = zerolog.InfoLevel | |
| 45 | + | log.Warn().Str("invalid_level", cfg.Logging.Level).Msg("Invalid log level, using info") | |
| 46 | + | } | |
| 47 | + | zerolog.SetGlobalLevel(level) | |
| 48 | + | ||
| 49 | + | // Configure time format | |
| 50 | + | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |
| 51 | + | ||
| 52 | + | // Setup main application logger | |
| 53 | + | mainLogFile := filepath.Join(cfg.Logging.Dir, cfg.Logging.MainLogFile) | |
| 54 | + | mainFileWriter := &lumberjack.Logger{ | |
| 55 | + | Filename: mainLogFile, | |
| 56 | + | MaxSize: cfg.Logging.MaxSize, | |
| 57 | + | MaxBackups: cfg.Logging.MaxBackups, | |
| 58 | + | MaxAge: cfg.Logging.MaxAge, | |
| 59 | + | Compress: cfg.Logging.Compress, | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Set file permissions to be secure (readable only by owner) | |
| 63 | + | if err := os.Chmod(mainLogFile, 0600); err != nil && !os.IsNotExist(err) { | |
| 64 | + | log.Warn().Err(err).Str("file", mainLogFile).Msg("Failed to set secure permissions on log file") | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Setup proxy logger | |
| 68 | + | proxyLogFile := filepath.Join(cfg.Logging.Dir, cfg.Logging.ProxyLogFile) | |
| 69 | + | proxyFileWriter := &lumberjack.Logger{ | |
| 70 | + | Filename: proxyLogFile, | |
| 71 | + | MaxSize: cfg.Logging.MaxSize, | |
| 72 | + | MaxBackups: cfg.Logging.MaxBackups, | |
| 73 | + | MaxAge: cfg.Logging.MaxAge, | |
| 74 | + | Compress: cfg.Logging.Compress, | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // Set file permissions to be secure (readable only by owner) | |
| 78 | + | if err := os.Chmod(proxyLogFile, 0600); err != nil && !os.IsNotExist(err) { | |
| 79 | + | log.Warn().Err(err).Str("file", proxyLogFile).Msg("Failed to set secure permissions on log file") | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Create multi-writers (console + file) | |
| 83 | + | consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} | |
| 84 | + | ||
| 85 | + | mainMultiWriter := io.MultiWriter(consoleWriter, mainFileWriter) | |
| 86 | + | proxyMultiWriter := io.MultiWriter(consoleWriter, proxyFileWriter) | |
| 87 | + | ||
| 88 | + | // Initialize loggers | |
| 89 | + | MainLogger = zerolog.New(mainMultiWriter).With().Timestamp().Logger() | |
| 90 | + | ProxyLogger = zerolog.New(proxyMultiWriter).With().Timestamp().Logger() | |
| 91 | + | ||
| 92 | + | // Set the global logger to the main logger | |
| 93 | + | log.Logger = MainLogger | |
| 94 | + | ||
| 95 | + | log.Info(). | |
| 96 | + | Str("main_log", mainLogFile). | |
| 97 | + | Str("proxy_log", proxyLogFile). | |
| 98 | + | Str("container_log_dir", containerLogDir). | |
| 99 | + | Str("level", level.String()). | |
| 100 | + | Msg("File logging initialized") | |
| 101 | + | ||
| 102 | + | return nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // GetContainerLogWriter returns a log writer for a specific container | |
| 106 | + | func GetContainerLogWriter(cfg *config.Config, containerID string, containerName string) (*lumberjack.Logger, error) { | |
| 107 | + | if !cfg.Logging.Enabled { | |
| 108 | + | return nil, fmt.Errorf("logging is disabled") | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | containerLogDir := filepath.Join(cfg.Logging.Dir, cfg.Logging.ContainerLogDir) | |
| 112 | + | ||
| 113 | + | // Ensure container log directory exists with secure permissions | |
| 114 | + | if err := os.MkdirAll(containerLogDir, 0700); err != nil { | |
| 115 | + | return nil, fmt.Errorf("failed to create container log directory: %w", err) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // Use container ID as the main log file name | |
| 119 | + | logFile := filepath.Join(containerLogDir, fmt.Sprintf("%s.log", containerID)) | |
| 120 | + | ||
| 121 | + | writer := &lumberjack.Logger{ | |
| 122 | + | Filename: logFile, | |
| 123 | + | MaxSize: cfg.Logging.MaxSize, | |
| 124 | + | MaxBackups: cfg.Logging.MaxBackups, | |
| 125 | + | MaxAge: cfg.Logging.MaxAge, | |
| 126 | + | Compress: cfg.Logging.Compress, | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // Set file permissions to be secure (readable only by owner) | |
| 130 | + | if err := os.Chmod(logFile, 0600); err != nil && !os.IsNotExist(err) { | |
| 131 | + | log.Warn().Err(err).Str("file", logFile).Msg("Failed to set secure permissions on container log file") | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // Create a symlink for easier access using container name (if provided) | |
| 135 | + | if containerName != "" && containerName != containerID { | |
| 136 | + | symlinkPath := filepath.Join(containerLogDir, fmt.Sprintf("%s.log", containerName)) | |
| 137 | + | ||
| 138 | + | // Remove existing symlink if it exists | |
| 139 | + | if _, err := os.Lstat(symlinkPath); err == nil { | |
| 140 | + | os.Remove(symlinkPath) | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | // Create new symlink | |
| 144 | + | relativeLogFile := fmt.Sprintf("%s.log", containerID) | |
| 145 | + | if err := os.Symlink(relativeLogFile, symlinkPath); err != nil { | |
| 146 | + | log.Warn(). | |
| 147 | + | Err(err). | |
| 148 | + | Str("symlink", symlinkPath). | |
| 149 | + | Str("target", relativeLogFile). | |
| 150 | + | Msg("Failed to create container name symlink") | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | return writer, nil | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // Close gracefully closes all logging writers | |
| 158 | + | func Close() { | |
| 159 | + | // Lumberjack loggers will be closed automatically when the program exits | |
| 160 | + | // This function is provided for future extensibility | |
| 161 | + | }⏎̸ | |
| @@ -0,0 +1,161 @@ | |||||
| 1 | + | package logging | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | ||||
| 9 | + | "github.com/rs/zerolog" | |||
| 10 | + | "github.com/rs/zerolog/log" | |||
| 11 | + | "gopkg.in/natefinch/lumberjack.v2" | |||
| 12 | + | "gordon/internal/config" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | var ( | |||
| 16 | + | MainLogger zerolog.Logger | |||
| 17 | + | ProxyLogger zerolog.Logger | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // Setup initializes the logging system based on the configuration | |||
| 21 | + | func Setup(cfg *config.Config) error { | |||
| 22 | + | if !cfg.Logging.Enabled { | |||
| 23 | + | // Keep console logging only | |||
| 24 | + | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) | |||
| 25 | + | MainLogger = log.Logger | |||
| 26 | + | ProxyLogger = log.Logger | |||
| 27 | + | return nil | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // Create logs directory with secure permissions (0700 - owner only) | |||
| 31 | + | if err := os.MkdirAll(cfg.Logging.Dir, 0700); err != nil { | |||
| 32 | + | return fmt.Errorf("failed to create logs directory: %w", err) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Create container logs directory with secure permissions | |||
| 36 | + | containerLogDir := filepath.Join(cfg.Logging.Dir, cfg.Logging.ContainerLogDir) | |||
| 37 | + | if err := os.MkdirAll(containerLogDir, 0700); err != nil { | |||
| 38 | + | return fmt.Errorf("failed to create container logs directory: %w", err) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Set log level | |||
| 42 | + | level, err := zerolog.ParseLevel(cfg.Logging.Level) | |||
| 43 | + | if err != nil { | |||
| 44 | + | level = zerolog.InfoLevel | |||
| 45 | + | log.Warn().Str("invalid_level", cfg.Logging.Level).Msg("Invalid log level, using info") | |||
| 46 | + | } | |||
| 47 | + | zerolog.SetGlobalLevel(level) | |||
| 48 | + | ||||
| 49 | + | // Configure time format | |||
| 50 | + | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix | |||
| 51 | + | ||||
| 52 | + | // Setup main application logger | |||
| 53 | + | mainLogFile := filepath.Join(cfg.Logging.Dir, cfg.Logging.MainLogFile) | |||
| 54 | + | mainFileWriter := &lumberjack.Logger{ | |||
| 55 | + | Filename: mainLogFile, | |||
| 56 | + | MaxSize: cfg.Logging.MaxSize, | |||
| 57 | + | MaxBackups: cfg.Logging.MaxBackups, | |||
| 58 | + | MaxAge: cfg.Logging.MaxAge, | |||
| 59 | + | Compress: cfg.Logging.Compress, | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Set file permissions to be secure (readable only by owner) | |||
| 63 | + | if err := os.Chmod(mainLogFile, 0600); err != nil && !os.IsNotExist(err) { | |||
| 64 | + | log.Warn().Err(err).Str("file", mainLogFile).Msg("Failed to set secure permissions on log file") | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Setup proxy logger | |||
| 68 | + | proxyLogFile := filepath.Join(cfg.Logging.Dir, cfg.Logging.ProxyLogFile) | |||
| 69 | + | proxyFileWriter := &lumberjack.Logger{ | |||
| 70 | + | Filename: proxyLogFile, | |||
| 71 | + | MaxSize: cfg.Logging.MaxSize, | |||
| 72 | + | MaxBackups: cfg.Logging.MaxBackups, | |||
| 73 | + | MaxAge: cfg.Logging.MaxAge, | |||
| 74 | + | Compress: cfg.Logging.Compress, | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // Set file permissions to be secure (readable only by owner) | |||
| 78 | + | if err := os.Chmod(proxyLogFile, 0600); err != nil && !os.IsNotExist(err) { | |||
| 79 | + | log.Warn().Err(err).Str("file", proxyLogFile).Msg("Failed to set secure permissions on log file") | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Create multi-writers (console + file) | |||
| 83 | + | consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} | |||
| 84 | + | ||||
| 85 | + | mainMultiWriter := io.MultiWriter(consoleWriter, mainFileWriter) | |||
| 86 | + | proxyMultiWriter := io.MultiWriter(consoleWriter, proxyFileWriter) | |||
| 87 | + | ||||
| 88 | + | // Initialize loggers | |||
| 89 | + | MainLogger = zerolog.New(mainMultiWriter).With().Timestamp().Logger() | |||
| 90 | + | ProxyLogger = zerolog.New(proxyMultiWriter).With().Timestamp().Logger() | |||
| 91 | + | ||||
| 92 | + | // Set the global logger to the main logger | |||
| 93 | + | log.Logger = MainLogger | |||
| 94 | + | ||||
| 95 | + | log.Info(). | |||
| 96 | + | Str("main_log", mainLogFile). | |||
| 97 | + | Str("proxy_log", proxyLogFile). | |||
| 98 | + | Str("container_log_dir", containerLogDir). | |||
| 99 | + | Str("level", level.String()). | |||
| 100 | + | Msg("File logging initialized") | |||
| 101 | + | ||||
| 102 | + | return nil | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // GetContainerLogWriter returns a log writer for a specific container | |||
| 106 | + | func GetContainerLogWriter(cfg *config.Config, containerID string, containerName string) (*lumberjack.Logger, error) { | |||
| 107 | + | if !cfg.Logging.Enabled { | |||
| 108 | + | return nil, fmt.Errorf("logging is disabled") | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | containerLogDir := filepath.Join(cfg.Logging.Dir, cfg.Logging.ContainerLogDir) | |||
| 112 | + | ||||
| 113 | + | // Ensure container log directory exists with secure permissions | |||
| 114 | + | if err := os.MkdirAll(containerLogDir, 0700); err != nil { | |||
| 115 | + | return nil, fmt.Errorf("failed to create container log directory: %w", err) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // Use container ID as the main log file name | |||
| 119 | + | logFile := filepath.Join(containerLogDir, fmt.Sprintf("%s.log", containerID)) | |||
| 120 | + | ||||
| 121 | + | writer := &lumberjack.Logger{ | |||
| 122 | + | Filename: logFile, | |||
| 123 | + | MaxSize: cfg.Logging.MaxSize, | |||
| 124 | + | MaxBackups: cfg.Logging.MaxBackups, | |||
| 125 | + | MaxAge: cfg.Logging.MaxAge, | |||
| 126 | + | Compress: cfg.Logging.Compress, | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // Set file permissions to be secure (readable only by owner) | |||
| 130 | + | if err := os.Chmod(logFile, 0600); err != nil && !os.IsNotExist(err) { | |||
| 131 | + | log.Warn().Err(err).Str("file", logFile).Msg("Failed to set secure permissions on container log file") | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // Create a symlink for easier access using container name (if provided) | |||
| 135 | + | if containerName != "" && containerName != containerID { | |||
| 136 | + | symlinkPath := filepath.Join(containerLogDir, fmt.Sprintf("%s.log", containerName)) | |||
| 137 | + | ||||
| 138 | + | // Remove existing symlink if it exists | |||
| 139 | + | if _, err := os.Lstat(symlinkPath); err == nil { | |||
| 140 | + | os.Remove(symlinkPath) | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | // Create new symlink | |||
| 144 | + | relativeLogFile := fmt.Sprintf("%s.log", containerID) | |||
| 145 | + | if err := os.Symlink(relativeLogFile, symlinkPath); err != nil { | |||
| 146 | + | log.Warn(). | |||
| 147 | + | Err(err). | |||
| 148 | + | Str("symlink", symlinkPath). | |||
| 149 | + | Str("target", relativeLogFile). | |||
| 150 | + | Msg("Failed to create container name symlink") | |||
| 151 | + | } | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | return writer, nil | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | // Close gracefully closes all logging writers | |||
| 158 | + | func Close() { | |||
| 159 | + | // Lumberjack loggers will be closed automatically when the program exits | |||
| 160 | + | // This function is provided for future extensibility | |||
| 161 | + | }⏎̸ | |||
M internal/middleware/logging.go +5 -4
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "net/http" | |
| 5 | 5 | "time" | |
| 6 | 6 | ||
| 7 | + | "gordon/internal/logging" | |
| 7 | 8 | "github.com/rs/zerolog/log" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| @@ -57,8 +58,8 @@ func RequestLogger(next http.Handler) http.Handler { | |||
| 57 | 58 | // Calculate duration | |
| 58 | 59 | duration := time.Since(start) | |
| 59 | 60 | ||
| 60 | - | // Log the request | |
| 61 | - | log.Info(). | |
| 61 | + | // Log the request using proxy logger | |
| 62 | + | logging.ProxyLogger.Info(). | |
| 62 | 63 | Str("method", r.Method). | |
| 63 | 64 | Str("path", r.URL.Path). | |
| 64 | 65 | Str("query", r.URL.RawQuery). | |
| @@ -119,8 +120,8 @@ func ContainerRequestLogger(containerID string, domain string) func(http.Handler | |||
| 119 | 120 | // Calculate duration | |
| 120 | 121 | duration := time.Since(start) | |
| 121 | 122 | ||
| 122 | - | // Log the request with container information | |
| 123 | - | log.Info(). | |
| 123 | + | // Log the request with container information using proxy logger | |
| 124 | + | logging.ProxyLogger.Info(). | |
| 124 | 125 | Str("method", r.Method). | |
| 125 | 126 | Str("path", r.URL.Path). | |
| 126 | 127 | Str("query", r.URL.RawQuery). | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "net/http" | 4 | "net/http" | ||
| 5 | "time" | 5 | "time" | ||
| 6 | 6 | ||||
| 7 | + | "gordon/internal/logging" | |||
| 7 | "github.com/rs/zerolog/log" | 8 | "github.com/rs/zerolog/log" | ||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| @@ -57,8 +58,8 @@ func RequestLogger(next http.Handler) http.Handler { | |||||
| 57 | // Calculate duration | 58 | // Calculate duration | ||
| 58 | duration := time.Since(start) | 59 | duration := time.Since(start) | ||
| 59 | 60 | ||||
| 60 | - | // Log the request | 61 | + | // Log the request using proxy logger |
| 61 | - | log.Info(). | 62 | + | logging.ProxyLogger.Info(). |
| 62 | Str("method", r.Method). | 63 | Str("method", r.Method). | ||
| 63 | Str("path", r.URL.Path). | 64 | Str("path", r.URL.Path). | ||
| 64 | Str("query", r.URL.RawQuery). | 65 | Str("query", r.URL.RawQuery). | ||
| @@ -119,8 +120,8 @@ func ContainerRequestLogger(containerID string, domain string) func(http.Handler | |||||
| 119 | // Calculate duration | 120 | // Calculate duration | ||
| 120 | duration := time.Since(start) | 121 | duration := time.Since(start) | ||
| 121 | 122 | ||||
| 122 | - | // Log the request with container information | 123 | + | // Log the request with container information using proxy logger |
| 123 | - | log.Info(). | 124 | + | logging.ProxyLogger.Info(). |
| 124 | Str("method", r.Method). | 125 | Str("method", r.Method). | ||
| 125 | Str("path", r.URL.Path). | 126 | Str("path", r.URL.Path). | ||
| 126 | Str("query", r.URL.RawQuery). | 127 | Str("query", r.URL.RawQuery). | ||