Files
coredns/plugin/errors/errors.go
cangming cad961f75f plugin/errors: add show_first option to consolidate (#7702) (#7703)
Add optional show_first flag to consolidate directive that logs
the first error immediately and then consolidates subsequent errors.

When show_first is enabled:
- The first matching error is logged immediately with full details
  (rcode, domain, type, error message) using the configured log level
- Subsequent matching errors are consolidated during the period
- At period end:
  - If only one error occurred, no summary is printed (already logged)
  - If multiple errors occurred, summary shows the total count

Syntax:
  consolidate DURATION REGEXP [LEVEL] [show_first]

Example with 3 errors:
  [WARNING] 2 example.org. A: read udp 10.0.0.1:53->8.8.8.8:53: i/o timeout
  [WARNING] 3 errors like '^read udp .* i/o timeout$' occurred in last 30s

Example with 1 error:
  [WARNING] 2 example.org. A: read udp 10.0.0.1:53->8.8.8.8:53: i/o timeout

Implementation details:
- Add showFirst bool to pattern struct
- Rename inc() to consolidateError(), return false for showFirst case
- Use function pointer in ServeDNS to unify log calls with proper level
- Simplify logPattern() with single condition (cnt > 1 || !showFirst)
- Refactor parseLogLevel() to parseOptionalParams() with map-based dispatch
- Validate parameter order: log level must come before show_first
- Update README.md with show_first documentation and examples
- Add comprehensive test cases for show_first functionality

Signed-off-by: cangming <cangming@cangming.app>
2025-12-09 18:15:49 -08:00

126 lines
3.1 KiB
Go

// Package errors implements an error handling plugin.
package errors
import (
"context"
"regexp"
"sync/atomic"
"time"
"unsafe"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
var log = clog.NewWithPlugin("errors")
type pattern struct {
ptimer unsafe.Pointer
count uint32
period time.Duration
pattern *regexp.Regexp
logCallback func(format string, v ...any)
showFirst bool
}
func (p *pattern) timer() *time.Timer {
return (*time.Timer)(atomic.LoadPointer(&p.ptimer))
}
func (p *pattern) setTimer(t *time.Timer) {
atomic.StorePointer(&p.ptimer, unsafe.Pointer(t))
}
// errorHandler handles DNS errors (and errors from other plugin).
type errorHandler struct {
patterns []*pattern
stopFlag uint32
Next plugin.Handler
}
func newErrorHandler() *errorHandler {
return &errorHandler{}
}
func (h *errorHandler) logPattern(i int) {
cnt := atomic.SwapUint32(&h.patterns[i].count, 0)
if cnt == 0 {
return
}
if cnt > 1 || !h.patterns[i].showFirst {
h.patterns[i].logCallback("%d errors like '%s' occurred in last %s",
cnt, h.patterns[i].pattern.String(), h.patterns[i].period)
}
}
// consolidateError records an error occurrence for pattern i.
// Returns false when cnt == 1 and showFirst is configured, so the error
// will be printed by the caller using the pattern's logCallback.
func (h *errorHandler) consolidateError(i int) bool {
if atomic.LoadUint32(&h.stopFlag) > 0 {
return false
}
cnt := atomic.AddUint32(&h.patterns[i].count, 1)
if cnt == 1 {
ind := i
t := time.AfterFunc(h.patterns[ind].period, func() {
h.logPattern(ind)
})
h.patterns[ind].setTimer(t)
if atomic.LoadUint32(&h.stopFlag) > 0 && t.Stop() {
h.logPattern(ind)
}
// If showFirst is enabled, return false so the first error
// will be printed by the caller using the pattern's logCallback
return !h.patterns[i].showFirst
}
return true
}
func (h *errorHandler) stop() {
atomic.StoreUint32(&h.stopFlag, 1)
for i := range h.patterns {
t := h.patterns[i].timer()
if t != nil && t.Stop() {
h.logPattern(i)
}
}
}
// ServeDNS implements the plugin.Handler interface.
func (h *errorHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
rcode, err := plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
if err != nil {
strErr := err.Error()
state := request.Request{W: w, Req: r}
// Default to error logging
logFunc := log.Errorf
for i := range h.patterns {
if h.patterns[i].pattern.MatchString(strErr) {
if h.consolidateError(i) {
// Error is consolidated, no need to log
return rcode, err
}
// consolidateError returned false (showFirst case)
// Use the pattern's configured log level
logFunc = h.patterns[i].logCallback
break
}
}
// Log with the appropriate log level
logFunc("%d %s %s: %s", rcode, state.Name(), state.Type(), strErr)
}
return rcode, err
}
// Name implements the plugin.Handler interface.
func (h *errorHandler) Name() string { return "errors" }