aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md23
-rw-r--r--http/http.go56
-rw-r--r--http/http_test.go8
-rw-r--r--sql/logger.go35
-rw-r--r--sql/sql.go37
-rw-r--r--sql/sql_test.go26
6 files changed, 175 insertions, 10 deletions
diff --git a/README.md b/README.md
index 7e6531f..bd67d5a 100644
--- a/README.md
+++ b/README.md
@@ -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 {
diff --git a/sql/sql.go b/sql/sql.go
index 5bc07e4..0ac86fa 100644
--- a/sql/sql.go
+++ b/sql/sql.go
@@ -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)
+ }
+}