summaryrefslogtreecommitdiffstats
path: root/internal/config/config.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-14 09:41:18 -0800
committerbndw <ben@bdw.to>2026-02-14 09:41:18 -0800
commit688548d4ac3293449a88913275f886fd2e103cdf (patch)
tree5bf83c9a9b50863b6201ebf5066ee6855fefe725 /internal/config/config.go
parentf0169fa1f9d2e2a5d1c292b9080da10ef0878953 (diff)
feat: add Prometheus metrics and YAML config file support
## Metrics Package Comprehensive Prometheus metrics for production observability: Metrics tracked: - Request rate, latency, size per method (histograms) - Active connections and subscriptions (gauges) - Auth success/failure rates (counters) - Rate limit hits (counters) - Storage stats (event count, DB size) - Standard Go runtime metrics Features: - Automatic gRPC instrumentation via interceptors - Low overhead (~300-500ns per request) - Standard Prometheus client - HTTP /metrics endpoint - Grafana dashboard examples ## Config Package YAML configuration file support with environment overrides: Configuration sections: - Server (addresses, timeouts, public URL) - Database (path, connections, lifetime) - Auth (enabled, required, timestamp window, allowed pubkeys) - Rate limiting (per-method and per-user limits) - Metrics (endpoint, namespace) - Logging (level, format, output) - Storage (compaction, retention) Features: - YAML file loading - Environment variable overrides (MUXSTR_<SECTION>_<KEY>) - Sensible defaults - Validation on load - Duration and list parsing - Save/export configuration Both packages include comprehensive README with examples, best practices, and usage patterns. Config tests verify YAML parsing, env overrides, validation, and round-trip serialization.
Diffstat (limited to 'internal/config/config.go')
-rw-r--r--internal/config/config.go324
1 files changed, 324 insertions, 0 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..87ca4eb
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,324 @@
1package config
2
3import (
4 "fmt"
5 "os"
6 "strings"
7 "time"
8
9 "gopkg.in/yaml.v3"
10)
11
12// Config holds all configuration for the relay.
13type Config struct {
14 Server ServerConfig `yaml:"server"`
15 Database DatabaseConfig `yaml:"database"`
16 Auth AuthConfig `yaml:"auth"`
17 RateLimit RateLimitConfig `yaml:"rate_limit"`
18 Metrics MetricsConfig `yaml:"metrics"`
19 Logging LoggingConfig `yaml:"logging"`
20 Storage StorageConfig `yaml:"storage"`
21}
22
23// ServerConfig holds server configuration.
24type ServerConfig struct {
25 GrpcAddr string `yaml:"grpc_addr"`
26 HttpAddr string `yaml:"http_addr"`
27 PublicURL string `yaml:"public_url"`
28 ReadTimeout time.Duration `yaml:"read_timeout"`
29 WriteTimeout time.Duration `yaml:"write_timeout"`
30}
31
32// DatabaseConfig holds database configuration.
33type DatabaseConfig struct {
34 Path string `yaml:"path"`
35 MaxConnections int `yaml:"max_connections"`
36 MaxLifetime time.Duration `yaml:"max_lifetime"`
37}
38
39// AuthConfig holds authentication configuration.
40type AuthConfig struct {
41 Enabled bool `yaml:"enabled"`
42 Required bool `yaml:"required"`
43 TimestampWindow int64 `yaml:"timestamp_window"`
44 AllowedPubkeys []string `yaml:"allowed_pubkeys"`
45 SkipMethods []string `yaml:"skip_methods"`
46}
47
48// RateLimitConfig holds rate limiting configuration.
49type RateLimitConfig struct {
50 Enabled bool `yaml:"enabled"`
51 DefaultRPS float64 `yaml:"default_rps"`
52 DefaultBurst int `yaml:"default_burst"`
53 IPRPS float64 `yaml:"ip_rps"`
54 IPBurst int `yaml:"ip_burst"`
55 Methods map[string]MethodLimit `yaml:"methods"`
56 Users map[string]UserLimit `yaml:"users"`
57 SkipMethods []string `yaml:"skip_methods"`
58 SkipUsers []string `yaml:"skip_users"`
59 CleanupInterval time.Duration `yaml:"cleanup_interval"`
60 MaxIdleTime time.Duration `yaml:"max_idle_time"`
61}
62
63// MethodLimit defines rate limits for a specific method.
64type MethodLimit struct {
65 RPS float64 `yaml:"rps"`
66 Burst int `yaml:"burst"`
67}
68
69// UserLimit defines rate limits for a specific user.
70type UserLimit struct {
71 RPS float64 `yaml:"rps"`
72 Burst int `yaml:"burst"`
73 Methods map[string]MethodLimit `yaml:"methods"`
74}
75
76// MetricsConfig holds metrics configuration.
77type MetricsConfig struct {
78 Enabled bool `yaml:"enabled"`
79 Addr string `yaml:"addr"`
80 Path string `yaml:"path"`
81 Namespace string `yaml:"namespace"`
82 Subsystem string `yaml:"subsystem"`
83}
84
85// LoggingConfig holds logging configuration.
86type LoggingConfig struct {
87 Level string `yaml:"level"`
88 Format string `yaml:"format"`
89 Output string `yaml:"output"`
90}
91
92// StorageConfig holds storage configuration.
93type StorageConfig struct {
94 AutoCompact bool `yaml:"auto_compact"`
95 CompactInterval time.Duration `yaml:"compact_interval"`
96 MaxEventAge time.Duration `yaml:"max_event_age"`
97}
98
99// Default returns the default configuration.
100func Default() *Config {
101 return &Config{
102 Server: ServerConfig{
103 GrpcAddr: ":50051",
104 HttpAddr: ":8080",
105 ReadTimeout: 30 * time.Second,
106 WriteTimeout: 30 * time.Second,
107 },
108 Database: DatabaseConfig{
109 Path: "relay.db",
110 MaxConnections: 10,
111 MaxLifetime: 1 * time.Hour,
112 },
113 Auth: AuthConfig{
114 Enabled: false,
115 Required: false,
116 TimestampWindow: 60,
117 },
118 RateLimit: RateLimitConfig{
119 Enabled: false,
120 DefaultRPS: 10,
121 DefaultBurst: 20,
122 IPRPS: 5,
123 IPBurst: 10,
124 CleanupInterval: 5 * time.Minute,
125 MaxIdleTime: 10 * time.Minute,
126 },
127 Metrics: MetricsConfig{
128 Enabled: true,
129 Addr: ":9090",
130 Path: "/metrics",
131 Namespace: "muxstr",
132 Subsystem: "relay",
133 },
134 Logging: LoggingConfig{
135 Level: "info",
136 Format: "json",
137 Output: "stdout",
138 },
139 Storage: StorageConfig{
140 AutoCompact: true,
141 CompactInterval: 24 * time.Hour,
142 MaxEventAge: 0, // unlimited
143 },
144 }
145}
146
147// Load loads configuration from a YAML file and applies environment variable overrides.
148func Load(filename string) (*Config, error) {
149 // Start with defaults
150 cfg := Default()
151
152 // Read file if provided
153 if filename != "" {
154 data, err := os.ReadFile(filename)
155 if err != nil {
156 return nil, fmt.Errorf("failed to read config file: %w", err)
157 }
158
159 if err := yaml.Unmarshal(data, cfg); err != nil {
160 return nil, fmt.Errorf("failed to parse config file: %w", err)
161 }
162 }
163
164 // Apply environment variable overrides
165 applyEnvOverrides(cfg)
166
167 // Validate
168 if err := cfg.Validate(); err != nil {
169 return nil, fmt.Errorf("invalid configuration: %w", err)
170 }
171
172 return cfg, nil
173}
174
175// Validate validates the configuration.
176func (c *Config) Validate() error {
177 // Validate server addresses
178 if c.Server.GrpcAddr == "" {
179 return fmt.Errorf("server.grpc_addr is required")
180 }
181 if c.Server.HttpAddr == "" {
182 return fmt.Errorf("server.http_addr is required")
183 }
184
185 // Validate database path
186 if c.Database.Path == "" {
187 return fmt.Errorf("database.path is required")
188 }
189
190 // Validate metrics config if enabled
191 if c.Metrics.Enabled {
192 if c.Metrics.Addr == "" {
193 return fmt.Errorf("metrics.addr is required when metrics enabled")
194 }
195 if c.Metrics.Namespace == "" {
196 return fmt.Errorf("metrics.namespace is required when metrics enabled")
197 }
198 }
199
200 // Validate logging
201 validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
202 if !validLevels[c.Logging.Level] {
203 return fmt.Errorf("invalid logging.level: %s (must be debug, info, warn, or error)", c.Logging.Level)
204 }
205
206 validFormats := map[string]bool{"json": true, "text": true}
207 if !validFormats[c.Logging.Format] {
208 return fmt.Errorf("invalid logging.format: %s (must be json or text)", c.Logging.Format)
209 }
210
211 return nil
212}
213
214// applyEnvOverrides applies environment variable overrides to the configuration.
215// Environment variables follow the pattern: MUXSTR_<SECTION>_<KEY>
216func applyEnvOverrides(cfg *Config) {
217 // Server
218 if val := os.Getenv("MUXSTR_SERVER_GRPC_ADDR"); val != "" {
219 cfg.Server.GrpcAddr = val
220 }
221 if val := os.Getenv("MUXSTR_SERVER_HTTP_ADDR"); val != "" {
222 cfg.Server.HttpAddr = val
223 }
224 if val := os.Getenv("MUXSTR_SERVER_PUBLIC_URL"); val != "" {
225 cfg.Server.PublicURL = val
226 }
227 if val := os.Getenv("MUXSTR_SERVER_READ_TIMEOUT"); val != "" {
228 if d, err := time.ParseDuration(val); err == nil {
229 cfg.Server.ReadTimeout = d
230 }
231 }
232 if val := os.Getenv("MUXSTR_SERVER_WRITE_TIMEOUT"); val != "" {
233 if d, err := time.ParseDuration(val); err == nil {
234 cfg.Server.WriteTimeout = d
235 }
236 }
237
238 // Database
239 if val := os.Getenv("MUXSTR_DATABASE_PATH"); val != "" {
240 cfg.Database.Path = val
241 }
242 if val := os.Getenv("MUXSTR_DATABASE_MAX_CONNECTIONS"); val != "" {
243 var n int
244 if _, err := fmt.Sscanf(val, "%d", &n); err == nil {
245 cfg.Database.MaxConnections = n
246 }
247 }
248
249 // Auth
250 if val := os.Getenv("MUXSTR_AUTH_ENABLED"); val != "" {
251 cfg.Auth.Enabled = parseBool(val)
252 }
253 if val := os.Getenv("MUXSTR_AUTH_REQUIRED"); val != "" {
254 cfg.Auth.Required = parseBool(val)
255 }
256 if val := os.Getenv("MUXSTR_AUTH_TIMESTAMP_WINDOW"); val != "" {
257 var n int64
258 if _, err := fmt.Sscanf(val, "%d", &n); err == nil {
259 cfg.Auth.TimestampWindow = n
260 }
261 }
262 if val := os.Getenv("MUXSTR_AUTH_ALLOWED_PUBKEYS"); val != "" {
263 cfg.Auth.AllowedPubkeys = strings.Split(val, ",")
264 }
265
266 // Rate limit
267 if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" {
268 cfg.RateLimit.Enabled = parseBool(val)
269 }
270 if val := os.Getenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS"); val != "" {
271 var n float64
272 if _, err := fmt.Sscanf(val, "%f", &n); err == nil {
273 cfg.RateLimit.DefaultRPS = n
274 }
275 }
276 if val := os.Getenv("MUXSTR_RATE_LIMIT_DEFAULT_BURST"); val != "" {
277 var n int
278 if _, err := fmt.Sscanf(val, "%d", &n); err == nil {
279 cfg.RateLimit.DefaultBurst = n
280 }
281 }
282
283 // Metrics
284 if val := os.Getenv("MUXSTR_METRICS_ENABLED"); val != "" {
285 cfg.Metrics.Enabled = parseBool(val)
286 }
287 if val := os.Getenv("MUXSTR_METRICS_ADDR"); val != "" {
288 cfg.Metrics.Addr = val
289 }
290 if val := os.Getenv("MUXSTR_METRICS_PATH"); val != "" {
291 cfg.Metrics.Path = val
292 }
293
294 // Logging
295 if val := os.Getenv("MUXSTR_LOGGING_LEVEL"); val != "" {
296 cfg.Logging.Level = val
297 }
298 if val := os.Getenv("MUXSTR_LOGGING_FORMAT"); val != "" {
299 cfg.Logging.Format = val
300 }
301 if val := os.Getenv("MUXSTR_LOGGING_OUTPUT"); val != "" {
302 cfg.Logging.Output = val
303 }
304}
305
306// parseBool parses a boolean from a string.
307func parseBool(s string) bool {
308 s = strings.ToLower(s)
309 return s == "true" || s == "1" || s == "yes" || s == "on"
310}
311
312// Save saves the configuration to a YAML file.
313func (c *Config) Save(filename string) error {
314 data, err := yaml.Marshal(c)
315 if err != nil {
316 return fmt.Errorf("failed to marshal config: %w", err)
317 }
318
319 if err := os.WriteFile(filename, data, 0644); err != nil {
320 return fmt.Errorf("failed to write config file: %w", err)
321 }
322
323 return nil
324}