This document explains the configuration management system in Synapse Go, including how configuration is loaded, stored, and accessed throughout the application.
Synapse Go uses a centralized configuration system built around the ConfigContext struct, which serves as a container for all configuration data. The configuration management is implemented in the internal/pkg/config package and used throughout the application.
flowchart TB A[Configuration Files] -->|"(deployment.toml, logger.toml)"| B B[Configuration Initialization] -->|"config.InitializeConfig"| C C[Configuration Context] -->|"artifacts.ConfigContext"| D D[Application Components] classDef files fill:#f9e8a0,stroke:#e6b800,stroke-width:1px classDef init fill:#a0d8f9,stroke:#0073e6,stroke-width:1px classDef context fill:#a0f9a5,stroke:#00b33c,stroke-width:1px classDef components fill:#e2a0f9,stroke:#9900cc,stroke-width:1px class A files class B init class C context class D components
The central component of the configuration system is the ConfigContext struct, defined in internal/pkg/core/artifacts/artifact.go:
type ConfigContext struct {
ApiMap map[string]API
EndpointMap map[string]Endpoint
SequenceMap map[string]Sequence
InboundMap map[string]Inbound
DeploymentConfig map[string]interface{}
}
This struct:
The ConfigContext is implemented as a singleton to ensure that there's a single consistent source of configuration throughout the application:
var instance *ConfigContext var once sync.Once // singleton instance of the ConfigContext func GetConfigContext() *ConfigContext { once.Do(func() { instance = &ConfigContext{ ApiMap: make(map[string]API), EndpointMap: make(map[string]Endpoint), SequenceMap: make(map[string]Sequence), InboundMap: make(map[string]Inbound), DeploymentConfig: make(map[string]interface{}), } }) return instance }
This ensures that regardless of where or how many times the function is called, the same instance is returned.
Configuration is initialized in internal/pkg/config/config.go:
func InitializeConfig(ctx context.Context, confFolderPath string) error { // Get the configuration files from the confFolderPath configFiles, err := os.ReadDir(confFolderPath) // ... // Process each configuration file for _, configFile := range configFiles { // ... configFilePath := filepath.Join(confFolderPath, configFile.Name()) cfg := koanf.New(".") // Load configuration using Koanf library if err := cfg.Load(file.Provider(configFilePath), toml.Parser()); err != nil { return err } // Process configuration based on file name switch { case strings.Contains(configFile.Name(), "Logger"): // Process logger configuration // ... case strings.Contains(configFile.Name(), "deployment"): // Process deployment configuration deploymentConfigMap := make(map[string]interface{}) if cfg.IsSet("server") { var serverConfigMap map[string]string cfg.MustUnmarshal("server", &serverConfigMap) // Validate configuration // ... deploymentConfigMap["server"] = serverConfigMap } configContext.AddDeploymentConfig(deploymentConfigMap) } } return nil }
The initialization process:
ConfigContext with the loaded configurationConfiguration is accessed by retrieving the ConfigContext from the Go context:
// Example of accessing configuration from context configContext := ctx.Value(utils.ConfigContextKey).(*artifacts.ConfigContext)
The ConfigContext provides methods for accessing specific types of configuration:
// Get an endpoint configuration endpoint := configContext.GetEndpoint("endpointName") // Access server configuration serverConfig, ok := configContext.DeploymentConfig["server"].(map[string]string) if ok { hostname := serverConfig["hostname"] // ... }
The configuration system includes validation to ensure that required configuration values are present and valid:
// Validate required hostname key hostname, exists := serverConfigMap["hostname"] if !exists { return fmt.Errorf("missing required server configuration key: hostname") } // Validate hostname value if hostname == "" { return fmt.Errorf("server hostname cannot be empty") } // Validate offset if it exists (optional) if offsetStr, hasOffset := serverConfigMap["offset"]; hasOffset && offsetStr != "" { offset, err := strconv.Atoi(offsetStr) if err != nil { return fmt.Errorf("invalid server offset value: %s, must be an integer", offsetStr) } if offset < 0 { return fmt.Errorf("server offset must be non-negative, got: %d", offset) } }
This validation ensures that the application has all the configuration values it needs and that they are in the expected format.
The configuration files are structured as TOML files, which provide a clear and readable format for configuration.
[server] hostname = "localhost" offset = "0"
This file contains server-level configuration, such as the hostname and port offset.
[levels] default = "INFO" deployers = "INFO" router = "INFO" mediation = "INFO"
This file configures the logging levels for different components of the application.
The configuration system is designed to support dynamic configuration changes, particularly for logging levels. This is implemented through a file watcher mechanism:
// Start watching for config changes cfg.Watch(context.Background(), configFilePath)
When a configuration file changes, the application automatically reloads the configuration without requiring a restart.
The Synapse Go configuration system:
This design ensures that configuration is consistent, accessible, and manageable throughout the application lifecycle.