Skip to content

Commit e44cde3

Browse files
authored
Add config validation flag and cache Prometheus metrics (#439)
- Add -t/--test flag for configuration validation without starting server - Returns exit code 0 if valid, 1 if invalid - Useful for CI/CD pipelines and pre-deployment checks - Add Prometheus metrics for cache monitoring: - dns_cache_hits_total: Total cache hits - dns_cache_misses_total: Total cache misses - dns_cache_evictions_total: Total cache evictions - dns_cache_prefetches_total: Total prefetch operations - dns_cache_size{type}: Current cache size (positive/negative) - dns_cache_hit_rate: Cache hit rate percentage - Update README with new flag and cache metrics documentation Closes #411, Closes #424
1 parent 6ada438 commit e44cde3

File tree

5 files changed

+157
-2
lines changed

5 files changed

+157
-2
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ $ make test
9090
| Flag | Description |
9191
| ----------------- | ------------------------------------------------------------------------------ |
9292
| -c, --config PATH | Location of the config file. If it doesn't exist, a new one will be generated. Default: /sdns.conf |
93+
| -t, --test | Test configuration file and exit. Returns exit code 0 if valid, 1 if invalid |
9394
| -v, --version | Show the SDNS version |
9495
| -h, --help | Show help information and exit |
9596

@@ -224,6 +225,33 @@ When `killer_mode` is enabled:
224225
225226
For detailed information, see the [Kubernetes middleware documentation](middleware/kubernetes/README.md).
226227
228+
#### Cache Metrics
229+
230+
SDNS exports comprehensive cache metrics via the Prometheus `/metrics` endpoint for monitoring cache performance.
231+
232+
**Prometheus Metrics:**
233+
- `dns_cache_hits_total` - Total number of cache hits
234+
- `dns_cache_misses_total` - Total number of cache misses
235+
- `dns_cache_evictions_total` - Total number of cache evictions
236+
- `dns_cache_prefetches_total` - Total number of prefetch operations
237+
- `dns_cache_size{type="positive|negative"}` - Current number of entries in the cache
238+
- `dns_cache_hit_rate` - Cache hit rate percentage
239+
240+
**Example Prometheus Queries:**
241+
```promql
242+
# Cache hit rate
243+
dns_cache_hit_rate
244+
245+
# Cache hit ratio (alternative calculation)
246+
rate(dns_cache_hits_total[5m]) / (rate(dns_cache_hits_total[5m]) + rate(dns_cache_misses_total[5m]))
247+
248+
# Total cache size
249+
sum(dns_cache_size)
250+
251+
# Cache operations per second
252+
rate(dns_cache_hits_total[1m]) + rate(dns_cache_misses_total[1m])
253+
```
254+
227255
### External Plugins
228256
229257
SDNS supports custom plugins to extend its functionality. The execution order of plugins and middlewares affects their behavior. Configuration keys must be strings, while values can be any type. Plugins are loaded before the cache middleware in the order specified.

middleware/cache/cache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ func New(cfg *config.Config) *Cache {
101101
c.pcache = c.positive
102102
c.ncache = c.negative
103103

104+
// Register metrics instance for Prometheus hit rate calculation
105+
SetMetricsInstance(c.metrics)
106+
SetCacheSizeFuncs(c.positive.Len, c.negative.Len)
107+
104108
return c
105109
}
106110

middleware/cache/prometheus.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cache
2+
3+
import (
4+
"github.com/prometheus/client_golang/prometheus"
5+
)
6+
7+
var (
8+
cacheHits = prometheus.NewCounter(prometheus.CounterOpts{
9+
Name: "dns_cache_hits_total",
10+
Help: "Total number of DNS cache hits",
11+
})
12+
13+
cacheMisses = prometheus.NewCounter(prometheus.CounterOpts{
14+
Name: "dns_cache_misses_total",
15+
Help: "Total number of DNS cache misses",
16+
})
17+
18+
cacheEvictions = prometheus.NewCounter(prometheus.CounterOpts{
19+
Name: "dns_cache_evictions_total",
20+
Help: "Total number of DNS cache evictions",
21+
})
22+
23+
cachePrefetches = prometheus.NewCounter(prometheus.CounterOpts{
24+
Name: "dns_cache_prefetches_total",
25+
Help: "Total number of DNS cache prefetches",
26+
})
27+
28+
cacheSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
29+
Name: "dns_cache_size",
30+
Help: "Current number of entries in the DNS cache",
31+
}, []string{"type"})
32+
33+
// cacheHitRate is calculated as hits / (hits + misses) * 100
34+
cacheHitRate = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
35+
Name: "dns_cache_hit_rate",
36+
Help: "DNS cache hit rate percentage",
37+
}, calculateHitRate)
38+
)
39+
40+
// cacheInstance holds references to cache components for metrics
41+
var (
42+
metricsInstance *CacheMetrics
43+
positiveCacheLen func() int
44+
negativeCacheLen func() int
45+
)
46+
47+
func init() {
48+
prometheus.MustRegister(cacheHits)
49+
prometheus.MustRegister(cacheMisses)
50+
prometheus.MustRegister(cacheEvictions)
51+
prometheus.MustRegister(cachePrefetches)
52+
prometheus.MustRegister(cacheSize)
53+
prometheus.MustRegister(cacheHitRate)
54+
}
55+
56+
// SetMetricsInstance sets the metrics instance for hit rate calculation
57+
func SetMetricsInstance(m *CacheMetrics) {
58+
metricsInstance = m
59+
}
60+
61+
// SetCacheSizeFuncs sets the functions to get cache sizes
62+
func SetCacheSizeFuncs(positive, negative func() int) {
63+
positiveCacheLen = positive
64+
negativeCacheLen = negative
65+
}
66+
67+
// UpdateCacheSizeMetrics updates the cache size gauges
68+
func UpdateCacheSizeMetrics() {
69+
if positiveCacheLen != nil {
70+
cacheSize.WithLabelValues("positive").Set(float64(positiveCacheLen()))
71+
}
72+
if negativeCacheLen != nil {
73+
cacheSize.WithLabelValues("negative").Set(float64(negativeCacheLen()))
74+
}
75+
}
76+
77+
func calculateHitRate() float64 {
78+
// Update cache size metrics while calculating hit rate
79+
UpdateCacheSizeMetrics()
80+
81+
if metricsInstance == nil {
82+
return 0
83+
}
84+
hits, misses, _, _ := metricsInstance.Stats()
85+
total := float64(hits + misses)
86+
if total == 0 {
87+
return 0
88+
}
89+
return float64(hits) / total * 100
90+
}

middleware/cache/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,21 +261,25 @@ type CacheMetrics struct {
261261
// (*CacheMetrics).Hit hit records a cache hit.
262262
func (m *CacheMetrics) Hit() {
263263
m.hits.Add(1)
264+
cacheHits.Inc()
264265
}
265266

266267
// (*CacheMetrics).Miss miss records a cache miss.
267268
func (m *CacheMetrics) Miss() {
268269
m.misses.Add(1)
270+
cacheMisses.Inc()
269271
}
270272

271273
// (*CacheMetrics).Eviction eviction records a cache eviction.
272274
func (m *CacheMetrics) Eviction() {
273275
m.evictions.Add(1)
276+
cacheEvictions.Inc()
274277
}
275278

276279
// (*CacheMetrics).Prefetch prefetch records a prefetch operation.
277280
func (m *CacheMetrics) Prefetch() {
278281
m.prefetches.Add(1)
282+
cachePrefetches.Inc()
279283
}
280284

281285
// (*CacheMetrics).Stats stats returns current metrics.

sdns.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import (
2222
const version = "1.6.1"
2323

2424
var (
25-
cfgPath string
26-
cfg *config.Config
25+
cfgPath string
26+
testConfig bool
27+
cfg *config.Config
2728

2829
rootCmd = &cobra.Command{
2930
Use: "sdns",
@@ -42,6 +43,7 @@ focused on preserving privacy. For more information, visit https://sdns.dev`,
4243

4344
func init() {
4445
rootCmd.PersistentFlags().StringVarP(&cfgPath, "config", "c", "sdns.conf", "Location of the config file. If it doesn't exist, a new one will be generated.")
46+
rootCmd.PersistentFlags().BoolVarP(&testConfig, "test", "t", false, "Test configuration file and exit. Returns exit code 0 if valid, 1 if invalid.")
4547
rootCmd.AddCommand(versionCmd)
4648
}
4749

@@ -86,6 +88,11 @@ func setup() error {
8688
}
8789

8890
func runServer(cmd *cobra.Command, args []string) error {
91+
// Handle config test mode
92+
if testConfig {
93+
return validateConfiguration()
94+
}
95+
8996
zlog.Info("Starting sdns...", "version", version)
9097

9198
if err := setup(); err != nil {
@@ -149,6 +156,28 @@ func runServer(cmd *cobra.Command, args []string) error {
149156
return nil
150157
}
151158

159+
func validateConfiguration() error {
160+
var err error
161+
162+
if cfg, err = config.Load(cfgPath, version); err != nil {
163+
fmt.Fprintf(os.Stderr, "Configuration test failed: %v\n", err)
164+
return err
165+
}
166+
167+
// Validate log level
168+
switch cfg.LogLevel {
169+
case "", "debug", "info", "warn", "error":
170+
// Valid log levels
171+
default:
172+
err := fmt.Errorf("log verbosity level unknown: %s", cfg.LogLevel)
173+
fmt.Fprintf(os.Stderr, "Configuration test failed: %v\n", err)
174+
return err
175+
}
176+
177+
fmt.Printf("Configuration file %s test successful\n", cfgPath)
178+
return nil
179+
}
180+
152181
func printVersion(cmd *cobra.Command, args []string) {
153182
buildInfo, _ := debug.ReadBuildInfo()
154183

0 commit comments

Comments
 (0)