Skip to content

Commit b2e7506

Browse files
committed
feat: add configuration and database layer
1 parent 768c659 commit b2e7506

File tree

9 files changed

+1226
-0
lines changed

9 files changed

+1226
-0
lines changed

internal/config/config.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package config
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/spf13/viper"
10+
)
11+
12+
// Config holds application configuration
13+
type Config struct {
14+
Server ServerConfig `mapstructure:"server"`
15+
Database DatabaseConfig `mapstructure:"database"`
16+
Ingestion IngestionConfig `mapstructure:"ingestion"`
17+
Auth AuthConfig `mapstructure:"auth"`
18+
Privacy PrivacyConfig `mapstructure:"privacy"`
19+
Monitoring MonitoringConfig `mapstructure:"monitoring"`
20+
Logging LoggingConfig `mapstructure:"logging"`
21+
Features FeaturesConfig `mapstructure:"features"`
22+
}
23+
24+
// ServerConfig holds server configuration
25+
type ServerConfig struct {
26+
Host string `mapstructure:"host"`
27+
Port int `mapstructure:"port"`
28+
GRPCPort int `mapstructure:"grpc_port"`
29+
ReadTimeout time.Duration `mapstructure:"read_timeout"`
30+
WriteTimeout time.Duration `mapstructure:"write_timeout"`
31+
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
32+
}
33+
34+
// DatabaseConfig holds database configuration
35+
type DatabaseConfig struct {
36+
Type string `mapstructure:"type"`
37+
Host string `mapstructure:"host"`
38+
Port int `mapstructure:"port"`
39+
Database string `mapstructure:"database"`
40+
Username string `mapstructure:"username"`
41+
Password string `mapstructure:"password"`
42+
MaxConnections int `mapstructure:"max_connections"`
43+
MaxIdleConnections int `mapstructure:"max_idle_connections"`
44+
MaxLifetime time.Duration `mapstructure:"connection_max_lifetime"`
45+
MigrationsPath string `mapstructure:"migrations_path"`
46+
Retention RetentionConfig `mapstructure:"retention"`
47+
}
48+
49+
// RetentionConfig holds data retention configuration
50+
type RetentionConfig struct {
51+
RawData string `mapstructure:"raw_data"`
52+
HourlyAggregates string `mapstructure:"hourly_aggregates"`
53+
DailyAggregates string `mapstructure:"daily_aggregates"`
54+
}
55+
56+
// IngestionConfig holds ingestion configuration
57+
type IngestionConfig struct {
58+
BufferSize int `mapstructure:"buffer_size"`
59+
BatchSize int `mapstructure:"batch_size"`
60+
BatchTimeout time.Duration `mapstructure:"batch_timeout"`
61+
MaxBatchWait time.Duration `mapstructure:"max_batch_wait"`
62+
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
63+
}
64+
65+
// RateLimitConfig holds rate limiting configuration
66+
type RateLimitConfig struct {
67+
Enabled bool `mapstructure:"enabled"`
68+
RequestsPerSecond int `mapstructure:"requests_per_second"`
69+
Burst int `mapstructure:"burst"`
70+
}
71+
72+
// AuthConfig holds authentication configuration
73+
type AuthConfig struct {
74+
JWTSecret string `mapstructure:"jwt_secret"`
75+
AccessTokenTTL time.Duration `mapstructure:"access_token_ttl"`
76+
RefreshTokenTTL time.Duration `mapstructure:"refresh_token_ttl"`
77+
}
78+
79+
// PrivacyConfig holds privacy configuration
80+
type PrivacyConfig struct {
81+
SanitizeParameters bool `mapstructure:"sanitize_parameters"`
82+
SensitiveKeys []string `mapstructure:"sensitive_keys"`
83+
HashSessionIDs bool `mapstructure:"hash_session_ids"`
84+
}
85+
86+
// MonitoringConfig holds monitoring configuration
87+
type MonitoringConfig struct {
88+
Prometheus PrometheusConfig `mapstructure:"prometheus"`
89+
HealthCheck HealthCheckConfig `mapstructure:"health_check"`
90+
}
91+
92+
// PrometheusConfig holds Prometheus configuration
93+
type PrometheusConfig struct {
94+
Enabled bool `mapstructure:"enabled"`
95+
Path string `mapstructure:"path"`
96+
}
97+
98+
// HealthCheckConfig holds health check configuration
99+
type HealthCheckConfig struct {
100+
Enabled bool `mapstructure:"enabled"`
101+
Path string `mapstructure:"path"`
102+
}
103+
104+
// LoggingConfig holds logging configuration
105+
type LoggingConfig struct {
106+
Level string `mapstructure:"level"`
107+
Format string `mapstructure:"format"`
108+
Output string `mapstructure:"output"`
109+
}
110+
111+
// FeaturesConfig holds feature flags
112+
type FeaturesConfig struct {
113+
WebSocket bool `mapstructure:"websocket"`
114+
AnomalyDetection bool `mapstructure:"anomaly_detection"`
115+
Alerting bool `mapstructure:"alerting"`
116+
MultiTenancy bool `mapstructure:"multi_tenancy"`
117+
}
118+
119+
// Load loads configuration from file
120+
func Load(configPath string) (*Config, error) {
121+
v := viper.New()
122+
123+
// Set defaults
124+
setDefaults(v)
125+
126+
// Set config file path
127+
if configPath != "" {
128+
v.SetConfigFile(configPath)
129+
} else {
130+
v.SetConfigName("config")
131+
v.SetConfigType("yaml")
132+
v.AddConfigPath(".")
133+
v.AddConfigPath("$HOME/.mcpulse")
134+
v.AddConfigPath("/etc/mcpulse")
135+
}
136+
137+
// Read environment variables
138+
v.AutomaticEnv()
139+
v.SetEnvPrefix("MCPULSE")
140+
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
141+
142+
// Read config file
143+
if err := v.ReadInConfig(); err != nil {
144+
var configFileNotFoundError viper.ConfigFileNotFoundError
145+
if !errors.As(err, &configFileNotFoundError) {
146+
return nil, fmt.Errorf("failed to read config: %w", err)
147+
}
148+
}
149+
150+
for _, key := range v.AllKeys() {
151+
_ = v.BindEnv(key)
152+
}
153+
154+
var cfg Config
155+
if err := v.Unmarshal(&cfg); err != nil {
156+
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
157+
}
158+
159+
return &cfg, nil
160+
}
161+
162+
// setDefaults sets default configuration values
163+
func setDefaults(v *viper.Viper) {
164+
// Server defaults
165+
v.SetDefault("server.host", "0.0.0.0")
166+
v.SetDefault("server.port", 8080)
167+
v.SetDefault("server.grpc_port", 9090)
168+
v.SetDefault("server.read_timeout", "30s")
169+
v.SetDefault("server.write_timeout", "30s")
170+
v.SetDefault("server.idle_timeout", "120s")
171+
172+
// Database defaults
173+
v.SetDefault("database.type", "timescaledb")
174+
v.SetDefault("database.host", "localhost")
175+
v.SetDefault("database.port", 5432)
176+
v.SetDefault("database.database", "mcp_analytics")
177+
v.SetDefault("database.username", "mcp_user")
178+
v.SetDefault("database.password", "")
179+
v.SetDefault("database.max_connections", 50)
180+
v.SetDefault("database.max_idle_connections", 10)
181+
v.SetDefault("database.connection_max_lifetime", "1h")
182+
v.SetDefault("database.migrations_path", "migrations")
183+
v.SetDefault("database.retention.raw_data", "90d")
184+
v.SetDefault("database.retention.hourly_aggregates", "1y")
185+
v.SetDefault("database.retention.daily_aggregates", "forever")
186+
187+
// Ingestion defaults
188+
v.SetDefault("ingestion.buffer_size", 10000)
189+
v.SetDefault("ingestion.batch_size", 1000)
190+
v.SetDefault("ingestion.batch_timeout", "5s")
191+
v.SetDefault("ingestion.max_batch_wait", "10s")
192+
v.SetDefault("ingestion.rate_limit.enabled", true)
193+
v.SetDefault("ingestion.rate_limit.requests_per_second", 1000)
194+
v.SetDefault("ingestion.rate_limit.burst", 2000)
195+
196+
// Auth defaults
197+
v.SetDefault("auth.jwt_secret", "change_this_secret")
198+
v.SetDefault("auth.access_token_ttl", "15m")
199+
v.SetDefault("auth.refresh_token_ttl", "168h") // 7 days
200+
201+
// Privacy defaults
202+
v.SetDefault("privacy.sanitize_parameters", true)
203+
v.SetDefault("privacy.sensitive_keys", []string{"password", "token", "api_key", "secret"})
204+
v.SetDefault("privacy.hash_session_ids", false)
205+
206+
// Monitoring defaults
207+
v.SetDefault("monitoring.prometheus.enabled", true)
208+
v.SetDefault("monitoring.prometheus.path", "/metrics")
209+
v.SetDefault("monitoring.health_check.enabled", true)
210+
v.SetDefault("monitoring.health_check.path", "/health")
211+
212+
// Logging defaults
213+
v.SetDefault("logging.level", "info")
214+
v.SetDefault("logging.output", "stdout")
215+
216+
// Features defaults
217+
v.SetDefault("features.websocket", true)
218+
v.SetDefault("features.anomaly_detection", false)
219+
v.SetDefault("features.alerting", false)
220+
v.SetDefault("features.multi_tenancy", false)
221+
}
222+
223+
// Validate validates the configuration
224+
func (c *Config) Validate() error {
225+
if c.Server.Port < 1 || c.Server.Port > 65535 {
226+
return fmt.Errorf("invalid server port: %d", c.Server.Port)
227+
}
228+
229+
if c.Server.GRPCPort < 1 || c.Server.GRPCPort > 65535 {
230+
return fmt.Errorf("invalid gRPC port: %d", c.Server.GRPCPort)
231+
}
232+
233+
if c.Server.GRPCPort == c.Server.Port {
234+
return fmt.Errorf("gRPC port (%d) must differ from HTTP port (%d)", c.Server.GRPCPort, c.Server.Port)
235+
}
236+
237+
if c.Database.Host == "" {
238+
return fmt.Errorf("database host is required")
239+
}
240+
241+
if c.Database.Database == "" {
242+
return fmt.Errorf("database name is required")
243+
}
244+
245+
if c.Database.Username == "" {
246+
return fmt.Errorf("database username is required")
247+
}
248+
249+
if strings.TrimSpace(c.Database.MigrationsPath) == "" {
250+
return fmt.Errorf("database migrations path is required")
251+
}
252+
253+
if c.Ingestion.BufferSize < 1 {
254+
return fmt.Errorf("ingestion buffer size must be at least 1")
255+
}
256+
257+
if c.Ingestion.BatchSize < 1 {
258+
return fmt.Errorf("ingestion batch size must be at least 1")
259+
}
260+
261+
if c.Ingestion.BatchSize > c.Ingestion.BufferSize {
262+
return fmt.Errorf("ingestion batch size cannot exceed buffer size")
263+
}
264+
265+
return nil
266+
}

0 commit comments

Comments
 (0)