Add heatmap and per country break down (#13)
This commit is contained in:
parent
8d95c82d7c
commit
725baf0971
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user