diff options
-rw-r--r-- | README.md | 23 | ||||
-rw-r--r-- | http/http.go | 56 | ||||
-rw-r--r-- | http/http_test.go | 8 | ||||
-rw-r--r-- | sql/logger.go | 35 | ||||
-rw-r--r-- | sql/sql.go | 37 | ||||
-rw-r--r-- | sql/sql_test.go | 26 |
6 files changed, 175 insertions, 10 deletions
@@ -121,6 +121,29 @@ $ curl -s -XDELETE 'http://127.0.0.1:8053/cache/v1/' | jq . } ``` +Metrics: + +``` shell +$ curl 'http://127.0.0.1:8053/metric/v1/' | jq . +{ + "summary": { + "since": "2020-01-05T00:58:49Z", + "total": 2307, + "hijacked": 867 + }, + "requests": [ + { + "time": "2020-01-05T00:58:49Z", + "count": 1 + } + ] +} +``` + +Note that `log_mode = "hijacked"` or `log_mode = "all"` is required to make +metrics available. Choosing `hijacked` will only produce metrics for hijacked +requests. + ## Port redirection Most operating systems expect to find their DNS resolver on UDP port 53. diff --git a/http/http.go b/http/http.go index 053435f..3bf047c 100644 --- a/http/http.go +++ b/http/http.go @@ -33,12 +33,35 @@ type entry struct { Rcode string `json:"rcode,omitempty"` } +type summary struct { + Since string `json:"since"` + Total int64 `json:"total"` + Hijacked int64 `json:"hijacked"` +} + +type request struct { + Time string `json:"time"` + Count int64 `json:"count"` +} + +type logStats struct { + Summary summary `json:"summary"` + Requests []request `json:"requests"` +} + type httpError struct { err error Status int `json:"status"` Message string `json:"message"` } +func newHTTPError(err error) *httpError { + return &httpError{ + err: err, + Status: http.StatusInternalServerError, + } +} + // NewServer creates a new HTTP server, serving logs from the given logger and listening on addr. func NewServer(cache *cache.Cache, logger *sql.Logger, addr string) *Server { server := &http.Server{Addr: addr} @@ -55,6 +78,7 @@ func (s *Server) handler() http.Handler { r := newRouter() r.route(http.MethodGet, "/cache/v1/", s.cacheHandler) r.route(http.MethodGet, "/log/v1/", s.logHandler) + r.route(http.MethodGet, "/metric/v1/", s.metricHandler) r.route(http.MethodDelete, "/cache/v1/", s.cacheResetHandler) return r.handler() } @@ -89,18 +113,13 @@ func (s *Server) cacheResetHandler(w http.ResponseWriter, r *http.Request) (inte s.cache.Reset() return struct { Message string `json:"message"` - }{ - "Cleared cache", - }, nil + }{"Cleared cache."}, nil } func (s *Server) logHandler(w http.ResponseWriter, r *http.Request) (interface{}, *httpError) { logEntries, err := s.logger.Read(listCountFrom(r)) if err != nil { - return nil, &httpError{ - err: err, - Status: http.StatusInternalServerError, - } + return nil, newHTTPError(err) } entries := make([]entry, 0, len(logEntries)) for _, le := range logEntries { @@ -117,6 +136,29 @@ func (s *Server) logHandler(w http.ResponseWriter, r *http.Request) (interface{} return entries, nil } +func (s *Server) metricHandler(w http.ResponseWriter, r *http.Request) (interface{}, *httpError) { + stats, err := s.logger.Stats() + if err != nil { + return nil, newHTTPError(err) + } + requests := make([]request, 0, len(stats.Events)) + for _, e := range stats.Events { + requests = append(requests, request{ + Time: e.Time.Format(time.RFC3339), + Count: e.Count, + }) + } + logStats := logStats{ + Summary: summary{ + Since: stats.Since.Format(time.RFC3339), + Total: stats.Total, + Hijacked: stats.Hijacked, + }, + Requests: requests, + } + return logStats, nil +} + // Close shuts down the HTTP server. func (s *Server) Close() error { return s.server.Shutdown(context.TODO()) } diff --git a/http/http_test.go b/http/http_test.go index 7cd5a9c..901903f 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -30,11 +30,11 @@ func newA(name string, ttl uint32, ipAddr ...net.IP) *dns.Msg { } func testServer() (*httptest.Server, *Server) { - db, err := sql.New(":memory:") + sqlClient, err := sql.New(":memory:") if err != nil { panic(err) } - logger := sql.NewLogger(db, sql.LogAll, 0) + logger := sql.NewLogger(sqlClient, sql.LogAll, 0) cache := cache.New(10, nil) server := Server{logger: logger, cache: cache} return httptest.NewServer(server.handler()), &server @@ -89,6 +89,7 @@ func TestRequests(t *testing.T) { lr1 := `[{"time":"RFC3339","remote_addr":"127.0.0.254","hijacked":true,"type":"AAAA","question":"example.com.","answers":["2001:db8::1"]},` + `{"time":"RFC3339","remote_addr":"127.0.0.42","hijacked":false,"type":"A","question":"example.com.","answers":["192.0.2.101","192.0.2.100"]}]` lr2 := `[{"time":"RFC3339","remote_addr":"127.0.0.254","hijacked":true,"type":"AAAA","question":"example.com.","answers":["2001:db8::1"]}]` + mr1 := `{"summary":{"since":"RFC3339","total":2,"hijacked":1},"requests":[{"time":"RFC3339","count":2}]}` var tests = []struct { method string @@ -103,7 +104,8 @@ func TestRequests(t *testing.T) { {http.MethodGet, "/cache/v1/", cr1, 200}, {http.MethodGet, "/cache/v1/?n=foo", cr1, 200}, {http.MethodGet, "/cache/v1/?n=1", cr2, 200}, - {http.MethodDelete, "/cache/v1/", `{"message":"Cleared cache"}`, 200}, + {http.MethodDelete, "/cache/v1/", `{"message":"Cleared cache."}`, 200}, + {http.MethodGet, "/metric/v1/", mr1, 200}, } for i, tt := range tests { diff --git a/sql/logger.go b/sql/logger.go index 0c2fd6a..85e9152 100644 --- a/sql/logger.go +++ b/sql/logger.go @@ -35,6 +35,20 @@ type LogEntry struct { Answers []string } +// LogStats contains log statistics. +type LogStats struct { + Since time.Time + Total int64 + Hijacked int64 + Events []LogEvent +} + +// LogEvent contains the number of requests at a point in time. +type LogEvent struct { + Time time.Time + Count int64 +} + // NewLogger creates a new logger. Persisted entries are kept according to ttl. func NewLogger(client *Client, mode int, ttl time.Duration) *Logger { l := &Logger{ @@ -103,6 +117,27 @@ func (l *Logger) Read(n int) ([]LogEntry, error) { return logEntries, nil } +// Stats returns logger statistics. +func (l *Logger) Stats() (LogStats, error) { + stats, err := l.client.readLogStats() + if err != nil { + return LogStats{}, err + } + events := make([]LogEvent, 0, len(stats.Events)) + for _, le := range stats.Events { + events = append(events, LogEvent{ + Time: time.Unix(le.Time, 0).UTC(), + Count: le.Count, + }) + } + return LogStats{ + Since: time.Unix(stats.Since, 0).UTC(), + Total: stats.Total, + Hijacked: stats.Hijacked, + Events: events, + }, nil +} + func (l *Logger) readQueue(ttl time.Duration) { for e := range l.queue { if err := l.client.writeLog(e.Time, e.RemoteAddr, e.Hijacked, e.Qtype, e.Question, e.Answers...); err != nil { @@ -78,6 +78,18 @@ type logEntry struct { Answer string `db:"answer"` } +type logStats struct { + Since int64 `db:"since"` + Hijacked int64 `db:"hijacked"` + Total int64 `db:"total"` + Events []logEvent +} + +type logEvent struct { + Time int64 `db:"time"` + Count int64 `db:"count"` +} + type cacheEntry struct { Key uint32 `db:"key"` Data string `db:"data"` @@ -227,6 +239,31 @@ func (c *Client) deleteLogBefore(t time.Time) (err error) { return tx.Commit() } +func (c *Client) readLogStats() (logStats, error) { + c.mu.RLock() + defer c.mu.RUnlock() + var stats logStats + q1 := `SELECT COUNT(*) as total, + COUNT(CASE hijacked WHEN 1 THEN 1 ELSE NULL END) as hijacked, + time AS since + FROM log + ORDER BY time ASC LIMIT 1` + if err := c.db.Get(&stats, q1); err != nil { + return logStats{}, err + } + var events []logEvent + q2 := `SELECT time, + COUNT(*) AS count + FROM log + GROUP BY time + ORDER BY time ASC` + if err := c.db.Select(&events, q2); err != nil { + return logStats{}, err + } + stats.Events = events + return stats, nil +} + func (c *Client) writeCacheValue(key uint32, data string) error { c.mu.Lock() defer c.mu.Unlock() diff --git a/sql/sql_test.go b/sql/sql_test.go index 6f8bbbe..5c03c2d 100644 --- a/sql/sql_test.go +++ b/sql/sql_test.go @@ -182,3 +182,29 @@ func TestInterleavedRW(t *testing.T) { t.Fatal(err) } } + +func TestReadLogStats(t *testing.T) { + c := testClient() + writeTests(c, t) + got, err := c.readLogStats() + if err != nil { + t.Fatal(err) + } + want := logStats{ + Since: 1560636910, + Hijacked: 1, + Total: 8, + Events: []logEvent{ + {Time: 1560636910, Count: 1}, + {Time: 1560636980, Count: 1}, + {Time: 1560637050, Count: 1}, + {Time: 1560637120, Count: 1}, + {Time: 1560639880, Count: 1}, + {Time: 1560641700, Count: 2}, + {Time: 1560647100, Count: 1}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("readLogStats() = (%+v, _), want (%+v, _)", got, want) + } +} |