Add heatmap and per country break down (#13)

This commit is contained in:
Audrius Butkevicius 2017-11-11 15:51:59 +00:00 committed by GitHub
parent 8d95c82d7c
commit 725baf0971
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 178 additions and 12 deletions

View File

@ -23,6 +23,7 @@ import (
"unicode" "unicode"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/oschwald/geoip2-golang"
) )
var ( var (
@ -32,6 +33,7 @@ var (
certFile = getEnvDefault("UR_CRT_FILE", "crt.pem") certFile = getEnvDefault("UR_CRT_FILE", "crt.pem")
dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable") dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable")
listenAddr = getEnvDefault("UR_LISTEN", "0.0.0.0:8443") listenAddr = getEnvDefault("UR_LISTEN", "0.0.0.0:8443")
geoIPPath = getEnvDefault("UR_GEOIP", "GeoLite2-City.mmdb")
tpl *template.Template tpl *template.Template
compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`) compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`)
progressBarClass = []string{"", "progress-bar-success", "progress-bar-info", "progress-bar-warning", "progress-bar-danger"} progressBarClass = []string{"", "progress-bar-success", "progress-bar-info", "progress-bar-warning", "progress-bar-danger"}
@ -49,6 +51,20 @@ var funcs = map[string]interface{}{
"progressBarClassByIndex": func(a int) string { "progressBarClassByIndex": func(a int) string {
return progressBarClass[a%len(progressBarClass)] return progressBarClass[a%len(progressBarClass)]
}, },
"slice": func(numParts, whichPart int, input []feature) []feature {
var part []feature
perPart := (len(input) / numParts) + len(input)%2
parts := make([][]feature, 0, numParts)
for len(input) >= perPart {
part, input = input[:perPart], input[perPart:]
parts = append(parts, part)
}
if len(input) > 0 {
parts = append(parts, input[:len(input)])
}
return parts[whichPart-1]
},
} }
func getEnvDefault(key, def string) string { func getEnvDefault(key, def string) string {
@ -680,7 +696,7 @@ func main() {
srv := http.Server{ srv := http.Server{
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second, WriteTimeout: 15 * time.Second,
} }
http.HandleFunc("/", withDB(db, rootHandler)) http.HandleFunc("/", withDB(db, rootHandler))
@ -924,8 +940,22 @@ func inc(storage map[string]int, key string, i interface{}) {
storage[key] = cv storage[key] = cv
} }
type location struct {
Latitude float64
Longitude float64
}
func getReport(db *sql.DB) map[string]interface{} { func getReport(db *sql.DB) map[string]interface{} {
geoip, err := geoip2.Open(geoIPPath)
if err != nil {
log.Println("opening geoip db", err)
geoip = nil
} else {
defer geoip.Close()
}
nodes := 0 nodes := 0
countriesTotal := 0
var versions []string var versions []string
var platforms []string var platforms []string
var numFolders []int var numFolders []int
@ -940,6 +970,8 @@ func getReport(db *sql.DB) map[string]interface{} {
var uptime []int var uptime []int
var compilers []string var compilers []string
var builders []string var builders []string
locations := make(map[location]int)
countries := make(map[string]int)
reports := make(map[string]int) reports := make(map[string]int)
totals := make(map[string]int) totals := make(map[string]int)
@ -989,6 +1021,21 @@ func getReport(db *sql.DB) map[string]interface{} {
return nil return nil
} }
if geoip != nil && rep.Address != "" {
if addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(rep.Address, "0")); err == nil {
city, err := geoip.City(addr.IP)
if err == nil {
loc := location{
Latitude: city.Location.Latitude,
Longitude: city.Location.Longitude,
}
locations[loc]++
countries[city.Country.Names["en"]]++
countriesTotal++
}
}
}
nodes++ nodes++
versions = append(versions, transformVersion(rep.Version)) versions = append(versions, transformVersion(rep.Version))
platforms = append(platforms, rep.Platform) platforms = append(platforms, rep.Platform)
@ -1266,6 +1313,16 @@ func getReport(db *sql.DB) map[string]interface{} {
reportFeatureGroups[featureType] = featureList reportFeatureGroups[featureType] = featureList
} }
var countryList []feature
for country, count := range countries {
countryList = append(countryList, feature{
Key: country,
Count: count,
Pct: (100 * float64(count)) / float64(countriesTotal),
})
sort.Sort(sort.Reverse(sortableFeatureList(countryList)))
}
r := make(map[string]interface{}) r := make(map[string]interface{})
r["features"] = reportFeatures r["features"] = reportFeatures
r["featureGroups"] = reportFeatureGroups r["featureGroups"] = reportFeatureGroups
@ -1277,6 +1334,8 @@ func getReport(db *sql.DB) map[string]interface{} {
r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 3) r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 3)
r["builders"] = analyticsFor(builders, 12) r["builders"] = analyticsFor(builders, 12)
r["featureOrder"] = featureOrder r["featureOrder"] = featureOrder
r["locations"] = locations
r["contries"] = countryList
return r return r
} }

View File

@ -17,6 +17,7 @@ found in the LICENSE file.
<link href="static/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="static/bootstrap/js/bootstrap.min.js"></script> <script type="text/javascript" src="static/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=visualization"></script>
<style type="text/css"> <style type="text/css">
body { body {
margin: 40px; margin: 40px;
@ -48,6 +49,7 @@ found in the LICENSE file.
google.setOnLoadCallback(drawMovementChart); google.setOnLoadCallback(drawMovementChart);
google.setOnLoadCallback(drawBlockStatsChart); google.setOnLoadCallback(drawBlockStatsChart);
google.setOnLoadCallback(drawPerformanceCharts); google.setOnLoadCallback(drawPerformanceCharts);
google.setOnLoadCallback(drawHeatMap);
function drawVersionChart() { function drawVersionChart() {
var jsonData = $.ajax({url: "summary.json", dataType:"json", async: false}).responseText; var jsonData = $.ajax({url: "summary.json", dataType:"json", async: false}).responseText;
@ -143,6 +145,10 @@ found in the LICENSE file.
} }
content += "</table>"; content += "</table>";
document.getElementById("data-to-date").innerHTML = content; document.getElementById("data-to-date").innerHTML = content;
} else {
// No data, hide it.
document.getElementById("block-stats").outerHTML = "";
return;
} }
var options = { var options = {
@ -195,6 +201,51 @@ found in the LICENSE file.
var chart = new google.visualization.LineChart(document.getElementById(id)); var chart = new google.visualization.LineChart(document.getElementById(id));
chart.draw(data, options); chart.draw(data, options);
} }
var locations = [];
{{range $location, $weight := .locations}}
locations.push({location: new google.maps.LatLng({{- $location.Latitude -}}, {{- $location.Longitude -}}), weight: {{- $weight -}}});
{{- end}}
function drawHeatMap() {
if (locations.length == 0) {
return;
}
var mapBounds = new google.maps.LatLngBounds();
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 1,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var heatmap = new google.maps.visualization.HeatmapLayer({
data: locations
});
heatmap.set('radius', 10);
heatmap.set('maxIntensity', 20);
heatmap.set('gradient', [
'rgba(0, 255, 255, 0)',
'rgba(0, 255, 255, 1)',
'rgba(0, 191, 255, 1)',
'rgba(0, 127, 255, 1)',
'rgba(0, 63, 255, 1)',
'rgba(0, 0, 255, 1)',
'rgba(0, 0, 223, 1)',
'rgba(0, 0, 191, 1)',
'rgba(0, 0, 159, 1)',
'rgba(0, 0, 127, 1)',
'rgba(63, 0, 91, 1)',
'rgba(127, 0, 63, 1)',
'rgba(191, 0, 31, 1)',
'rgba(255, 0, 0, 1)'
]);
heatmap.setMap(map);
for (var x = 0; x < locations.length; x++) {
mapBounds.extend(locations[x].location);
}
map.fitBounds(mapBounds);
if (locations.length == 1) {
map.setZoom(13);
}
}
</script> </script>
</head> </head>
@ -218,22 +269,74 @@ found in the LICENSE file.
<p class="text-muted"> <p class="text-muted">
Reappearance of users cause the "left" data to shrink retroactively. Reappearance of users cause the "left" data to shrink retroactively.
</p> </p>
<div id="block-stats">
<h4 id="block-stats">Data Transfers per Day</h4> <h4>Data Transfers per Day</h4>
<p> <p>
This is total data transferred per day. Also shows how much data was saved (not transferred) by each of the methods syncthing uses. This is total data transferred per day. Also shows how much data was saved (not transferred) by each of the methods syncthing uses.
</p> </p>
<div class="img-thumbnail" id="blockStatsChart" style="width: 1130px; height: 400px; padding: 10px;"></div> <div class="img-thumbnail" id="blockStatsChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<h4 id="totals-to-date">Totals to date</h4> <h4 id="totals-to-date">Totals to date</h4>
<p id="data-to-date"> <p id="data-to-date">
No data No data
</p> </p>
</div>
<h4 id="metrics">Usage Metrics</h4> <h4 id="metrics">Usage Metrics</h4>
<p> <p>
This is the aggregated usage report data for the last 24 hours. Data based on <b>{{.nodes}}</b> devices that have reported in. This is the aggregated usage report data for the last 24 hours. Data based on <b>{{.nodes}}</b> devices that have reported in.
</p> </p>
{{if .locations}}
<div class="img-thumbnail" id="map" style="width: 1130px; height: 400px; padding: 10px;"></div>
<p class="text-muted">
Heatmap max intensity is capped at 20 reports within a location.
</p>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#collapseTwo">Break down per country</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<table class="table table-striped">
<tbody>
{{range .contries | slice 2 1}}
<tr>
<td style="width: 45%">{{.Key}}</td>
<td style="width: 5%" class="text-right">{{if ge .Pct 10.0}}{{.Pct | printf "%.0f"}}{{else if ge .Pct 1.0}}{{.Pct | printf "%.01f"}}{{else}}{{.Pct | printf "%.02f"}}{{end}}%</td>
<td style="width: 5%" class="text-right">{{.Count}}</td>
<td>
<div class="progress-bar" role="progressbar" aria-valuenow="{{.Pct | printf "%.02f"}}" aria-valuemin="0" aria-valuemax="100" style="width: {{.Pct | printf "%.02f"}}%; height:20px"></div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-striped">
<tbody>
{{range .contries | slice 2 2}}
<tr>
<td style="width: 45%">{{.Key}}</td>
<td style="width: 5%" class="text-right">{{if ge .Pct 10.0}}{{.Pct | printf "%.0f"}}{{else if ge .Pct 1.0}}{{.Pct | printf "%.01f"}}{{else}}{{.Pct | printf "%.02f"}}{{end}}%</td>
<td style="width: 5%" class="text-right">{{.Count}}</td>
<td>
<div class="progress-bar" role="progressbar" aria-valuenow="{{.Pct | printf "%.02f"}}" aria-valuemin="0" aria-valuemax="100" style="width: {{.Pct | printf "%.02f"}}%; height:20px"></div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{end}}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -266,7 +369,6 @@ found in the LICENSE file.
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -492,6 +594,11 @@ found in the LICENSE file.
</div> </div>
</div> </div>
</div> </div>
<hr>
<p>
This product includes GeoLite2 data created by MaxMind, available from
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
</p>
<script type="text/javascript"> <script type="text/javascript">
$('[data-toggle="tooltip"]').tooltip({html:true}); $('[data-toggle="tooltip"]').tooltip({html:true});
</script> </script>