Skip to content

Commit

Permalink
Added statistics collection and aggregation
Browse files Browse the repository at this point in the history
  • Loading branch information
0xERR0R committed Feb 7, 2020
1 parent 352986f commit 5b2a78b
Show file tree
Hide file tree
Showing 16 changed files with 580 additions and 36 deletions.
7 changes: 5 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,8 @@ services:
Download binary file for your architecture, put it in one directory with config file. Please be aware, you must run the binary with root privileges if you want to use port 53 or 953.

### Additional information
To print runtime configuration and statistics, you can send SIGUSR1 signal to running process:
`kill -s USR1 <PID>` or `docker kill -s SIGUSR1 blocky` for docker setup
To print runtime configuration / statistics, you can send signal to running process:
* `SIGUSR1` will print current configuration
* `SIGUSR2` prints 24h statistics

Hint: `kill -s USR1 <PID>` or `docker kill -s SIGUSR1 blocky` for docker setup
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ module blocky
go 1.13

require (
github.com/go-openapi/strfmt v0.19.4 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-runewidth v0.0.8 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/dns v1.1.22
github.com/onsi/ginkgo v1.11.0 // indirect
Expand Down
25 changes: 25 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/strfmt v0.19.4 h1:eRvaqAhpL0IL6Trh5fDsGnGhiXndzHFuA05w6sXH6/g=
github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
Expand All @@ -20,10 +34,14 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand All @@ -38,11 +56,18 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
Expand Down
4 changes: 2 additions & 2 deletions resolver/blocking_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (r *BlockingResolver) Resolve(request *Request) (*Response, error) {
response.SetReply(request.Req)
resp, err := r.handleBlocked(question, response)

return &Response{Res: resp, Reason: fmt.Sprintf("BLOCKED (WHITELIST ONLY)")}, err
return &Response{Res: resp, rType: BLOCKED, Reason: fmt.Sprintf("BLOCKED (WHITELIST ONLY)")}, err
}
if blocked, group := r.matches(groupsToCheck, r.blacklistMatcher, domain); blocked {
logger.WithField("group", group).Debug("domain is blocked")
Expand All @@ -163,7 +163,7 @@ func (r *BlockingResolver) Resolve(request *Request) (*Response, error) {
response.SetReply(request.Req)
resp, err := r.handleBlocked(question, response)

return &Response{Res: resp, Reason: fmt.Sprintf("BLOCKED (%s)", group)}, err
return &Response{Res: resp, rType: BLOCKED, Reason: fmt.Sprintf("BLOCKED (%s)", group)}, err
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions resolver/caching_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ func (r *CachingResolver) Resolve(request *Request) (response *Response, err err
rr.Header().Ttl = remainingTTL
}

return &Response{Res: resp, Reason: fmt.Sprintf("CACHED (ttl %d)", remainingTTL)}, nil
return &Response{Res: resp, rType: CACHED, Reason: "CACHED"}, nil
}
// Answer with response code != OK
resp.Rcode = val.(int)

return &Response{Res: resp, Reason: fmt.Sprintf("CACHED NEGATIVE (ttl %d)", remainingTTL)}, nil
return &Response{Res: resp, rType: CACHED, Reason: "CACHED NEGATIVE"}, nil
}

logger.WithField("next_resolver", r.next).Debug("not in cache: go to next resolver")
Expand Down
3 changes: 1 addition & 2 deletions resolver/caching_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ func Test_Resolve_A_NegativeCache(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, dns.RcodeNameError, resp.Res.Rcode)

// ttl is smaler
assert.Equal(t, "CACHED NEGATIVE (ttl 1799)", resp.Reason)
assert.Equal(t, "CACHED NEGATIVE", resp.Reason)

// still one call to resolver
assert.Len(t, m.Calls, 1)
Expand Down
1 change: 1 addition & 0 deletions resolver/conditionall_upstream_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func (r *ConditionalUpstreamResolver) Resolve(request *Request) (*Response, erro
response, err := r.Resolve(request)
if err == nil {
response.Reason = "CONDITIONAL"
response.rType = CONDITIONAL
}

logger.WithFields(logrus.Fields{
Expand Down
4 changes: 2 additions & 2 deletions resolver/custom_dns_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ func (r *CustomDNSResolver) Resolve(request *Request) (*Response, error) {
"domain": domain,
}).Debugf("returning custom dns entry")

return &Response{Res: response, Reason: "CUSTOM DNS"}, nil
return &Response{Res: response, rType: CUSTOMDNS, Reason: "CUSTOM DNS"}, nil
}

return nil, err
}

response.Rcode = dns.RcodeNameError

return &Response{Res: response, Reason: "CUSTOM DNS"}, nil
return &Response{Res: response, rType: CUSTOMDNS, Reason: "CUSTOM DNS"}, nil
}

if i := strings.Index(domain, "."); i >= 0 {
Expand Down
26 changes: 23 additions & 3 deletions resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,24 @@ type Request struct {
Log *logrus.Entry
}

type ResponseType uint8
type ResponseType int

const (
Resolved ResponseType = iota
Blocked
RESOLVED ResponseType = iota
CACHED
BLOCKED
CONDITIONAL
CUSTOMDNS
)

func (d ResponseType) String() string {
return [...]string{"RESOLVED", "CACHED", "BLOCKED", "CONDITIONAL", "CUSTOM DNS"}[d]
}

type Response struct {
Res *dns.Msg
Reason string
rType ResponseType
}
type Resolver interface {
Resolve(req *Request) (*Response, error)
Expand Down Expand Up @@ -55,3 +63,15 @@ func logger(prefix string) *logrus.Entry {
func withPrefix(logger *logrus.Entry, prefix string) *logrus.Entry {
return logger.WithField("prefix", prefix)
}

func Chain(resolvers ...Resolver) Resolver {
for i, res := range resolvers {
if i+1 < len(resolvers) {
if cr, ok := res.(ChainedResolver); ok {
cr.Next(resolvers[i+1])
}
}
}

return resolvers[0]
}
152 changes: 152 additions & 0 deletions resolver/stats_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package resolver

import (
"blocky/stats"
"blocky/util"
"fmt"
"os"
"os/signal"
"strings"
"syscall"

"github.com/jedib0t/go-pretty/table"
"github.com/miekg/dns"
)

type StatsResolver struct {
NextResolver
recorders []*resolverStatRecorder
statsChan chan *statsEntry
}

type statsEntry struct {
request *Request
response *Response
}

type resolverStatRecorder struct {
aggregator *stats.Aggregator
fn func(*statsEntry) string
}

func newRecorder(name string, fn func(*statsEntry) string) *resolverStatRecorder {
return &resolverStatRecorder{
aggregator: stats.NewAggregator(name),
fn: fn,
}
}

func newRecorderWithMax(name string, max uint, fn func(*statsEntry) string) *resolverStatRecorder {
return &resolverStatRecorder{
aggregator: stats.NewAggregatorWithMax(name, max),
fn: fn,
}
}

func (r *StatsResolver) collectStats() {
for statsEntry := range r.statsChan {
for _, rec := range r.recorders {
rec.recordStats(statsEntry)
}
}
}

func (r *StatsResolver) Resolve(request *Request) (*Response, error) {
resp, err := r.next.Resolve(request)

if err == nil {
r.statsChan <- &statsEntry{
request: request,
response: resp,
}
}

return resp, err
}

func (r *StatsResolver) Configuration() (result []string) {
result = append(result, "stats:")
for _, rec := range r.recorders {
result = append(result, fmt.Sprintf(" - %s", rec.aggregator.Name))
}

return
}

func (r StatsResolver) String() string {
return fmt.Sprintf("statistic resolver")
}

func (r *resolverStatRecorder) recordStats(e *statsEntry) {
r.aggregator.Put(r.fn(e))
}

func NewStatsResolver() ChainedResolver {
resolver := &StatsResolver{
statsChan: make(chan *statsEntry, 20),
recorders: createRecorders(),
}

go resolver.collectStats()

signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGUSR2)

go func() {
for {
<-signals
resolver.printStats()
}
}()

return resolver
}

func (r *StatsResolver) printStats() {
logger := logger("stats_resover")

w := logger.Writer()
defer w.Close()

logger.Info("******* STATS 24h *******")

for _, s := range r.recorders {
t := table.NewWriter()
t.SetOutputMirror(w)
t.SetTitle(s.aggregator.Name)

t.SetStyle(table.StyleLight)

util.IterateValueSorted(s.aggregator.AggregateResult(), func(k string, v int) {
t.AppendRow([]interface{}{fmt.Sprintf("%50s", k), v})
})

t.Render()
}
}

func createRecorders() []*resolverStatRecorder {
return []*resolverStatRecorder{
newRecorderWithMax("Top 20 queries", 20, func(e *statsEntry) string {
return util.ExtractDomain(e.request.Req.Question[0])
}),
newRecorderWithMax("Top 20 blocked queries", 20, func(e *statsEntry) string {
if e.response.rType == BLOCKED {
return util.ExtractDomain(e.request.Req.Question[0])
}
return ""
}),
newRecorder("Query count per client", func(e *statsEntry) string {
return strings.Join(e.request.ClientNames, ",")
}),
newRecorder("Reason", func(e *statsEntry) string {
return e.response.Reason
}),
newRecorder("Query type", func(e *statsEntry) string {
return util.QTypeToString()(e.request.Req.Question[0].Qtype)
}),
newRecorder("Response type", func(e *statsEntry) string {
return dns.RcodeToString[e.response.Res.Rcode]
}),
}
}
39 changes: 39 additions & 0 deletions resolver/stats_resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package resolver

import (
"blocky/util"
"testing"

"github.com/miekg/dns"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func Test_Resolve_WithStats(t *testing.T) {
sut := NewStatsResolver()
m := &resolverMock{}

resp, err := util.NewMsgWithAnswer("example.com. 300 IN A 123.122.121.120")
assert.NoError(t, err)

m.On("Resolve", mock.Anything).Return(&Response{Res: resp, Reason: "reason"}, nil)
sut.Next(m)

request := &Request{
Req: util.NewMsgWithQuestion("example.com.", dns.TypeA),
Log: logrus.NewEntry(logrus.New()),
}

_, err = sut.Resolve(request)
assert.NoError(t, err)
m.AssertExpectations(t)

sut.(*StatsResolver).printStats()
}

func Test_Configuration_StatsResolverg(t *testing.T) {
sut := NewStatsResolver()
c := sut.Configuration()
assert.True(t, len(c) > 1)
}
Loading

0 comments on commit 5b2a78b

Please sign in to comment.