plugin/log - Support for Metadata (#2251)
* - add metadata support to Log * - adapt ctx after rebase
This commit is contained in:
parent
35c5474660
commit
94c9aae323
5 changed files with 123 additions and 19 deletions
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 = "{/"
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue