plugin/log - Support for Metadata (#2251)

* - add metadata support to Log

* - adapt ctx after rebase
This commit is contained in:
Francois Tur 2018-11-13 14:20:49 -05:00 committed by John Belamaric
parent 35c5474660
commit 94c9aae323
5 changed files with 123 additions and 19 deletions

View file

@ -61,6 +61,7 @@ The following place holders are supported:
* `{class}`: qclass of the request * `{class}`: qclass of the request
* `{proto}`: protocol used (tcp or udp) * `{proto}`: protocol used (tcp or udp)
* `{remote}`: client's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]` * `{remote}`: client's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]`
* `{local}`: server's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]`
* `{size}`: request size in bytes * `{size}`: request size in bytes
* `{port}`: client's port * `{port}`: client's port
* `{duration}`: response duration * `{duration}`: response duration
@ -72,6 +73,10 @@ The following place holders are supported:
* `{>do}`: is the EDNS0 DO (DNSSEC OK) bit set in the query * `{>do}`: is the EDNS0 DO (DNSSEC OK) bit set in the query
* `{>id}`: query ID * `{>id}`: query ID
* `{>opcode}`: query OPCODE * `{>opcode}`: query OPCODE
* `{/[LABEL]}`: any metadata label is accepted as a place holder if it is enclosed between `{/` and `}`.
the place holder will be replaced by the corresponding metadata value or the default value `-` if label is not defined.
The default Common Log Format is: The default Common Log Format is:

View file

@ -57,7 +57,7 @@ func (l Logger) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
// If we don't set up a class in config, the default "all" will be added // If we don't set up a class in config, the default "all" will be added
// and we shouldn't have an empty rule.Class. // and we shouldn't have an empty rule.Class.
if rule.Class[response.All] || rule.Class[class] { if rule.Class[response.All] || rule.Class[class] {
rep := replacer.New(r, rrw, CommonLogEmptyValue) rep := replacer.New(ctx, r, rrw, CommonLogEmptyValue)
clog.Infof(rep.Replace(rule.Format)) clog.Infof(rep.Replace(rule.Format))
} }

View file

@ -1,10 +1,12 @@
package replacer package replacer
import ( import (
"context"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
@ -21,6 +23,7 @@ type Replacer interface {
} }
type replacer struct { type replacer struct {
ctx context.Context
replacements map[string]string replacements map[string]string
emptyValue string emptyValue string
} }
@ -31,9 +34,10 @@ type replacer struct {
// values into the replacer. rr may be nil if it is not // values into the replacer. rr may be nil if it is not
// available. emptyValue should be the string that is used // available. emptyValue should be the string that is used
// in place of empty string (can still be empty string). // in place of empty string (can still be empty string).
func New(r *dns.Msg, rr *dnstest.Recorder, emptyValue string) Replacer { func New(ctx context.Context, r *dns.Msg, rr *dnstest.Recorder, emptyValue string) Replacer {
req := request.Request{W: rr, Req: r} req := request.Request{W: rr, Req: r}
rep := replacer{ rep := replacer{
ctx: ctx,
replacements: map[string]string{ replacements: map[string]string{
"{type}": req.Type(), "{type}": req.Type(),
"{name}": req.Name(), "{name}": req.Name(),
@ -43,6 +47,7 @@ func New(r *dns.Msg, rr *dnstest.Recorder, emptyValue string) Replacer {
"{size}": strconv.Itoa(req.Len()), "{size}": strconv.Itoa(req.Len()),
"{remote}": addrToRFC3986(req.IP()), "{remote}": addrToRFC3986(req.IP()),
"{port}": req.Port(), "{port}": req.Port(),
"{local}": addrToRFC3986(req.LocalIP()),
}, },
emptyValue: emptyValue, emptyValue: emptyValue,
} }
@ -71,23 +76,36 @@ func New(r *dns.Msg, rr *dnstest.Recorder, emptyValue string) Replacer {
// Replace performs a replacement of values on s and returns // Replace performs a replacement of values on s and returns
// the string with the replaced values. // the string with the replaced values.
func (r replacer) Replace(s string) string { func (r replacer) Replace(s string) string {
// Header replacements - these are case-insensitive, so we can't just use strings.Replace()
for strings.Contains(s, headerReplacer) { // declare a function that replace based on header matching
idxStart := strings.Index(s, headerReplacer) fscanAndReplace := func(s string, header string, replace func(string) string) string {
endOffset := idxStart + len(headerReplacer) b := strings.Builder{}
idxEnd := strings.Index(s[endOffset:], "}") for strings.Contains(s, header) {
if idxEnd > -1 { idxStart := strings.Index(s, header)
placeholder := strings.ToLower(s[idxStart : endOffset+idxEnd+1]) endOffset := idxStart + len(header)
replacement := r.replacements[placeholder] idxEnd := strings.Index(s[endOffset:], "}")
if replacement == "" { if idxEnd > -1 {
replacement = r.emptyValue placeholder := strings.ToLower(s[idxStart : endOffset+idxEnd+1])
replacement := replace(placeholder)
if replacement == "" {
replacement = r.emptyValue
}
b.WriteString(s[:idxStart])
b.WriteString(replacement)
s = s[endOffset+idxEnd+1:]
} else {
break
} }
s = s[:idxStart] + replacement + s[endOffset+idxEnd+1:]
} else {
break
} }
b.WriteString(s)
return b.String()
} }
// Header replacements - these are case-insensitive, so we can't just use strings.Replace()
s = fscanAndReplace(s, headerReplacer, func(placeholder string) string {
return r.replacements[placeholder]
})
// Regular replacements - these are easier because they're case-sensitive // Regular replacements - these are easier because they're case-sensitive
for placeholder, replacement := range r.replacements { for placeholder, replacement := range r.replacements {
if replacement == "" { if replacement == "" {
@ -96,6 +114,16 @@ func (r replacer) Replace(s string) string {
s = strings.Replace(s, placeholder, replacement, -1) s = strings.Replace(s, placeholder, replacement, -1)
} }
// Metadata label replacements
s = fscanAndReplace(s, headerLabelReplacer, func(placeholder string) string {
// label place holder has the format {/<label>}
fm := metadata.ValueFunc(r.ctx, placeholder[len(headerLabelReplacer):len(placeholder)-1])
if fm != nil {
return fm()
}
return ""
})
return s return s
} }
@ -161,4 +189,7 @@ func addrToRFC3986(addr string) string {
return addr return addr
} }
const headerReplacer = "{>" const (
headerReplacer = "{>"
headerLabelReplacer = "{/"
)

View file

@ -1,9 +1,13 @@
package replacer package replacer
import ( import (
"context"
"strings" "strings"
"testing" "testing"
"github.com/coredns/coredns/plugin/metadata"
"github.com/coredns/coredns/request"
"github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test" "github.com/coredns/coredns/plugin/test"
@ -17,7 +21,7 @@ func TestNewReplacer(t *testing.T) {
r.SetQuestion("example.org.", dns.TypeHINFO) r.SetQuestion("example.org.", dns.TypeHINFO)
r.MsgHdr.AuthenticatedData = true r.MsgHdr.AuthenticatedData = true
replaceValues := New(r, w, "") replaceValues := New(context.TODO(), r, w, "")
switch v := replaceValues.(type) { switch v := replaceValues.(type) {
case replacer: case replacer:
@ -47,7 +51,7 @@ func TestSet(t *testing.T) {
r.SetQuestion("example.org.", dns.TypeHINFO) r.SetQuestion("example.org.", dns.TypeHINFO)
r.MsgHdr.AuthenticatedData = true r.MsgHdr.AuthenticatedData = true
repl := New(r, w, "") repl := New(context.TODO(), r, w, "")
repl.Set("name", "coredns.io.") repl.Set("name", "coredns.io.")
repl.Set("type", "A") repl.Set("type", "A")
@ -63,3 +67,66 @@ func TestSet(t *testing.T) {
t.Error("Expected size replacement failed") t.Error("Expected size replacement failed")
} }
} }
type testProvider map[string]metadata.Func
func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context {
for k, v := range tp {
metadata.SetValueFunc(ctx, k, v)
}
return ctx
}
type testHandler struct{ ctx context.Context }
func (m *testHandler) Name() string { return "test" }
func (m *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m.ctx = ctx
return 0, nil
}
func TestMetadataReplacement(t *testing.T) {
mdata := testProvider{
"test/key2": func() string { return "two" },
}
tests := []struct {
expr string
result string
}{
{"{name}", "example.org."},
{"{/test/key2}", "two"},
{"TYPE={type}, NAME={name}, BUFSIZE={>bufsize}, WHAT={/test/key2} .. and more", "TYPE=HINFO, NAME=example.org., BUFSIZE=512, WHAT=two .. and more"},
{"{/test/key2}{/test/key4}", "two-"},
{"{test/key2", "{test/key2"}, // if last } is missing, the end of format is considered static text
{"{/test-key2}", "-"}, // everything that is not a placeholder for log or a metadata label is invalid
}
next := &testHandler{} // fake handler which stores the resulting context
m := metadata.Metadata{
Zones: []string{"."},
Providers: []metadata.Provider{mdata},
Next: next,
}
ctx := context.TODO()
m.ServeDNS(ctx, &test.ResponseWriter{}, new(dns.Msg))
nctx := next.ctx
w := dnstest.NewRecorder(&test.ResponseWriter{})
r := new(dns.Msg)
r.SetQuestion("example.org.", dns.TypeHINFO)
r.MsgHdr.AuthenticatedData = true
repl := New(nctx, r, w, "-")
for i, ts := range tests {
r := repl.Replace(ts.expr)
if r != ts.result {
t.Errorf("Test %d - expr : %s, expected replacement being %s, and got %s", i, ts.expr, ts.result, r)
}
}
}

View file

@ -1,6 +1,7 @@
package rewrite package rewrite
import ( import (
"context"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@ -22,7 +23,7 @@ const (
NotMatch = "not_match" NotMatch = "not_match"
) )
func newReplacer(r *dns.Msg) replacer.Replacer { return replacer.New(r, nil, "") } func newReplacer(r *dns.Msg) replacer.Replacer { return replacer.New(context.TODO(), r, nil, "") }
// condition is a rewrite condition. // condition is a rewrite condition.
type condition func(string, string) bool type condition func(string, string) bool