middleware/log: allows logging based on response classes (#325)

Add the ability to add a class of responses to be logged; success,
denial or error. The default is to log everything (all).

Fixes #258
This commit is contained in:
Miek Gieben 2016-10-10 12:09:29 +01:00 committed by GitHub
parent caa3976bfe
commit c22b7b2252
13 changed files with 348 additions and 108 deletions

View file

@ -20,7 +20,7 @@ Or if you want more control:
~~~ txt
cache [ttl] [zones...] {
noerror capacity [ttl]
success capacity [ttl]
denial capacity [ttl]
}
~~~

View file

@ -60,7 +60,7 @@ type ResponseWriter struct {
// WriteMsg implements the dns.ResponseWriter interface.
func (c *ResponseWriter) WriteMsg(res *dns.Msg) error {
do := false
mt, opt := response.Classify(res)
mt, opt := response.Typify(res)
if opt != nil {
do = opt.Do()
}
@ -93,7 +93,7 @@ func (c *ResponseWriter) set(m *dns.Msg, key string, mt response.Type, duration
}
switch mt {
case response.Success, response.Delegation:
case response.NoError, response.Delegation:
i := newItem(m, duration)
c.pcache.Add(key, i)

View file

@ -94,7 +94,7 @@ func TestCache(t *testing.T) {
m = cacheMsg(m, tc)
do := tc.in.Do
mt, _ := response.Classify(m)
mt, _ := response.Typify(m)
k := key(m, mt, do)
crr.set(m, k, mt, c.pttl)

View file

@ -87,7 +87,7 @@ func setMsgTTL(m *dns.Msg, ttl uint32) {
}
func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
if mt != response.Success && mt != response.NameError && mt != response.NoData {
if mt != response.NoError && mt != response.NameError && mt != response.NoData {
return 0
}
@ -98,7 +98,7 @@ func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
if r.Header().Rrtype == dns.TypeSOA {
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
}
case response.Success, response.Delegation:
case response.NoError, response.Delegation:
if r.Header().Ttl < uint32(minTTL.Seconds()) {
minTTL = time.Duration(r.Header().Ttl) * time.Second
}

View file

@ -41,7 +41,7 @@ func New(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec {
func (d Dnssec) Sign(state request.Request, zone string, now time.Time) *dns.Msg {
req := state.Req
mt, _ := response.Classify(req) // TODO(miek): need opt record here?
mt, _ := response.Typify(req) // TODO(miek): need opt record here?
if mt == response.Delegation {
return req
}

View file

@ -1,34 +1,53 @@
# log
`log` enables request logging. The request log is also known in some vernacular as an access log.
`log` enables query logging.
## Syntax
~~~
~~~ txt
log
~~~
* With no arguments, a query log entry is written to query.log in the common log format for all requests
(base name = .).
~~~
~~~ txt
log file
~~~
* file is the log file to create (or append to). The base path is assumed to be . .
* file is the log file to create (or append to). The base name is assumed to be '.' .
~~~
log name file [format]
~~~ txt
log [name] [file] [format]
~~~
* `name` is the base name to match in order to be logged
* `file` is the log file to create (or append to)
* `format` is the log format to use (default is Common Log Format)
You can further specify the class of responses that get logged:
~~~ txt
log [name] [file] [format] {
class [success|denial|error|all]
}
~~~
Here *success*, *denial* and *error* denotes the class of responses that should be logged. The
classes have the following meaning:
* `success`: successful response
* `denial`: either NXDOMAIN or NODATA (name exists, type does not)
* `error`: SERVFAIL, NOTIMP, REFUSED, etc. Any that indicated the remove server is not willing to
resolve the request.
* `all`: the default is nothing is specified.
If no class is specified it defaults to *all*.
## Log File
The log file can be any filename. It could also be stdout or stderr to write the log to the console,
or syslog to write to the system log (except on Windows). If the log file does not exist beforehand,
The log file can be any filename. It could also be *stdout* or *stderr* to write the log to the console,
or *syslog* to write to the system log (except on Windows). If the log file does not exist beforehand,
CoreDNS will create it before appending to it.
## Log Format
@ -53,6 +72,11 @@ The following place holders are supported:
* `{>id}`: query ID
* `{>opcode}`: query OPCODE
The default Common Log Format is:
~~~ txt
`{remote} - [{when}] "{type} {class} {name} {proto} {>do} {>bufsize}" {rcode} {size} {duration}`
~~~
## Examples
@ -67,3 +91,11 @@ Custom log format:
~~~
log . ../query.log "{proto} Request: {name} {type} {>id}"
~~~
Only log denials for example.org (and below to a file)
~~~
log example.org example-query-log {
class denial
}
~~~

View file

@ -10,6 +10,7 @@ import (
"github.com/miekg/coredns/middleware/pkg/dnsrecorder"
"github.com/miekg/coredns/middleware/pkg/rcode"
"github.com/miekg/coredns/middleware/pkg/replacer"
"github.com/miekg/coredns/middleware/pkg/response"
"github.com/miekg/coredns/request"
"github.com/miekg/dns"
@ -27,7 +28,10 @@ type Logger struct {
func (l Logger) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
for _, rule := range l.Rules {
if middleware.Name(rule.NameScope).Matches(state.Name()) {
if !middleware.Name(rule.NameScope).Matches(state.Name()) {
continue
}
responseRecorder := dnsrecorder.New(w)
rc, err := l.Next.ServeDNS(ctx, responseRecorder, r)
@ -42,22 +46,28 @@ func (l Logger) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
state.SizeAndDo(answer)
metrics.Report(state, metrics.Dropped, rcode.ToString(rc), answer.Len(), time.Now())
w.WriteMsg(answer)
}
rc = 0
}
class, _ := response.Classify(responseRecorder.Msg)
if rule.Class == response.All || rule.Class == class {
rep := replacer.New(r, responseRecorder, CommonLogEmptyValue)
rule.Log.Println(rep.Replace(rule.Format))
}
return rc, err
}
}
return l.Next.ServeDNS(ctx, w, r)
}
// Rule configures the logging middleware.
type Rule struct {
NameScope string
Class response.Class
OutputFile string
Format string
Log *log.Logger

View file

@ -7,21 +7,15 @@ import (
"testing"
"github.com/miekg/coredns/middleware/pkg/dnsrecorder"
"github.com/miekg/coredns/middleware/pkg/response"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
type erroringMiddleware struct{}
func (erroringMiddleware) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
return dns.RcodeServerFailure, nil
}
func TestLoggedStatus(t *testing.T) {
var f bytes.Buffer
var next erroringMiddleware
rule := Rule{
NameScope: ".",
Format: DefaultLogFormat,
@ -30,7 +24,7 @@ func TestLoggedStatus(t *testing.T) {
logger := Logger{
Rules: []Rule{rule},
Next: next,
Next: test.ErrorHandler(),
}
ctx := context.TODO()
@ -41,11 +35,67 @@ func TestLoggedStatus(t *testing.T) {
rcode, _ := logger.ServeDNS(ctx, rec, r)
if rcode != 0 {
t.Error("Expected rcode to be 0 - was", rcode)
t.Errorf("Expected rcode to be 0 - was: %d", rcode)
}
logged := f.String()
if !strings.Contains(logged, "A IN example.org. udp false 512") {
t.Error("Expected it to be logged. Logged string -", logged)
t.Errorf("Expected it to be logged. Logged string: %s", logged)
}
}
func TestLoggedClassDenial(t *testing.T) {
var f bytes.Buffer
rule := Rule{
NameScope: ".",
Format: DefaultLogFormat,
Log: log.New(&f, "", 0),
Class: response.Denial,
}
logger := Logger{
Rules: []Rule{rule},
Next: test.ErrorHandler(),
}
ctx := context.TODO()
r := new(dns.Msg)
r.SetQuestion("example.org.", dns.TypeA)
rec := dnsrecorder.New(&test.ResponseWriter{})
logger.ServeDNS(ctx, rec, r)
logged := f.String()
if len(logged) != 0 {
t.Errorf("Expected it not to be logged, but got string: %s", logged)
}
}
func TestLoggedClassError(t *testing.T) {
var f bytes.Buffer
rule := Rule{
NameScope: ".",
Format: DefaultLogFormat,
Log: log.New(&f, "", 0),
Class: response.Error,
}
logger := Logger{
Rules: []Rule{rule},
Next: test.ErrorHandler(),
}
ctx := context.TODO()
r := new(dns.Msg)
r.SetQuestion("example.org.", dns.TypeA)
rec := dnsrecorder.New(&test.ResponseWriter{})
logger.ServeDNS(ctx, rec, r)
logged := f.String()
if !strings.Contains(logged, "SERVFAIL") {
t.Errorf("Expected it to be logged. Logged string: %s", logged)
}
}

View file

@ -7,6 +7,7 @@ import (
"github.com/miekg/coredns/core/dnsserver"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/pkg/response"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy"
@ -105,6 +106,26 @@ func logParse(c *caddy.Controller) ([]Rule, error) {
Format: format,
})
}
// Class refinements in an extra block.
for c.NextBlock() {
switch c.Val() {
// class followed by all, denial, error or success.
case "class":
classes := c.RemainingArgs()
if len(classes) == 0 {
return nil, c.ArgErr()
}
cls, err := response.ClassFromString(classes[0])
if err != nil {
return nil, err
}
// update class and the last added Rule (bit icky)
rules[len(rules)-1].Class = cls
default:
return nil, c.ArgErr()
}
}
}
return rules, nil

View file

@ -3,6 +3,8 @@ package log
import (
"testing"
"github.com/miekg/coredns/middleware/pkg/response"
"github.com/mholt/caddy"
)
@ -62,6 +64,31 @@ func TestLogParse(t *testing.T) {
OutputFile: "log.txt",
Format: "{when}",
}}},
{`log example.org log.txt {
class all
}`, false, []Rule{{
NameScope: "example.org.",
OutputFile: "log.txt",
Format: CommonLogFormat,
Class: response.All,
}}},
{`log example.org log.txt {
class denial
}`, false, []Rule{{
NameScope: "example.org.",
OutputFile: "log.txt",
Format: CommonLogFormat,
Class: response.Denial,
}}},
{`log {
class denial
}`, false, []Rule{{
NameScope: ".",
OutputFile: DefaultLogFilename,
Format: CommonLogFormat,
Class: response.Denial,
}}},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.inputLogRules)
@ -92,6 +119,11 @@ func TestLogParse(t *testing.T) {
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
}
if actualLogRule.Class != test.expectedLogRules[j].Class {
t.Errorf("Test %d expected %dth LogRule Class to be %s , but got %s",
i, j, test.expectedLogRules[j].Class, actualLogRule.Class)
}
}
}

View file

@ -1,74 +1,73 @@
package response
import "github.com/miekg/dns"
import (
"fmt"
// Type is the type of the message
type Type int
const (
// Success indicates a positive reply
Success Type = iota
// NameError is a NXDOMAIN in header, SOA in auth.
NameError
// NoData indicated name found, but not the type: NOERROR in header, SOA in auth.
NoData
// Delegation is a msg with a pointer to another nameserver: NOERROR in header, NS in auth, optionally fluff in additional (not checked).
Delegation
// OtherError indicated any other error: don't cache these.
OtherError
"github.com/miekg/dns"
)
func (t Type) String() string {
switch t {
// Class holds sets of Types
type Class int
const (
// All is a meta class encompassing all the classes.
All Class = iota
// Success is a class for a successful response.
Success
// Denial is a class for denying existence (NXDOMAIN, or a nodata: type does not exist)
Denial
// Error is a class for errors, right now defined as not Success and not Denial
Error
)
func (c Class) String() string {
switch c {
case All:
return "all"
case Success:
return "NOERROR"
case NameError:
return "NXDOMAIN"
case NoData:
return "NODATA"
case Delegation:
return "DELEGATION"
case OtherError:
return "OTHERERROR"
return "success"
case Denial:
return "denial"
case Error:
return "error"
}
return ""
}
// Classify classifies a message, it returns the Type.
func Classify(m *dns.Msg) (Type, *dns.OPT) {
opt := m.IsEdns0()
if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess {
return Success, opt
// ClassFromString returns the class from the string s. If not class matches
// the All class and an error are returned
func ClassFromString(s string) (Class, error) {
switch s {
case "all":
return All, nil
case "success":
return Success, nil
case "denial":
return Denial, nil
case "error":
return Error, nil
}
soa := false
ns := 0
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeSOA {
soa = true
continue
}
if r.Header().Rrtype == dns.TypeNS {
ns++
}
}
// Check length of different sections, and drop stuff that is just to large? TODO(miek).
if soa && m.Rcode == dns.RcodeSuccess {
return NoData, opt
}
if soa && m.Rcode == dns.RcodeNameError {
return NameError, opt
}
if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess {
return Delegation, opt
}
if m.Rcode == dns.RcodeSuccess {
return Success, opt
}
return OtherError, opt
return All, fmt.Errorf("invalid Class: %s", s)
}
// Classify classifies a dns message: it returns its Class.
func Classify(m *dns.Msg) (Class, *dns.OPT) {
t, o := Typify(m)
return classify(t), o
}
// Does need to be exported?
func classify(t Type) Class {
switch t {
case NoError, Delegation:
return Success
case NameError, NoData:
return Denial
case OtherError:
fallthrough
default:
return Error
}
// never reached
return All
}

View file

@ -0,0 +1,96 @@
package response
import (
"fmt"
"github.com/miekg/dns"
)
// Type is the type of the message
type Type int
const (
// Success indicates a positive reply
NoError Type = iota
// NameError is a NXDOMAIN in header, SOA in auth.
NameError
// NoData indicated name found, but not the type: NOERROR in header, SOA in auth.
NoData
// Delegation is a msg with a pointer to another nameserver: NOERROR in header, NS in auth, optionally fluff in additional (not checked).
Delegation
// OtherError indicated any other error: don't cache these.
OtherError
)
func (t Type) String() string {
switch t {
case NoError:
return "NOERROR"
case NameError:
return "NXDOMAIN"
case NoData:
return "NODATA"
case Delegation:
return "DELEGATION"
case OtherError:
return "OTHERERROR"
}
return ""
}
// TypeFromString returns the type from the string s. If not type matches
// the OtherError type and an error are returned.
func TypeFromString(s string) (Type, error) {
switch s {
case "NOERROR":
return NoError, nil
case "NXDOMAIN":
return NameError, nil
case "NODATA":
return NoData, nil
case "DELEGATION":
return Delegation, nil
case "OTHERERROR":
return OtherError, nil
}
return NoError, fmt.Errorf("invalid Type: %s", s)
}
// Typify classifies a message, it returns the Type.
func Typify(m *dns.Msg) (Type, *dns.OPT) {
opt := m.IsEdns0()
if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess {
return NoError, opt
}
soa := false
ns := 0
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeSOA {
soa = true
continue
}
if r.Header().Rrtype == dns.TypeNS {
ns++
}
}
// Check length of different sections, and drop stuff that is just to large? TODO(miek).
if soa && m.Rcode == dns.RcodeSuccess {
return NoData, opt
}
if soa && m.Rcode == dns.RcodeNameError {
return NameError, opt
}
if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess {
return Delegation, opt
}
if m.Rcode == dns.RcodeSuccess {
return NoError, opt
}
return OtherError, opt
}

View file

@ -10,7 +10,7 @@ import (
func TestClassifyDelegation(t *testing.T) {
m := delegationMsg()
mt, _ := Classify(m)
mt, _ := Typify(m)
if mt != Delegation {
t.Errorf("message is wrongly classified, expected delegation, got %d", mt)
}