aboutsummaryrefslogtreecommitdiff
path: root/plugin/log
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/log')
-rw-r--r--plugin/log/README.md102
-rw-r--r--plugin/log/log.go91
-rw-r--r--plugin/log/log_test.go101
-rw-r--r--plugin/log/setup.go116
-rw-r--r--plugin/log/setup_test.go130
5 files changed, 540 insertions, 0 deletions
diff --git a/plugin/log/README.md b/plugin/log/README.md
new file mode 100644
index 000000000..223888ccc
--- /dev/null
+++ b/plugin/log/README.md
@@ -0,0 +1,102 @@
+# log
+
+*log* enables query logging to standard output.
+
+## Syntax
+
+~~~ txt
+log
+~~~
+
+* With no arguments, a query log entry is written to *stdout* in the common log format for all requests
+
+~~~ txt
+log FILE
+~~~
+
+* **FILE** is the log file to create (or append to). The *only* valid name for **FILE** is *stdout*.
+
+~~~ txt
+log [NAME] FILE [FORMAT]
+~~~
+
+* `NAME` is the name to match in order to be logged
+* `FILE` is the log file (again only *stdout* is allowed here).
+* `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. Anything that indicates the remote server is not willing to
+ resolve the request.
+* `all`: the default - nothing is specified.
+
+If no class is specified, it defaults to *all*.
+
+## Log File
+
+The "log file" can only be *stdout*. CoreDNS expects another service to pick up this output and deal
+with it, i.e. journald when using systemd or Docker's logging capabilities.
+
+## Log Format
+
+You can specify a custom log format with any placeholder values. Log supports both request and
+response placeholders.
+
+The following place holders are supported:
+
+* `{type}`: qtype of the request
+* `{name}`: qname of the request
+* `{class}`: qclass of the request
+* `{proto}`: protocol used (tcp or udp)
+* `{when}`: time of the query
+* `{remote}`: client's IP address
+* `{size}`: request size in bytes
+* `{port}`: client's port
+* `{duration}`: response duration
+* `{rcode}`: response RCODE
+* `{rsize}`: response size
+* `{>rflags}`: response flags, each set flag will be displayed, e.g. "aa, tc". This includes the qr
+ bit as well.
+* `{>bufsize}`: the EDNS0 buffer size advertised in the query
+* `{>do}`: is the EDNS0 DO (DNSSEC OK) bit set in the query
+* `{>id}`: query ID
+* `{>opcode}`: query OPCODE
+
+The default Common Log Format is:
+
+~~~ txt
+`{remote} - [{when}] "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}`
+~~~
+
+## Examples
+
+Log all requests to stdout
+
+~~~
+log stdout
+~~~
+
+Custom log format, for all zones (`.`)
+
+~~~
+log . stdout "{proto} Request: {name} {type} {>id}"
+~~~
+
+Only log denials for example.org (and below to a file)
+
+~~~
+log example.org stdout {
+ class denial
+}
+~~~
diff --git a/plugin/log/log.go b/plugin/log/log.go
new file mode 100644
index 000000000..52af79d35
--- /dev/null
+++ b/plugin/log/log.go
@@ -0,0 +1,91 @@
+// Package log implements basic but useful request (access) logging plugin.
+package log
+
+import (
+ "log"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics/vars"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/pkg/rcode"
+ "github.com/coredns/coredns/plugin/pkg/replacer"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Logger is a basic request logging plugin.
+type Logger struct {
+ Next plugin.Handler
+ Rules []Rule
+ ErrorFunc func(dns.ResponseWriter, *dns.Msg, int) // failover error handler
+}
+
+// ServeDNS implements the plugin.Handler interface.
+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 !plugin.Name(rule.NameScope).Matches(state.Name()) {
+ continue
+ }
+
+ rrw := dnsrecorder.New(w)
+ rc, err := plugin.NextOrFailure(l.Name(), l.Next, ctx, rrw, r)
+
+ if rc > 0 {
+ // There was an error up the chain, but no response has been written yet.
+ // The error must be handled here so the log entry will record the response size.
+ if l.ErrorFunc != nil {
+ l.ErrorFunc(rrw, r, rc)
+ } else {
+ answer := new(dns.Msg)
+ answer.SetRcode(r, rc)
+ state.SizeAndDo(answer)
+
+ vars.Report(state, vars.Dropped, rcode.ToString(rc), answer.Len(), time.Now())
+
+ w.WriteMsg(answer)
+ }
+ rc = 0
+ }
+
+ tpe, _ := response.Typify(rrw.Msg, time.Now().UTC())
+ class := response.Classify(tpe)
+ if rule.Class == response.All || rule.Class == class {
+ rep := replacer.New(r, rrw, CommonLogEmptyValue)
+ rule.Log.Println(rep.Replace(rule.Format))
+ }
+
+ return rc, err
+
+ }
+ return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r)
+}
+
+// Name implements the Handler interface.
+func (l Logger) Name() string { return "log" }
+
+// Rule configures the logging plugin.
+type Rule struct {
+ NameScope string
+ Class response.Class
+ OutputFile string
+ Format string
+ Log *log.Logger
+}
+
+const (
+ // DefaultLogFilename is the default output name. This is the only supported value.
+ DefaultLogFilename = "stdout"
+ // CommonLogFormat is the common log format.
+ CommonLogFormat = `{remote} ` + CommonLogEmptyValue + ` [{when}] "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}`
+ // CommonLogEmptyValue is the common empty log value.
+ CommonLogEmptyValue = "-"
+ // CombinedLogFormat is the combined log format.
+ CombinedLogFormat = CommonLogFormat + ` "{>opcode}"`
+ // DefaultLogFormat is the default log format.
+ DefaultLogFormat = CommonLogFormat
+)
diff --git a/plugin/log/log_test.go b/plugin/log/log_test.go
new file mode 100644
index 000000000..ee1201a13
--- /dev/null
+++ b/plugin/log/log_test.go
@@ -0,0 +1,101 @@
+package log
+
+import (
+ "bytes"
+ "log"
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/pkg/response"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestLoggedStatus(t *testing.T) {
+ var f bytes.Buffer
+ rule := Rule{
+ NameScope: ".",
+ Format: DefaultLogFormat,
+ Log: log.New(&f, "", 0),
+ }
+
+ 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{})
+
+ rcode, _ := logger.ServeDNS(ctx, rec, r)
+ if rcode != 0 {
+ t.Errorf("Expected rcode to be 0 - was: %d", rcode)
+ }
+
+ logged := f.String()
+ if !strings.Contains(logged, "A IN example.org. udp 29 false 512") {
+ 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)
+ }
+}
diff --git a/plugin/log/setup.go b/plugin/log/setup.go
new file mode 100644
index 000000000..673962f10
--- /dev/null
+++ b/plugin/log/setup.go
@@ -0,0 +1,116 @@
+package log
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/response"
+
+ "github.com/mholt/caddy"
+ "github.com/miekg/dns"
+)
+
+func init() {
+ caddy.RegisterPlugin("log", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ rules, err := logParse(c)
+ if err != nil {
+ return plugin.Error("log", err)
+ }
+
+ // Open the log files for writing when the server starts
+ c.OnStartup(func() error {
+ for i := 0; i < len(rules); i++ {
+ // We only support stdout
+ writer := os.Stdout
+ if rules[i].OutputFile != "stdout" {
+ return plugin.Error("log", fmt.Errorf("invalid log file: %s", rules[i].OutputFile))
+ }
+
+ rules[i].Log = log.New(writer, "", 0)
+ }
+
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return Logger{Next: next, Rules: rules, ErrorFunc: dnsserver.DefaultErrorFunc}
+ })
+
+ return nil
+}
+
+func logParse(c *caddy.Controller) ([]Rule, error) {
+ var rules []Rule
+
+ for c.Next() {
+ args := c.RemainingArgs()
+
+ if len(args) == 0 {
+ // Nothing specified; use defaults
+ rules = append(rules, Rule{
+ NameScope: ".",
+ OutputFile: DefaultLogFilename,
+ Format: DefaultLogFormat,
+ })
+ } else if len(args) == 1 {
+ // Only an output file specified.
+ rules = append(rules, Rule{
+ NameScope: ".",
+ OutputFile: args[0],
+ Format: DefaultLogFormat,
+ })
+ } else {
+ // Name scope, output file, and maybe a format specified
+
+ format := DefaultLogFormat
+
+ if len(args) > 2 {
+ switch args[2] {
+ case "{common}":
+ format = CommonLogFormat
+ case "{combined}":
+ format = CombinedLogFormat
+ default:
+ format = args[2]
+ }
+ }
+
+ rules = append(rules, Rule{
+ NameScope: dns.Fqdn(args[0]),
+ OutputFile: args[1],
+ 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
+}
diff --git a/plugin/log/setup_test.go b/plugin/log/setup_test.go
new file mode 100644
index 000000000..161f674be
--- /dev/null
+++ b/plugin/log/setup_test.go
@@ -0,0 +1,130 @@
+package log
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/response"
+
+ "github.com/mholt/caddy"
+)
+
+func TestLogParse(t *testing.T) {
+ tests := []struct {
+ inputLogRules string
+ shouldErr bool
+ expectedLogRules []Rule
+ }{
+ {`log`, false, []Rule{{
+ NameScope: ".",
+ OutputFile: DefaultLogFilename,
+ Format: DefaultLogFormat,
+ }}},
+ {`log log.txt`, false, []Rule{{
+ NameScope: ".",
+ OutputFile: "log.txt",
+ Format: DefaultLogFormat,
+ }}},
+ {`log example.org log.txt`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: DefaultLogFormat,
+ }}},
+ {`log example.org. stdout`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "stdout",
+ Format: DefaultLogFormat,
+ }}},
+ {`log example.org log.txt {common}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: CommonLogFormat,
+ }}},
+ {`log example.org accesslog.txt {combined}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "accesslog.txt",
+ Format: CombinedLogFormat,
+ }}},
+ {`log example.org. log.txt
+ log example.net accesslog.txt {combined}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "log.txt",
+ Format: DefaultLogFormat,
+ }, {
+ NameScope: "example.net.",
+ OutputFile: "accesslog.txt",
+ Format: CombinedLogFormat,
+ }}},
+ {`log example.org stdout {host}
+ log example.org log.txt {when}`, false, []Rule{{
+ NameScope: "example.org.",
+ OutputFile: "stdout",
+ Format: "{host}",
+ }, {
+ NameScope: "example.org.",
+ 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)
+ actualLogRules, err := logParse(c)
+
+ if err == nil && test.shouldErr {
+ t.Errorf("Test %d didn't error, but it should have", i)
+ } else if err != nil && !test.shouldErr {
+ t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
+ }
+ if len(actualLogRules) != len(test.expectedLogRules) {
+ t.Fatalf("Test %d expected %d no of Log rules, but got %d ",
+ i, len(test.expectedLogRules), len(actualLogRules))
+ }
+ for j, actualLogRule := range actualLogRules {
+
+ if actualLogRule.NameScope != test.expectedLogRules[j].NameScope {
+ t.Errorf("Test %d expected %dth LogRule NameScope to be %s , but got %s",
+ i, j, test.expectedLogRules[j].NameScope, actualLogRule.NameScope)
+ }
+
+ if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
+ t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
+ i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
+ }
+
+ if actualLogRule.Format != test.expectedLogRules[j].Format {
+ 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)
+ }
+ }
+ }
+
+}