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
@@ -53,8 +53,6 @@ type Hooks struct {
|
||||
// StartedAt is the process start time.
|
||||
StartedAt time.Time
|
||||
|
||||
// Dashboard returns the aggregated dashboard payload.
|
||||
Dashboard func(ctx context.Context, rangeKey string) (any, error)
|
||||
// BlocklistState returns the compiled blocklist detail.
|
||||
BlocklistState func() any
|
||||
// BlocklistStats returns the compiled rule counts.
|
||||
@@ -107,18 +105,8 @@ type Hooks struct {
|
||||
GetUpstream func() any
|
||||
// PutUpstream validates and persists a new upstream config.
|
||||
PutUpstream func(ctx context.Context, up config.Upstream) (int64, error)
|
||||
// SearchLog pages the query log.
|
||||
SearchLog func(ctx context.Context, q LogQuery) (any, error)
|
||||
// LogStats is the live stats event pushed over SSE.
|
||||
LogStats func() any
|
||||
// SubscribeLog streams new log entries; the cancel func must be called.
|
||||
SubscribeLog func() (<-chan LogEntry, func())
|
||||
// ClearLog empties the query log.
|
||||
ClearLog func() error
|
||||
// CacheStats returns the cache snapshot.
|
||||
CacheStats func() any
|
||||
// CacheTop returns the hottest cached names.
|
||||
CacheTop func(limit int) any
|
||||
// FlushCache empties the cache.
|
||||
FlushCache func() int64
|
||||
// Health returns the health payload for /healthz.
|
||||
@@ -153,44 +141,6 @@ type ZoneFile struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// LogQuery pages the query log.
|
||||
type LogQuery struct {
|
||||
Limit int
|
||||
Offset int
|
||||
Text string
|
||||
Action string
|
||||
Client string
|
||||
RCode int
|
||||
BlockedOnly bool
|
||||
Since time.Time
|
||||
}
|
||||
|
||||
// LogEntry is one query-log row as the admin API serves it.
|
||||
//
|
||||
// These fields are not omitempty, and that is deliberate: omitempty makes the
|
||||
// key set depend on the row, so a query with no ACL group arrived without
|
||||
// `client_group` and the console rendered "undefined" where the column should
|
||||
// be empty. The contract test in internal/app pins the key set, and it can
|
||||
// only pin one shape if the server emits one.
|
||||
type LogEntry struct {
|
||||
ID uint64 `json:"id"`
|
||||
TS time.Time `json:"ts"`
|
||||
Client string `json:"client"`
|
||||
Group string `json:"client_group"`
|
||||
Name string `json:"name"`
|
||||
QType uint16 `json:"qtype"`
|
||||
QTypeStr string `json:"qtype_name"`
|
||||
RCode int `json:"rcode"`
|
||||
RCodeStr string `json:"rcode_name"`
|
||||
Action string `json:"action"`
|
||||
Response string `json:"response"`
|
||||
Duration int64 `json:"duration_ms"`
|
||||
Upstream string `json:"upstream"`
|
||||
List string `json:"list"`
|
||||
Protocol string `json:"protocol"`
|
||||
Blocked bool `json:"blocked"`
|
||||
}
|
||||
|
||||
// Server is the admin HTTP server.
|
||||
type Server struct {
|
||||
hooks Hooks
|
||||
@@ -284,7 +234,6 @@ func (s *Server) routes(cfg *config.Config) *http.ServeMux {
|
||||
mux.Handle("POST "+base+"auth/logout", s.authed(s.handleLogout, false))
|
||||
|
||||
// Everything below requires a principal.
|
||||
mux.Handle("GET "+base+"dashboard", s.authed(s.handleDashboard, false))
|
||||
mux.Handle("GET "+base+"status", s.authed(s.handleStatus, false))
|
||||
mux.Handle("GET "+base+"blocklists", s.authed(s.handleBlocklists, false))
|
||||
mux.Handle("POST "+base+"blocklists/refresh", s.authed(s.handleBlocklistRefresh, true))
|
||||
@@ -311,11 +260,7 @@ func (s *Server) routes(cfg *config.Config) *http.ServeMux {
|
||||
mux.Handle("PUT "+base+"clients", s.authed(s.handleClientsPut, true))
|
||||
mux.Handle("GET "+base+"upstreams", s.authed(s.handleUpstreams, false))
|
||||
mux.Handle("PUT "+base+"upstreams", s.authed(s.handleUpstreamsPut, true))
|
||||
mux.Handle("GET "+base+"logs", s.authed(s.handleLogs, false))
|
||||
mux.Handle("GET "+base+"logs/stream", s.authed(s.handleLogStream, false))
|
||||
mux.Handle("POST "+base+"logs/clear", s.authed(s.handleLogsClear, true))
|
||||
mux.Handle("GET "+base+"cache", s.authed(s.handleCache, false))
|
||||
mux.Handle("GET "+base+"cache/top", s.authed(s.handleCacheTop, false))
|
||||
mux.Handle("POST "+base+"cache/flush", s.authed(s.handleCacheFlush, true))
|
||||
mux.Handle("GET "+base+"settings", s.authed(s.handleSettingsGet, false))
|
||||
mux.Handle("PUT "+base+"settings", s.authed(s.handleSettingsPut, true))
|
||||
|
||||
Reference in new issue
Block a user