1package config
2
3import (
4 "fmt"
5 "os"
6 "slices"
7 "time"
8
9 "github.com/BurntSushi/toml"
10)
11
12type ServerConfig struct {
13 Address string `toml:"address"`
14 Port int `toml:"port"`
15 RootHintsFile string `toml:"root_hints_file"`
16 UDPSize int `toml:"udp_payload_size"`
17}
18
19type LoggingConfig struct {
20 Output string `toml:"output"`
21 FilePath string `toml:"file_path"`
22 Level string `toml:"level"`
23}
24
25type RatelimitConfig struct {
26 Rate int `toml:"rate"`
27 Burst int `toml:"burst"`
28 Window duration `toml:"window_seconds"`
29 ExpirationTime duration `toml:"cleanup_seconds"`
30}
31
32type MetricsConfig struct {
33 DSN string `toml:"dsn"`
34 BatchSize int `toml:"batch_size"`
35 FlushInterval duration `toml:"flush_interval"`
36 RetentionPeriod duration `toml:"retention_period"`
37}
38
39type AdvancedConfig struct {
40 QueryTimeout duration `toml:"query_timeout"`
41 ReadTimeout duration `toml:"read_timeout"`
42 WriteTimeout duration `toml:"write_timeout"`
43}
44
45type Config struct {
46 Server ServerConfig `toml:"server"`
47 Logging LoggingConfig `toml:"logging"`
48 Ratelimit RatelimitConfig `toml:"ratelimit"`
49 Metrics MetricsConfig `toml:"metrics"`
50 Advanced AdvancedConfig `toml:"advanced"`
51}
52
53type duration struct {
54 time.Duration
55}
56
57func (d *duration) UnmarshalText(text []byte) error {
58 var err error
59 d.Duration, err = time.ParseDuration(string(text))
60 if err != nil {
61 var seconds int64
62 seconds, err = parseInt64(string(text))
63 if err == nil {
64 d.Duration = time.Duration(seconds) * time.Second
65 return nil
66 }
67 return fmt.Errorf("invalid duration format: %w", err)
68 }
69 return nil
70}
71
72func parseInt64(s string) (int64, error) {
73 var i int64
74 _, err := fmt.Sscan(s, &i)
75 return i, err
76}
77
78func LoadConfig(path string) (Config, error) {
79 cfg := Config{}
80 cfg.Server.Address = "127.0.0.1"
81 cfg.Server.Port = 53
82 cfg.Server.RootHintsFile = "/etc/alky/root.hints"
83 cfg.Server.UDPSize = 512
84
85 cfg.Logging.Output = "stdout"
86 cfg.Logging.Level = "info"
87
88 cfg.Ratelimit.Rate = 100
89 cfg.Ratelimit.Burst = 200
90 cfg.Ratelimit.Window.Duration = 1 * time.Second
91 cfg.Ratelimit.ExpirationTime.Duration = 1 * time.Minute
92
93 cfg.Metrics.DSN = "clickhouse://localhost:9000/default"
94 cfg.Metrics.BatchSize = 1000
95 cfg.Metrics.FlushInterval.Duration = 10 * time.Second
96 cfg.Metrics.RetentionPeriod.Duration = 30 * 24 * time.Hour
97
98 cfg.Advanced.QueryTimeout.Duration = 5 * time.Second
99 cfg.Advanced.ReadTimeout.Duration = 2 * time.Second
100 cfg.Advanced.WriteTimeout.Duration = 2 * time.Second
101
102 md, err := toml.DecodeFile(path, &cfg)
103 if err != nil {
104 if os.IsNotExist(err) {
105 fmt.Printf("Warning: Config file '%s' not found, using default settings.\n", path)
106 } else {
107 return cfg, fmt.Errorf("error decoding config file '%s': %w", path, err)
108 }
109 }
110
111 if len(md.Undecoded()) > 0 {
112 return cfg, fmt.Errorf("unknown configuration keys found: %v", md.Undecoded())
113 }
114
115 if cfg.Logging.Output == "file" && cfg.Logging.FilePath == "" {
116 return cfg, fmt.Errorf("logging output is 'file' but 'file_path' is not set")
117 }
118
119 validLevels := []string{"debug", "info", "warn", "error"}
120 if !slices.Contains(validLevels, cfg.Logging.Level) {
121 return cfg, fmt.Errorf("invalid logging level '%s', must be one of: %v", cfg.Logging.Level, validLevels)
122 }
123
124 if cfg.Advanced.QueryTimeout.Duration <= 0 {
125 cfg.Advanced.QueryTimeout.Duration = 5 * time.Second
126 }
127
128 if cfg.Advanced.ReadTimeout.Duration <= 0 {
129 cfg.Advanced.ReadTimeout.Duration = 2 * time.Second
130 }
131
132 if cfg.Advanced.WriteTimeout.Duration <= 0 {
133 cfg.Advanced.WriteTimeout.Duration = 2 * time.Second
134 }
135
136 if cfg.Server.UDPSize <= 0 || cfg.Server.UDPSize > 4096 {
137 cfg.Server.UDPSize = 512
138 }
139
140 if cfg.Ratelimit.Rate > 0 {
141 if cfg.Ratelimit.Window.Duration <= 0 {
142 cfg.Ratelimit.Window.Duration = 1 * time.Second
143 }
144 if cfg.Ratelimit.ExpirationTime.Duration <= 0 {
145 cfg.Ratelimit.ExpirationTime.Duration = 1 * time.Minute
146 }
147 }
148
149 return cfg, nil
150}