replacer: evaluate format once and improve perf by ~3x (#3002)
* replacer: evaluate format once and improve perf by ~3x This improves the performance of logging by almost 3x and reduces memory usage by ~8x. Benchmark results: benchmark old ns/op new ns/op delta BenchmarkReplacer-12 644 324 -49.69% BenchmarkReplacer_CommonLogFormat-12 4228 1471 -65.21% benchmark old allocs new allocs delta BenchmarkReplacer-12 8 2 -75.00% BenchmarkReplacer_CommonLogFormat-12 51 17 -66.67% benchmark old bytes new bytes delta BenchmarkReplacer-12 240 48 -80.00% BenchmarkReplacer_CommonLogFormat-12 3723 446 -88.02% * replacer: code review comments * bufPool: document why we use a pointer to a slice * parseFormat: fix confusing comment * TestParseFormat_Nodes: rename to TestParseFormatNodes * replacer: use a value for bufPool instead of a pointer * replacer: remove comment * replacer: replace labels with knownLabels The previous slice of label names is no longer needed.
This commit is contained in:
parent
21e9c6047b
commit
a2af651ecb
2 changed files with 453 additions and 180 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coredns/coredns/plugin/metadata"
|
"github.com/coredns/coredns/plugin/metadata"
|
||||||
|
@ -14,195 +15,262 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Replacer replaces labels for values in strings.
|
// Replacer replaces labels for values in strings.
|
||||||
type Replacer struct {
|
type Replacer struct{}
|
||||||
valueFunc func(request.Request, *dnstest.Recorder, string) string
|
|
||||||
labels []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// labels are all supported labels that can be used in the default Replacer.
|
// New makes a new replacer. This only needs to be called once in the setup and
|
||||||
var labels = []string{
|
// then call Replace for each incoming message. A replacer is safe for concurrent use.
|
||||||
"{type}",
|
|
||||||
"{name}",
|
|
||||||
"{class}",
|
|
||||||
"{proto}",
|
|
||||||
"{size}",
|
|
||||||
"{remote}",
|
|
||||||
"{port}",
|
|
||||||
"{local}",
|
|
||||||
// Header values.
|
|
||||||
headerReplacer + "id}",
|
|
||||||
headerReplacer + "opcode}",
|
|
||||||
headerReplacer + "do}",
|
|
||||||
headerReplacer + "bufsize}",
|
|
||||||
// Recorded replacements.
|
|
||||||
"{rcode}",
|
|
||||||
"{rsize}",
|
|
||||||
"{duration}",
|
|
||||||
headerReplacer + "rflags}",
|
|
||||||
}
|
|
||||||
|
|
||||||
// value returns the current value of label.
|
|
||||||
func value(state request.Request, rr *dnstest.Recorder, label string) string {
|
|
||||||
switch label {
|
|
||||||
case "{type}":
|
|
||||||
return state.Type()
|
|
||||||
case "{name}":
|
|
||||||
return state.Name()
|
|
||||||
case "{class}":
|
|
||||||
return state.Class()
|
|
||||||
case "{proto}":
|
|
||||||
return state.Proto()
|
|
||||||
case "{size}":
|
|
||||||
return strconv.Itoa(state.Req.Len())
|
|
||||||
case "{remote}":
|
|
||||||
return addrToRFC3986(state.IP())
|
|
||||||
case "{port}":
|
|
||||||
return state.Port()
|
|
||||||
case "{local}":
|
|
||||||
return addrToRFC3986(state.LocalIP())
|
|
||||||
// Header placeholders (case-insensitive).
|
|
||||||
case headerReplacer + "id}":
|
|
||||||
return strconv.Itoa(int(state.Req.Id))
|
|
||||||
case headerReplacer + "opcode}":
|
|
||||||
return strconv.Itoa(state.Req.Opcode)
|
|
||||||
case headerReplacer + "do}":
|
|
||||||
return boolToString(state.Do())
|
|
||||||
case headerReplacer + "bufsize}":
|
|
||||||
return strconv.Itoa(state.Size())
|
|
||||||
// Recorded replacements.
|
|
||||||
case "{rcode}":
|
|
||||||
if rr == nil {
|
|
||||||
return EmptyValue
|
|
||||||
}
|
|
||||||
rcode := dns.RcodeToString[rr.Rcode]
|
|
||||||
if rcode == "" {
|
|
||||||
rcode = strconv.Itoa(rr.Rcode)
|
|
||||||
}
|
|
||||||
return rcode
|
|
||||||
case "{rsize}":
|
|
||||||
if rr == nil {
|
|
||||||
return EmptyValue
|
|
||||||
}
|
|
||||||
return strconv.Itoa(rr.Len)
|
|
||||||
case "{duration}":
|
|
||||||
if rr == nil {
|
|
||||||
return EmptyValue
|
|
||||||
}
|
|
||||||
return strconv.FormatFloat(time.Since(rr.Start).Seconds(), 'f', -1, 64) + "s"
|
|
||||||
case headerReplacer + "rflags}":
|
|
||||||
if rr != nil && rr.Msg != nil {
|
|
||||||
return flagsToString(rr.Msg.MsgHdr)
|
|
||||||
}
|
|
||||||
return EmptyValue
|
|
||||||
}
|
|
||||||
return EmptyValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// New makes a new replacer. This only needs to be called once in the setup and then call Replace for each incoming message.
|
|
||||||
// A replacer is safe for concurrent use.
|
|
||||||
func New() Replacer {
|
func New() Replacer {
|
||||||
return Replacer{
|
return Replacer{}
|
||||||
valueFunc: value,
|
|
||||||
labels: labels,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace performs a replacement of values on s and returns the string with the replaced values.
|
// Replace performs a replacement of values on s and returns the string with the replaced values.
|
||||||
func (r Replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder, s string) string {
|
func (r Replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder, s string) string {
|
||||||
for _, placeholder := range r.labels {
|
return loadFormat(s).Replace(ctx, state, rr)
|
||||||
if strings.Contains(s, placeholder) {
|
|
||||||
s = strings.Replace(s, placeholder, r.valueFunc(state, rr, placeholder), -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata label replacements. Scan for {/ and search for next }, replace that metadata label with
|
|
||||||
// any meta data that is available.
|
|
||||||
b := strings.Builder{}
|
|
||||||
for strings.Contains(s, labelReplacer) {
|
|
||||||
idxStart := strings.Index(s, labelReplacer)
|
|
||||||
endOffset := idxStart + len(labelReplacer)
|
|
||||||
idxEnd := strings.Index(s[endOffset:], "}")
|
|
||||||
if idxEnd > -1 {
|
|
||||||
label := s[idxStart+2 : endOffset+idxEnd]
|
|
||||||
|
|
||||||
fm := metadata.ValueFunc(ctx, label)
|
|
||||||
replacement := EmptyValue
|
|
||||||
if fm != nil {
|
|
||||||
replacement = fm()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString(s[:idxStart])
|
|
||||||
b.WriteString(replacement)
|
|
||||||
s = s[endOffset+idxEnd+1:]
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString(s)
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolToString(b bool) string {
|
|
||||||
if b {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
// flagsToString checks all header flags and returns those
|
|
||||||
// that are set as a string separated with commas
|
|
||||||
func flagsToString(h dns.MsgHdr) string {
|
|
||||||
flags := make([]string, 7)
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
if h.Response {
|
|
||||||
flags[i] = "qr"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.Authoritative {
|
|
||||||
flags[i] = "aa"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if h.Truncated {
|
|
||||||
flags[i] = "tc"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if h.RecursionDesired {
|
|
||||||
flags[i] = "rd"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if h.RecursionAvailable {
|
|
||||||
flags[i] = "ra"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if h.Zero {
|
|
||||||
flags[i] = "z"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if h.AuthenticatedData {
|
|
||||||
flags[i] = "ad"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if h.CheckingDisabled {
|
|
||||||
flags[i] = "cd"
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return strings.Join(flags[:i], ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// addrToRFC3986 will add brackets to the address if it is an IPv6 address.
|
|
||||||
func addrToRFC3986(addr string) string {
|
|
||||||
if strings.Contains(addr, ":") {
|
|
||||||
return "[" + addr + "]"
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
headerReplacer = "{>"
|
headerReplacer = "{>"
|
||||||
labelReplacer = "{/"
|
|
||||||
// EmptyValue is the default empty value.
|
// EmptyValue is the default empty value.
|
||||||
EmptyValue = "-"
|
EmptyValue = "-"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// labels are all supported labels that can be used in the default Replacer.
|
||||||
|
var labels = map[string]struct{}{
|
||||||
|
"{type}": {},
|
||||||
|
"{name}": {},
|
||||||
|
"{class}": {},
|
||||||
|
"{proto}": {},
|
||||||
|
"{size}": {},
|
||||||
|
"{remote}": {},
|
||||||
|
"{port}": {},
|
||||||
|
"{local}": {},
|
||||||
|
// Header values.
|
||||||
|
headerReplacer + "id}": {},
|
||||||
|
headerReplacer + "opcode}": {},
|
||||||
|
headerReplacer + "do}": {},
|
||||||
|
headerReplacer + "bufsize}": {},
|
||||||
|
// Recorded replacements.
|
||||||
|
"{rcode}": {},
|
||||||
|
"{rsize}": {},
|
||||||
|
"{duration}": {},
|
||||||
|
headerReplacer + "rflags}": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendValue appends the current value of label.
|
||||||
|
func appendValue(b []byte, state request.Request, rr *dnstest.Recorder, label string) []byte {
|
||||||
|
switch label {
|
||||||
|
case "{type}":
|
||||||
|
return append(b, state.Type()...)
|
||||||
|
case "{name}":
|
||||||
|
return append(b, state.Name()...)
|
||||||
|
case "{class}":
|
||||||
|
return append(b, state.Class()...)
|
||||||
|
case "{proto}":
|
||||||
|
return append(b, state.Proto()...)
|
||||||
|
case "{size}":
|
||||||
|
return strconv.AppendInt(b, int64(state.Req.Len()), 10)
|
||||||
|
case "{remote}":
|
||||||
|
return appendAddrToRFC3986(b, state.IP())
|
||||||
|
case "{port}":
|
||||||
|
return append(b, state.Port()...)
|
||||||
|
case "{local}":
|
||||||
|
return appendAddrToRFC3986(b, state.LocalIP())
|
||||||
|
// Header placeholders (case-insensitive).
|
||||||
|
case headerReplacer + "id}":
|
||||||
|
return strconv.AppendInt(b, int64(state.Req.Id), 10)
|
||||||
|
case headerReplacer + "opcode}":
|
||||||
|
return strconv.AppendInt(b, int64(state.Req.Opcode), 10)
|
||||||
|
case headerReplacer + "do}":
|
||||||
|
return strconv.AppendBool(b, state.Do())
|
||||||
|
case headerReplacer + "bufsize}":
|
||||||
|
return strconv.AppendInt(b, int64(state.Size()), 10)
|
||||||
|
// Recorded replacements.
|
||||||
|
case "{rcode}":
|
||||||
|
if rr == nil {
|
||||||
|
return append(b, EmptyValue...)
|
||||||
|
}
|
||||||
|
if rcode := dns.RcodeToString[rr.Rcode]; rcode != "" {
|
||||||
|
return append(b, rcode...)
|
||||||
|
}
|
||||||
|
return strconv.AppendInt(b, int64(rr.Rcode), 10)
|
||||||
|
case "{rsize}":
|
||||||
|
if rr == nil {
|
||||||
|
return append(b, EmptyValue...)
|
||||||
|
}
|
||||||
|
return strconv.AppendInt(b, int64(rr.Len), 10)
|
||||||
|
case "{duration}":
|
||||||
|
if rr == nil {
|
||||||
|
return append(b, EmptyValue...)
|
||||||
|
}
|
||||||
|
secs := time.Since(rr.Start).Seconds()
|
||||||
|
return append(strconv.AppendFloat(b, secs, 'f', -1, 64), 's')
|
||||||
|
case headerReplacer + "rflags}":
|
||||||
|
if rr != nil && rr.Msg != nil {
|
||||||
|
return appendFlags(b, rr.Msg.MsgHdr)
|
||||||
|
}
|
||||||
|
return append(b, EmptyValue...)
|
||||||
|
default:
|
||||||
|
return append(b, EmptyValue...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendFlags checks all header flags and appends those
|
||||||
|
// that are set as a string separated with commas
|
||||||
|
func appendFlags(b []byte, h dns.MsgHdr) []byte {
|
||||||
|
origLen := len(b)
|
||||||
|
if h.Response {
|
||||||
|
b = append(b, "qr,"...)
|
||||||
|
}
|
||||||
|
if h.Authoritative {
|
||||||
|
b = append(b, "aa,"...)
|
||||||
|
}
|
||||||
|
if h.Truncated {
|
||||||
|
b = append(b, "tc,"...)
|
||||||
|
}
|
||||||
|
if h.RecursionDesired {
|
||||||
|
b = append(b, "rd,"...)
|
||||||
|
}
|
||||||
|
if h.RecursionAvailable {
|
||||||
|
b = append(b, "ra,"...)
|
||||||
|
}
|
||||||
|
if h.Zero {
|
||||||
|
b = append(b, "z,"...)
|
||||||
|
}
|
||||||
|
if h.AuthenticatedData {
|
||||||
|
b = append(b, "ad,"...)
|
||||||
|
}
|
||||||
|
if h.CheckingDisabled {
|
||||||
|
b = append(b, "cd,"...)
|
||||||
|
}
|
||||||
|
if n := len(b); n > origLen {
|
||||||
|
return b[:n-1] // trim trailing ','
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendAddrToRFC3986 will add brackets to the address if it is an IPv6 address.
|
||||||
|
func appendAddrToRFC3986(b []byte, addr string) []byte {
|
||||||
|
if strings.IndexByte(addr, ':') != -1 {
|
||||||
|
b = append(b, '[')
|
||||||
|
b = append(b, addr...)
|
||||||
|
b = append(b, ']')
|
||||||
|
} else {
|
||||||
|
b = append(b, addr...)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
typeLabel nodeType = iota // "{type}"
|
||||||
|
typeLiteral // "foo"
|
||||||
|
typeMetadata // "{/metadata}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A node represents a segment of a parsed format. For example: "A {type}"
|
||||||
|
// contains two nodes: "A " (literal); and "{type}" (label).
|
||||||
|
type node struct {
|
||||||
|
value string // Literal value, label or metadata label
|
||||||
|
typ nodeType
|
||||||
|
}
|
||||||
|
|
||||||
|
// A replacer is an ordered list of all the nodes in a format.
|
||||||
|
type replacer []node
|
||||||
|
|
||||||
|
func parseFormat(s string) replacer {
|
||||||
|
// Assume there is a literal between each label - its cheaper to over
|
||||||
|
// allocate once than allocate twice.
|
||||||
|
rep := make(replacer, 0, strings.Count(s, "{")*2)
|
||||||
|
for {
|
||||||
|
// We find the right bracket then backtrack to find the left bracket.
|
||||||
|
// This allows us to handle formats like: "{ {foo} }".
|
||||||
|
j := strings.IndexByte(s, '}')
|
||||||
|
if j < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i := strings.LastIndexByte(s[:j], '{')
|
||||||
|
if i < 0 {
|
||||||
|
// Handle: "A } {foo}" by treating "A }" as a literal
|
||||||
|
rep = append(rep, node{
|
||||||
|
value: s[:j+1],
|
||||||
|
typ: typeLiteral,
|
||||||
|
})
|
||||||
|
s = s[j+1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val := s[i : j+1]
|
||||||
|
var typ nodeType
|
||||||
|
switch _, ok := labels[val]; {
|
||||||
|
case ok:
|
||||||
|
typ = typeLabel
|
||||||
|
case strings.HasPrefix(val, "{/"):
|
||||||
|
// Strip "{/}" from metadata labels
|
||||||
|
val = val[2 : len(val)-1]
|
||||||
|
typ = typeMetadata
|
||||||
|
default:
|
||||||
|
// Given: "A {X}" val is "{X}" expand it to the whole literal.
|
||||||
|
val = s[:j+1]
|
||||||
|
typ = typeLiteral
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any leading literal. Given "A {type}" the literal is "A "
|
||||||
|
if i != 0 && typ != typeLiteral {
|
||||||
|
rep = append(rep, node{
|
||||||
|
value: s[:i],
|
||||||
|
typ: typeLiteral,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rep = append(rep, node{
|
||||||
|
value: val,
|
||||||
|
typ: typ,
|
||||||
|
})
|
||||||
|
s = s[j+1:]
|
||||||
|
}
|
||||||
|
if len(s) != 0 {
|
||||||
|
rep = append(rep, node{
|
||||||
|
value: s,
|
||||||
|
typ: typeLiteral,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rep
|
||||||
|
}
|
||||||
|
|
||||||
|
var replacerCache sync.Map // map[string]replacer
|
||||||
|
|
||||||
|
func loadFormat(s string) replacer {
|
||||||
|
if v, ok := replacerCache.Load(s); ok {
|
||||||
|
return v.(replacer)
|
||||||
|
}
|
||||||
|
v, _ := replacerCache.LoadOrStore(s, parseFormat(s))
|
||||||
|
return v.(replacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bufPool stores pointers to scratch buffers.
|
||||||
|
var bufPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return make([]byte, 0, 256)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder) string {
|
||||||
|
b := bufPool.Get().([]byte)
|
||||||
|
for _, s := range r {
|
||||||
|
switch s.typ {
|
||||||
|
case typeLabel:
|
||||||
|
b = appendValue(b, state, rr, s.value)
|
||||||
|
case typeLiteral:
|
||||||
|
b = append(b, s.value...)
|
||||||
|
case typeMetadata:
|
||||||
|
if fm := metadata.ValueFunc(ctx, s.value); fm != nil {
|
||||||
|
b = append(b, fm()...)
|
||||||
|
} else {
|
||||||
|
b = append(b, EmptyValue...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
bufPool.Put(b[:0])
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package replacer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coredns/coredns/plugin/metadata"
|
"github.com/coredns/coredns/plugin/metadata"
|
||||||
|
@ -12,6 +14,9 @@ import (
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// This is the default format used by the log package
|
||||||
|
const CommonLogFormat = `{remote}:{port} - {>id} "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}`
|
||||||
|
|
||||||
func TestReplacer(t *testing.T) {
|
func TestReplacer(t *testing.T) {
|
||||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
|
@ -32,6 +37,178 @@ func TestReplacer(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFormat(t *testing.T) {
|
||||||
|
type formatTest struct {
|
||||||
|
Format string
|
||||||
|
Expected replacer
|
||||||
|
}
|
||||||
|
tests := []formatTest{
|
||||||
|
{
|
||||||
|
Format: "",
|
||||||
|
Expected: replacer{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "A",
|
||||||
|
Expected: replacer{
|
||||||
|
{"A", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "A {A}",
|
||||||
|
Expected: replacer{
|
||||||
|
{"A {A}", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "{{remote}}",
|
||||||
|
Expected: replacer{
|
||||||
|
{"{", typeLiteral},
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
{"}", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "{ A {remote} A }",
|
||||||
|
Expected: replacer{
|
||||||
|
{"{ A ", typeLiteral},
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
{" A }", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "{remote}}",
|
||||||
|
Expected: replacer{
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
{"}", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "{{remote}",
|
||||||
|
Expected: replacer{
|
||||||
|
{"{", typeLiteral},
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: `Foo } {remote}`,
|
||||||
|
Expected: replacer{
|
||||||
|
// we don't do any optimizations to join adjacent literals
|
||||||
|
{"Foo }", typeLiteral},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: `{ Foo`,
|
||||||
|
Expected: replacer{
|
||||||
|
{"{ Foo", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: `} Foo`,
|
||||||
|
Expected: replacer{
|
||||||
|
{"}", typeLiteral},
|
||||||
|
{" Foo", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: "A { {remote} {type} {/meta1} } B",
|
||||||
|
Expected: replacer{
|
||||||
|
{"A { ", typeLiteral},
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{type}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"meta1", typeMetadata},
|
||||||
|
{" }", typeLiteral},
|
||||||
|
{" B", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Format: `LOG {remote}:{port} - {>id} "{type} {class} {name} {proto} ` +
|
||||||
|
`{size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {/meta1}-{/meta2} ` +
|
||||||
|
`{duration} END OF LINE`,
|
||||||
|
Expected: replacer{
|
||||||
|
{"LOG ", typeLiteral},
|
||||||
|
{"{remote}", typeLabel},
|
||||||
|
{":", typeLiteral},
|
||||||
|
{"{port}", typeLabel},
|
||||||
|
{" - ", typeLiteral},
|
||||||
|
{"{>id}", typeLabel},
|
||||||
|
{` "`, typeLiteral},
|
||||||
|
{"{type}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{class}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{name}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{proto}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{size}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{>do}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{>bufsize}", typeLabel},
|
||||||
|
{`" `, typeLiteral},
|
||||||
|
{"{rcode}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{>rflags}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{rsize}", typeLabel},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"meta1", typeMetadata},
|
||||||
|
{"-", typeLiteral},
|
||||||
|
{"meta2", typeMetadata},
|
||||||
|
{" ", typeLiteral},
|
||||||
|
{"{duration}", typeLabel},
|
||||||
|
{" END OF LINE", typeLiteral},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, x := range tests {
|
||||||
|
r := parseFormat(x.Format)
|
||||||
|
if !reflect.DeepEqual(r, x.Expected) {
|
||||||
|
t.Errorf("%d: Expected:\n\t%+v\nGot:\n\t%+v", i, x.Expected, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFormatNodes(t *testing.T) {
|
||||||
|
// If we parse the format successfully the result of joining all the
|
||||||
|
// segments should match the original format.
|
||||||
|
formats := []string{
|
||||||
|
"",
|
||||||
|
"msg",
|
||||||
|
"{remote}",
|
||||||
|
"{remote}",
|
||||||
|
"{{remote}",
|
||||||
|
"{{remote}}",
|
||||||
|
"{{remote}} A",
|
||||||
|
CommonLogFormat,
|
||||||
|
CommonLogFormat + " FOO} {BAR}",
|
||||||
|
"A " + CommonLogFormat + " FOO} {BAR}",
|
||||||
|
"A " + CommonLogFormat + " {/meta}",
|
||||||
|
}
|
||||||
|
join := func(r replacer) string {
|
||||||
|
a := make([]string, len(r))
|
||||||
|
for i, n := range r {
|
||||||
|
if n.typ == typeMetadata {
|
||||||
|
a[i] = "{/" + n.value + "}"
|
||||||
|
} else {
|
||||||
|
a[i] = n.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(a, "")
|
||||||
|
}
|
||||||
|
for _, format := range formats {
|
||||||
|
r := parseFormat(format)
|
||||||
|
s := join(r)
|
||||||
|
if s != format {
|
||||||
|
t.Errorf("Expected format to be: '%s' got: '%s'", format, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLabels(t *testing.T) {
|
func TestLabels(t *testing.T) {
|
||||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
|
@ -68,7 +245,7 @@ func TestLabels(t *testing.T) {
|
||||||
t.Fatalf("Expect %d labels, got %d", len(expect), len(labels))
|
t.Fatalf("Expect %d labels, got %d", len(expect), len(labels))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, lbl := range labels {
|
for lbl := range labels {
|
||||||
repl := replacer.Replace(ctx, state, w, lbl)
|
repl := replacer.Replace(ctx, state, w, lbl)
|
||||||
if lbl == "{duration}" {
|
if lbl == "{duration}" {
|
||||||
if repl[len(repl)-1] != 's' {
|
if repl[len(repl)-1] != 's' {
|
||||||
|
@ -98,6 +275,34 @@ func BenchmarkReplacer(b *testing.B) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkReplacer_CommonLogFormat(b *testing.B) {
|
||||||
|
|
||||||
|
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion("example.org.", dns.TypeHINFO)
|
||||||
|
r.Id = 1053
|
||||||
|
r.AuthenticatedData = true
|
||||||
|
r.CheckingDisabled = true
|
||||||
|
r.MsgHdr.AuthenticatedData = true
|
||||||
|
w.WriteMsg(r)
|
||||||
|
state := request.Request{W: w, Req: r}
|
||||||
|
|
||||||
|
replacer := New()
|
||||||
|
ctxt := context.TODO()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
replacer.Replace(ctxt, state, w, CommonLogFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkParseFormat(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
parseFormat(CommonLogFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type testProvider map[string]metadata.Func
|
type testProvider map[string]metadata.Func
|
||||||
|
|
||||||
func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context {
|
func (tp testProvider) Metadata(ctx context.Context, state request.Request) context.Context {
|
||||||
|
|
Loading…
Add table
Reference in a new issue