This document details the logging implementation in Synapse Go, including the architecture, configuration, and usage patterns.
The logging system in Synapse Go uses a centralized logger factory that provides consistent logging throughout the application.
flowchart TD A[LoggerConfig.toml] --> B[Configuration Manager] B --> C[Logger Factory] C --> D1[Component Logger 1] C --> D2[Component Logger 2] C --> D3[Component Logger ...] C --> D4[Component Logger N] classDef config fill:#f9e8a0,stroke:#e6b800,stroke-width:1px classDef manager fill:#a0d8f9,stroke:#0073e6,stroke-width:1px classDef factory fill:#a0f9a5,stroke:#00b33c,stroke-width:1px classDef loggers fill:#e2a0f9,stroke:#9900cc,stroke-width:1px class A config class B manager class C factory class D1,D2,D3,D4 loggers
The core component of the logging system is the LoggerFactory, which serves as the Subject in the observer pattern:
type LoggerManager struct {
mu sync.RWMutex
logLevelMap map[string]slog.Level
slogHandlerOpts *slog.HandlerOptions
loggers map[string]*slog.Logger
observers []Observer
}
The LoggerManager maintains:
logLevelMap)slogHandlerOpts)loggers)The observer pattern allows components to register as observers and be notified when log levels change:
type Observer interface { UpdateLogLevel(level slog.Level) } func (lm *LoggerManager) RegisterObserver(observer Observer, componentName string) { lm.mu.Lock() defer lm.mu.Unlock() lm.observers = append(lm.observers, observer) // Set the initial log level if level, exists := lm.logLevelMap[componentName]; exists { observer.UpdateLogLevel(level) } else if defaultLevel, exists := lm.logLevelMap["default"]; exists { observer.UpdateLogLevel(defaultLevel) } }
When log levels are updated (typically through configuration changes), the LoggerManager notifies all observers:
func (lm *LoggerManager) notifyObservers() {
for _, observer := range lm.observers {
componentName := observer.GetComponentName()
if level, exists := lm.logLevelMap[componentName]; exists {
observer.UpdateLogLevel(level)
} else if defaultLevel, exists := lm.logLevelMap["default"]; exists {
observer.UpdateLogLevel(defaultLevel)
}
}
}
Components in the application integrate with the logging system by:
Observer interfaceLoggerManagertype SomeComponent struct { logger *slog.Logger level slog.Level } func NewSomeComponent() *SomeComponent { sc := &SomeComponent{ logger: loggerfactory.GetLogger("componentName"), } // Register as an observer loggerfactory.RegisterObserver(sc, "componentName") return sc } // Implement the Observer interface func (sc *SomeComponent) UpdateLogLevel(level slog.Level) { sc.level = level } // Using the logger func (sc *SomeComponent) DoSomething() { sc.logger.Info("Doing something") }
One of the key features of the logging system is the ability to change log levels at runtime without restarting the application. This is achieved through:
LoggerManager when configuration changesfunc (lm *LoggerManager) SetLogLevelMap(levelMap *map[string]slog.Level) { lm.mu.Lock() defer lm.mu.Unlock() lm.logLevelMap = *levelMap // Update existing loggers for name, logger := range lm.loggers { if level, exists := lm.logLevelMap[name]; exists { // Update logger level // ... } } // Notify observers lm.notifyObservers() }
The logging system integrates with the configuration system to load and apply log level configurations:
func InitializeConfig(ctx context.Context, confFolderPath string) error { // ... case strings.Contains(configFile.Name(), "Logger"): levelMap := make(map[string]slog.Level) slogHandlerConfig := &loggerfactory.SlogHandlerConfig{} if cfg.IsSet("levels") { var levelsMap map[string]string cfg.MustUnmarshal("levels", &levelsMap) for component, levelStr := range levelsMap { level := parseLogLevel(levelStr) levelMap[component] = level } } cm := loggerfactory.GetLoggerManager() cm.SetLogLevelMap(&levelMap) cm.SetSlogHandlerConfig(slogHandlerConfig) // Start watching for config changes cfg.Watch(context.Background(), configFilePath) // ... }
When the configuration file is updated, the log levels are dynamically reconfigured without requiring an application restart.
The logging system uses structured logging through the slog package, which allows for:
logger.Info("Processing request",
"method", request.Method,
"path", request.URL.Path,
"remote_addr", request.RemoteAddr)
The observer pattern in the logging system provides several advantages:
The Synapse Go logging system:
slog packageThis design ensures that logging is flexible, consistent, and configurable throughout the application lifecycle, allowing for effective debugging and monitoring in both development and production environments.