mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
The journal records the network state of the host and each change of that state. The support team reads one file to see the interfaces, the routes, the resolvers, and the recoveries around an incident. The same file shows the health of the query path. A change that touches only AirDrop or virtual interfaces runs no DNS work.
179 lines
5.3 KiB
Go
179 lines
5.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// Classes of per-query errors. Each class keeps its own budget per upstream.
|
|
const (
|
|
sampleClassResolveFailed = "resolve_failed"
|
|
sampleClassAllEndpointsFailed = "all_endpoints_failed"
|
|
sampleClassSendResponseFailed = "send_response_failed"
|
|
// sampleClassInternalDomain names a failure of an Internal Domain resolver.
|
|
// It stays apart from the other classes, because an endpoint away from the
|
|
// organization network cannot reach such a resolver.
|
|
sampleClassInternalDomain = "internal_domain"
|
|
)
|
|
|
|
const (
|
|
// querySampleLimit is the number of lines that stay at error level in one window.
|
|
querySampleLimit = 5
|
|
// querySampleWindow is the length of a window. It starts at the first error of the window.
|
|
querySampleWindow = 60 * time.Second
|
|
)
|
|
|
|
// sampleKey identifies one budget.
|
|
type sampleKey struct {
|
|
class string
|
|
upstream string
|
|
}
|
|
|
|
// sampleWindow counts the events of one budget between its start and its end.
|
|
type sampleWindow struct {
|
|
start time.Time
|
|
count int
|
|
suppressed int
|
|
timer *time.Timer
|
|
}
|
|
|
|
// ended reports whether the window closed at or before now.
|
|
func (w *sampleWindow) ended(now time.Time) bool {
|
|
return !now.Before(w.start.Add(querySampleWindow))
|
|
}
|
|
|
|
// sampleSummary holds what one closed window reports.
|
|
type sampleSummary struct {
|
|
key sampleKey
|
|
count int
|
|
suppressed int
|
|
}
|
|
|
|
// errorSampler bounds the noise of a repeated per-query error. It lowers the
|
|
// level of a line, never its presence, so the debug stream stays complete. A
|
|
// run with -v holds every stream at info, and the lowered lines fall below it.
|
|
// The zero value is ready to use.
|
|
type errorSampler struct {
|
|
mu sync.Mutex
|
|
windows map[sampleKey]*sampleWindow
|
|
now func() time.Time
|
|
after func(time.Duration, func()) *time.Timer
|
|
// onFailure counts the event for the query health tracker. The daemon sets
|
|
// it once at start, before a listener serves a query.
|
|
onFailure func(class string)
|
|
}
|
|
|
|
// event returns the event a call site logs with. The first querySampleLimit
|
|
// events of a window come back at error level, the later ones at debug level.
|
|
// The caller adds its own fields and the message.
|
|
func (s *errorSampler) event(class, upstream string) *zerolog.Event {
|
|
key := sampleKey{class: class, upstream: upstream}
|
|
|
|
s.mu.Lock()
|
|
now := s.clock()
|
|
var summaries []sampleSummary
|
|
window := s.windows[key]
|
|
if window != nil && window.ended(now) {
|
|
summaries = appendWindowSummary(summaries, key, window)
|
|
s.dropWindow(key, window)
|
|
window = nil
|
|
}
|
|
if window == nil {
|
|
window = s.startWindow(key, now)
|
|
}
|
|
window.count++
|
|
suppressed := window.count > querySampleLimit
|
|
if suppressed {
|
|
window.suppressed++
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
// A lowered line keeps its place in the breakdown of the window. It is one
|
|
// event, not one failed query, so it does not change the grade.
|
|
if s.onFailure != nil {
|
|
s.onFailure(class)
|
|
}
|
|
s.logSummaries(summaries)
|
|
|
|
if suppressed {
|
|
return mainLog.Load().Debug()
|
|
}
|
|
return mainLog.Load().Error()
|
|
}
|
|
|
|
// closeExpired closes every window that ended at or before now. The window
|
|
// timer calls it, so an idle window reports without a new event.
|
|
func (s *errorSampler) closeExpired(now time.Time) {
|
|
s.mu.Lock()
|
|
var summaries []sampleSummary
|
|
for key, window := range s.windows {
|
|
if !window.ended(now) {
|
|
continue
|
|
}
|
|
summaries = appendWindowSummary(summaries, key, window)
|
|
s.dropWindow(key, window)
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
s.logSummaries(summaries)
|
|
}
|
|
|
|
// startWindow opens a window and arms the timer that closes it. The caller holds the lock.
|
|
func (s *errorSampler) startWindow(key sampleKey, start time.Time) *sampleWindow {
|
|
if s.windows == nil {
|
|
s.windows = make(map[sampleKey]*sampleWindow)
|
|
}
|
|
window := &sampleWindow{start: start}
|
|
s.windows[key] = window
|
|
window.timer = s.afterFunc()(querySampleWindow, func() { s.closeExpired(s.clock()) })
|
|
return window
|
|
}
|
|
|
|
// dropWindow forgets a window and stops its timer. The caller holds the lock.
|
|
func (s *errorSampler) dropWindow(key sampleKey, window *sampleWindow) {
|
|
if window.timer != nil {
|
|
window.timer.Stop()
|
|
}
|
|
delete(s.windows, key)
|
|
}
|
|
|
|
// logSummaries reports the closed windows. The caller released the lock first,
|
|
// because the logger writes back through code that can take it again.
|
|
func (s *errorSampler) logSummaries(summaries []sampleSummary) {
|
|
for _, summary := range summaries {
|
|
mainLog.Load().Error().
|
|
Str("class", summary.key.class).
|
|
Str("upstream", summary.key.upstream).
|
|
Int("count", summary.count).
|
|
Int("suppressed", summary.suppressed).
|
|
Int("window_s", int(querySampleWindow/time.Second)).
|
|
Msg("Per-query errors sampled")
|
|
}
|
|
}
|
|
|
|
// clock reads the time of the sampler. A test sets its own clock, so a window
|
|
// closes without a wait.
|
|
func (s *errorSampler) clock() time.Time {
|
|
if s.now != nil {
|
|
return s.now()
|
|
}
|
|
return time.Now()
|
|
}
|
|
|
|
func (s *errorSampler) afterFunc() func(time.Duration, func()) *time.Timer {
|
|
if s.after != nil {
|
|
return s.after
|
|
}
|
|
return time.AfterFunc
|
|
}
|
|
|
|
// appendWindowSummary keeps the windows that suppressed at least one line.
|
|
func appendWindowSummary(summaries []sampleSummary, key sampleKey, window *sampleWindow) []sampleSummary {
|
|
if window.suppressed == 0 {
|
|
return summaries
|
|
}
|
|
return append(summaries, sampleSummary{key: key, count: window.count, suppressed: window.suppressed})
|
|
}
|