Make the resolver pure DNS: JSON access log on stdout instead of a log store
The resolver kept its own query-log store (a bbolt ring, a live ring with SSE fanout, rolling per-domain counters and dashboard time-series) for the console. In a multi-node deployment each pod held a different slice of that, so every view disagreed with itself, and the cost sat on the query path. Charts belong in Grafana and log search in the log collector, so the store is gone. internal/accesslog writes one JSON line per notable query to stdout. The default is what was blocked and what failed (blocked, redirected, SERVFAIL, dropped, refused); log_cached and sample_rate add ordinary answers for a debugging session. Lines go through a bounded buffer to a single writer goroutine and are dropped, and counted in fancydns_access_log_dropped_total, when the stream cannot keep up, so a stalled log pipe can never stall a query. The config key is access_log (the old qlog block is ignored, like any unknown key). The metrics gain what the dashboard used to compute: fancydns_group_queries_total by ACL group and outcome and fancydns_responses_total by RCODE. Removed with the store: /api/dashboard, /api/logs, /api/logs/stream, /api/logs/clear, /api/cache/top, the series rings, and the cache's per-name hit table, which took a mutex and built a string on every cache hit. Tests that read the old log now read the access-log lines; the contract test drops the removed endpoints. The web console still calls them and is changed in the next commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd1117fd91
commit
c6c5ca4ff7
31 files changed
+818
-3389
No files matched your search
+38
-64
@@ -106,18 +106,18 @@ type Config struct {
|
||||
// echoed in /api/config and used to detect divergence in the cluster.
|
||||
Version int64 `yaml:"version,omitempty"`
|
||||
|
||||
Server Server `yaml:"server"`
|
||||
DNS DNS `yaml:"dns"`
|
||||
Upstream Upstream `yaml:"upstream"`
|
||||
Cache Cache `yaml:"cache"`
|
||||
Block Block `yaml:"blocklist"`
|
||||
ACL ACL `yaml:"acl"`
|
||||
QueryLog QueryLog `yaml:"qlog"`
|
||||
Cluster Cluster `yaml:"cluster"`
|
||||
API API `yaml:"api"`
|
||||
Metrics Metrics `yaml:"metrics"`
|
||||
Log Log `yaml:"log"`
|
||||
Memory Memory `yaml:"memory"`
|
||||
Server Server `yaml:"server"`
|
||||
DNS DNS `yaml:"dns"`
|
||||
Upstream Upstream `yaml:"upstream"`
|
||||
Cache Cache `yaml:"cache"`
|
||||
Block Block `yaml:"blocklist"`
|
||||
ACL ACL `yaml:"acl"`
|
||||
AccessLog AccessLog `yaml:"access_log"`
|
||||
Cluster Cluster `yaml:"cluster"`
|
||||
API API `yaml:"api"`
|
||||
Metrics Metrics `yaml:"metrics"`
|
||||
Log Log `yaml:"log"`
|
||||
Memory Memory `yaml:"memory"`
|
||||
}
|
||||
|
||||
// Server groups the process-level knobs.
|
||||
@@ -415,26 +415,24 @@ type Match struct {
|
||||
Any bool `yaml:"any" json:"any"`
|
||||
}
|
||||
|
||||
type QueryLog struct {
|
||||
// AccessLog configures the access log: one JSON line on stdout for each query
|
||||
// whose outcome is selected below. Nothing is stored by the resolver; the pod's
|
||||
// log collector indexes the lines, and the metrics count everything.
|
||||
type AccessLog struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Path is the bbolt file backing the query log ring.
|
||||
Path string `yaml:"path"`
|
||||
// MaxEntries bounds the ring; oldest are dropped.
|
||||
MaxEntries int `yaml:"max_entries"`
|
||||
// FlushInterval batches writes.
|
||||
FlushInterval Duration `yaml:"flush_interval"`
|
||||
// SampleRate is 0..1 for non-blocked, non-error queries.
|
||||
SampleRate float64 `yaml:"sample_rate"`
|
||||
// What to record.
|
||||
// LogBlocked logs blocklist hits and redirects.
|
||||
LogBlocked bool `yaml:"log_blocked"`
|
||||
LogCached bool `yaml:"log_cached"`
|
||||
LogErrors bool `yaml:"log_errors"`
|
||||
LogClients bool `yaml:"log_client_names"`
|
||||
// LiveBuffer is the ring of recent entries kept in memory for the UI/SSE.
|
||||
LiveBuffer int `yaml:"live_buffer"`
|
||||
// Retention of aggregated per-domain counters.
|
||||
TopDomains int `yaml:"top_domains"`
|
||||
TopTTL Duration `yaml:"top_ttl"`
|
||||
// LogErrors logs SERVFAILs, drops and ACL refusals.
|
||||
LogErrors bool `yaml:"log_errors"`
|
||||
// LogCached also logs cache hits.
|
||||
LogCached bool `yaml:"log_cached"`
|
||||
// SampleRate is the fraction of ordinary answers to log: 0 (the default)
|
||||
// none, 1 all. It is for temporary debugging; the metrics are the
|
||||
// permanent record of ordinary traffic.
|
||||
SampleRate float64 `yaml:"sample_rate"`
|
||||
// Buffer is how many lines may wait for the log stream before further ones
|
||||
// are dropped (and counted). Zero means 4096.
|
||||
Buffer int `yaml:"buffer"`
|
||||
}
|
||||
|
||||
type Cluster struct {
|
||||
@@ -606,17 +604,10 @@ func Default() *Config {
|
||||
ACL: ACL{
|
||||
DefaultPolicy: "allow",
|
||||
},
|
||||
QueryLog: QueryLog{
|
||||
Enabled: true,
|
||||
MaxEntries: 200000,
|
||||
FlushInterval: Duration(time.Second),
|
||||
SampleRate: 1,
|
||||
LogBlocked: true,
|
||||
LogCached: true,
|
||||
LogErrors: true,
|
||||
LiveBuffer: 2000,
|
||||
TopDomains: 5000,
|
||||
TopTTL: Duration(5 * time.Minute),
|
||||
AccessLog: AccessLog{
|
||||
Enabled: true,
|
||||
LogBlocked: true,
|
||||
LogErrors: true,
|
||||
},
|
||||
Cluster: Cluster{
|
||||
Enabled: false,
|
||||
@@ -905,20 +896,11 @@ func (c *Config) applyDefaults() {
|
||||
if c.ACL.DefaultPolicy == "" {
|
||||
c.ACL.DefaultPolicy = d.ACL.DefaultPolicy
|
||||
}
|
||||
if c.QueryLog.MaxEntries <= 0 {
|
||||
c.QueryLog.MaxEntries = d.QueryLog.MaxEntries
|
||||
if c.AccessLog.SampleRate < 0 {
|
||||
c.AccessLog.SampleRate = 0
|
||||
}
|
||||
if c.QueryLog.FlushInterval <= 0 {
|
||||
c.QueryLog.FlushInterval = d.QueryLog.FlushInterval
|
||||
}
|
||||
if c.QueryLog.LiveBuffer <= 0 {
|
||||
c.QueryLog.LiveBuffer = d.QueryLog.LiveBuffer
|
||||
}
|
||||
if c.QueryLog.SampleRate <= 0 {
|
||||
c.QueryLog.SampleRate = 1
|
||||
}
|
||||
if c.QueryLog.SampleRate > 1 {
|
||||
c.QueryLog.SampleRate = 1
|
||||
if c.AccessLog.SampleRate > 1 {
|
||||
c.AccessLog.SampleRate = 1
|
||||
}
|
||||
if c.Cluster.Valkey.Prefix == "" {
|
||||
c.Cluster.Valkey.Prefix = d.Cluster.Valkey.Prefix
|
||||
@@ -1208,8 +1190,8 @@ func (c *Config) Validate() error {
|
||||
add("acl.groups[%d] %q: match.any is set but is also the fallback; make it the last group", i, g.Name)
|
||||
}
|
||||
}
|
||||
if c.QueryLog.SampleRate < 0 || c.QueryLog.SampleRate > 1 {
|
||||
add("qlog.sample_rate must be between 0 and 1")
|
||||
if c.AccessLog.SampleRate < 0 || c.AccessLog.SampleRate > 1 {
|
||||
add("access_log.sample_rate must be between 0 and 1")
|
||||
}
|
||||
|
||||
if c.Cluster.Enabled {
|
||||
@@ -1341,14 +1323,6 @@ func (c *Config) CachePath() string {
|
||||
return c.AbsPath("data/cache.db")
|
||||
}
|
||||
|
||||
// QueryLogPath returns the resolved bbolt query-log path.
|
||||
func (c *Config) QueryLogPath() string {
|
||||
if c.QueryLog.Path != "" {
|
||||
return c.AbsPath(c.QueryLog.Path)
|
||||
}
|
||||
return c.AbsPath("data/queries.db")
|
||||
}
|
||||
|
||||
// ListPath returns the on-disk path for a list source, or "" when the source
|
||||
// is URL-only.
|
||||
func (c *Config) ListPath(l ListSource) string {
|
||||
|
||||
Reference in new issue
Block a user