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
+2
-144
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -56,20 +55,6 @@ func (s *Server) handleStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleDashboard serves the aggregated dashboard payload.
|
||||
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.Dashboard == nil {
|
||||
writeError(w, http.StatusNotImplemented, "dashboard not available")
|
||||
return
|
||||
}
|
||||
v, err := s.hooks.Dashboard(r.Context(), r.URL.Query().Get("range"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
// handleBlocklists serves the compiled blocklist state.
|
||||
func (s *Server) handleBlocklists(w http.ResponseWriter, _ *http.Request) {
|
||||
out := map[string]any{}
|
||||
@@ -470,122 +455,6 @@ func (s *Server) handleUpstreamsPut(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "version": version})
|
||||
}
|
||||
|
||||
// handleLogs pages the query log.
|
||||
func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.SearchLog == nil {
|
||||
writeError(w, http.StatusNotImplemented, "query log not available")
|
||||
return
|
||||
}
|
||||
q := LogQuery{
|
||||
Limit: queryInt(r, "limit", 200),
|
||||
Offset: queryInt(r, "offset", 0),
|
||||
Text: r.URL.Query().Get("q"),
|
||||
Action: r.URL.Query().Get("action"),
|
||||
Client: r.URL.Query().Get("client"),
|
||||
RCode: queryInt(r, "rcode", 0),
|
||||
BlockedOnly: queryBool(r, "blocked"),
|
||||
Since: queryTime(r, "since"),
|
||||
}
|
||||
v, err := s.hooks.SearchLog(r.Context(), q)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
// handleLogStream is the Server-Sent Events endpoint feeding the live view.
|
||||
func (s *Server) handleLogStream(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
writeError(w, http.StatusInternalServerError, "streaming unsupported")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
// Without this, a reverse proxy buffers the stream and the UI shows
|
||||
// nothing until the buffer fills.
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher.Flush()
|
||||
|
||||
send := func(event string, v any) bool {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
// A single-line data field is what the SSE grammar requires, so the
|
||||
// payload is compact JSON with no newlines (json.Marshal never emits
|
||||
// a raw newline).
|
||||
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, b); err != nil {
|
||||
return false
|
||||
}
|
||||
flusher.Flush()
|
||||
return true
|
||||
}
|
||||
// An initial event tells the client the stream is live, so the UI can show
|
||||
// a "connected" state before any query happens.
|
||||
if !send("ready", map[string]any{"at": time.Now(), "version": s.hooks.Version}) {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
entries <-chan LogEntry
|
||||
cancel func()
|
||||
)
|
||||
if s.hooks.SubscribeLog != nil {
|
||||
entries, cancel = s.hooks.SubscribeLog()
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
stats := time.NewTicker(time.Second)
|
||||
defer stats.Stop()
|
||||
ping := time.NewTicker(20 * time.Second)
|
||||
defer ping.Stop()
|
||||
|
||||
ctx := r.Context()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case e, ok := <-entries:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !send("entry", e) {
|
||||
return
|
||||
}
|
||||
case <-stats.C:
|
||||
if s.hooks.LogStats == nil {
|
||||
continue
|
||||
}
|
||||
if !send("stats", s.hooks.LogStats()) {
|
||||
return
|
||||
}
|
||||
case <-ping.C:
|
||||
// A comment frame keeps proxies from timing the stream out.
|
||||
if _, err := fmt.Fprint(w, ": ping\n\n"); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogsClear empties the query log.
|
||||
func (s *Server) handleLogsClear(w http.ResponseWriter, _ *http.Request) {
|
||||
if s.hooks.ClearLog == nil {
|
||||
writeError(w, http.StatusNotImplemented, "query log not available")
|
||||
return
|
||||
}
|
||||
if err := s.hooks.ClearLog(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// handleCache returns the cache snapshot.
|
||||
func (s *Server) handleCache(w http.ResponseWriter, _ *http.Request) {
|
||||
if s.hooks.CacheStats == nil {
|
||||
@@ -595,17 +464,6 @@ func (s *Server) handleCache(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, s.hooks.CacheStats())
|
||||
}
|
||||
|
||||
// handleCacheTop returns the hottest cached names.
|
||||
func (s *Server) handleCacheTop(w http.ResponseWriter, r *http.Request) {
|
||||
if s.hooks.CacheTop == nil {
|
||||
writeError(w, http.StatusNotImplemented, "cache not available")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": s.hooks.CacheTop(queryInt(r, "limit", 50)),
|
||||
})
|
||||
}
|
||||
|
||||
// handleCacheFlush empties the cache.
|
||||
func (s *Server) handleCacheFlush(w http.ResponseWriter, _ *http.Request) {
|
||||
if s.hooks.FlushCache == nil {
|
||||
@@ -856,8 +714,8 @@ func configWarnings(cfg *config.Config) []string {
|
||||
add("upstream.servers[%s]: insecure_skip_verify disables TLS validation", u.Addr)
|
||||
}
|
||||
}
|
||||
if cfg.QueryLog.Enabled && cfg.QueryLog.SampleRate == 1 && cfg.QueryLog.LogCached {
|
||||
add("qlog: logging every cached query on a busy instance is a lot of disk for little value; consider lowering sample_rate")
|
||||
if cfg.AccessLog.Enabled && cfg.AccessLog.SampleRate == 1 {
|
||||
add("access_log: sample_rate 1 logs every query; that is for short debugging sessions, the metrics already count ordinary traffic")
|
||||
}
|
||||
if cfg.Metrics.Enabled && !cfg.Metrics.Protect {
|
||||
add("metrics.protect: /metrics is unauthenticated and exposes client and domain statistics")
|
||||
|
||||
Reference in new issue
Block a user