Merge pull request #9 in DNS/adguard-dns from consistent-stats to master

* commit '31893410892bd047c9f6ea8f602717e6996c9491':
  web interface -- Make refresh buttons reload all data, not just counters
  web interface -- change text from 'general counters' to 'general statistics'
  Fixup of previous commit -- errand keystroke crept in
  API /stats_top -- sort top entries by value
  API /stats_top -- show only top entries for last 3 minutes
This commit is contained in:
Konstantin 🦄 Zamyakin 2018-09-07 18:21:46 +03:00
commit ba836220b8
4 changed files with 55 additions and 15 deletions

View File

@ -5,7 +5,7 @@ import Card from '../ui/Card';
import Tooltip from '../ui/Tooltip';
const Counters = props => (
<Card title="General counters" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<Card title="General statistics" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<table className="table card-table">
<tbody>
<tr>

View File

@ -27,8 +27,8 @@ class Dashboard extends Component {
dashboard.processingTopStats;
const disableButton = <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={() => this.props.disableDns()}>Disable DNS</button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.props.getStats()}>Refresh statistics</button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.props.getStats()}></button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.componentDidMount()}>Refresh statistics</button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.componentDidMount()}></button>;
return (
<Fragment>

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
@ -409,6 +410,9 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
domains := map[string]int{}
blocked := map[string]int{}
clients := map[string]int{}
now := time.Now()
timeWindow := time.Minute * 3
notBefore := now.Add(timeWindow * -1)
for _, value := range values {
entry, ok := value.(map[string]interface{})
@ -419,6 +423,11 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
host := getHost(entry)
reason := getReason(entry)
client := getClient(entry)
time := getTime(entry)
if time.Before(notBefore) {
// skip if the entry is before specified cutoff
continue
}
if len(host) > 0 {
domains[host]++
}
@ -430,21 +439,35 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
}
}
toMarshal := map[string]interface{}{
"top_queried_domains": produceTop(domains, 50),
"top_blocked_domains": produceTop(blocked, 50),
"top_clients": produceTop(clients, 50),
}
json, err := json.Marshal(toMarshal)
if err != nil {
errortext := fmt.Sprintf("Couldn't marshal into JSON: %s", err)
log.Println(errortext)
http.Error(w, errortext, http.StatusBadGateway)
return
// use manual json marshalling because we want maps to be sorted by value
json := bytes.Buffer{}
json.WriteString("{\n")
gen := func(json *bytes.Buffer, name string, top map[string]int, addComma bool) {
json.WriteString(" \"")
json.WriteString(name)
json.WriteString("\": {\n")
sorted := sortByValue(top)
for i, key := range sorted {
fmt.Fprintf(json, " \"%s\": %d", key, top[key])
if i+1 != len(sorted) {
json.WriteByte(',')
}
json.WriteByte('\n')
}
json.WriteString(" }")
if addComma {
json.WriteByte(',')
}
json.WriteByte('\n')
}
gen(&json, "top_queried_domains", domains, true)
gen(&json, "top_blocked_domains", blocked, true)
gen(&json, "top_clients", clients, false)
json.WriteString("}\n")
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(json)
_, err = w.Write(json.Bytes())
if err != nil {
errortext := fmt.Sprintf("Couldn't write body: %s", err)
log.Println(errortext)

View File

@ -7,6 +7,7 @@ import (
"net/http"
"sort"
"strings"
"time"
)
func clamp(value, low, high int) int {
@ -167,6 +168,22 @@ func getClient(entry map[string]interface{}) string {
return client
}
func getTime(entry map[string]interface{}) time.Time {
t, ok := entry["time"]
if !ok {
return time.Time{}
}
tstr, ok := t.(string)
if !ok {
return time.Time{}
}
value, err := time.Parse(time.RFC3339, tstr)
if err != nil {
return time.Time{}
}
return value
}
// -------------------------------------------------
// helper functions for parsing parameters from body
// -------------------------------------------------