mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-14 09:14:10 +00:00
Merge branch 'pr/556'
* pr/556: Add translation strings Display Last Seen value in the UI Add /rest/stats/node endpoint Add stats package and node related statistics model
This commit is contained in:
commit
00b662b53a
File diff suppressed because one or more lines are too long
@ -6,6 +6,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -24,7 +25,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crypto/tls"
|
|
||||||
"code.google.com/p/go.crypto/bcrypt"
|
"code.google.com/p/go.crypto/bcrypt"
|
||||||
"github.com/syncthing/syncthing/auto"
|
"github.com/syncthing/syncthing/auto"
|
||||||
"github.com/syncthing/syncthing/config"
|
"github.com/syncthing/syncthing/config"
|
||||||
@ -111,6 +111,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
|||||||
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
||||||
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
||||||
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
||||||
|
getRestMux.HandleFunc("/rest/stats/node", withModel(m, restGetNodeStats))
|
||||||
|
|
||||||
// Debug endpoints, not for general use
|
// Debug endpoints, not for general use
|
||||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
||||||
@ -265,6 +266,12 @@ func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request)
|
|||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restGetNodeStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var res = m.NodeStatistics()
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
encCfg := cfg
|
encCfg := cfg
|
||||||
if encCfg.GUI.Password != "" {
|
if encCfg.GUI.Password != "" {
|
||||||
|
@ -125,6 +125,7 @@ The following enviroment variables are interpreted by syncthing:
|
|||||||
- "net" (the main package; connections & network messages)
|
- "net" (the main package; connections & network messages)
|
||||||
- "model" (the model package)
|
- "model" (the model package)
|
||||||
- "scanner" (the scanner package)
|
- "scanner" (the scanner package)
|
||||||
|
- "stats" (the stats package)
|
||||||
- "upnp" (the upnp package)
|
- "upnp" (the upnp package)
|
||||||
- "xdr" (the xdr package)
|
- "xdr" (the xdr package)
|
||||||
- "all" (all of the above)
|
- "all" (all of the above)
|
||||||
|
16
gui/app.js
16
gui/app.js
@ -80,6 +80,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.repos = {};
|
$scope.repos = {};
|
||||||
$scope.seenError = '';
|
$scope.seenError = '';
|
||||||
$scope.upgradeInfo = {};
|
$scope.upgradeInfo = {};
|
||||||
|
$scope.stats = {};
|
||||||
|
|
||||||
$http.get(urlbase+"/lang").success(function (langs) {
|
$http.get(urlbase+"/lang").success(function (langs) {
|
||||||
// Find the first language in the list provided by the user's browser
|
// Find the first language in the list provided by the user's browser
|
||||||
@ -175,6 +176,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
$scope.$on('NodeDisconnected', function (event, arg) {
|
$scope.$on('NodeDisconnected', function (event, arg) {
|
||||||
delete $scope.connections[arg.data.id];
|
delete $scope.connections[arg.data.id];
|
||||||
|
refreshNodeStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$on('NodeConnected', function (event, arg) {
|
$scope.$on('NodeConnected', function (event, arg) {
|
||||||
@ -332,10 +334,18 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var refreshNodeStats = debounce(function () {
|
||||||
|
$http.get(urlbase+"/stats/node").success(function (data) {
|
||||||
|
$scope.stats = data;
|
||||||
|
console.log("refreshNodeStats", data);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = function() {
|
||||||
refreshSystem();
|
refreshSystem();
|
||||||
refreshConfig();
|
refreshConfig();
|
||||||
refreshConnectionStats();
|
refreshConnectionStats();
|
||||||
|
refreshNodeStats();
|
||||||
|
|
||||||
$http.get(urlbase + '/version').success(function (data) {
|
$http.get(urlbase + '/version').success(function (data) {
|
||||||
$scope.version = data;
|
$scope.version = data;
|
||||||
@ -1049,6 +1059,12 @@ syncthing.filter('clean', function () {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
syncthing.filter('asDate', function() {
|
||||||
|
return function (input) {
|
||||||
|
return new Date(input);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
syncthing.directive('optionEditor', function () {
|
syncthing.directive('optionEditor', function () {
|
||||||
return {
|
return {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
|
@ -342,6 +342,11 @@
|
|||||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||||
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="!connections[nodeCfg.NodeID]">
|
||||||
|
<th><span class="glyphicon glyphicon-eye-open"></span> <span translate>Last seen</span></th>
|
||||||
|
<td translate ng-if="stats[nodeCfg.NodeID].LastSeen.indexOf('1970') > -1" class="text-right">Never</td>
|
||||||
|
<td ng-if="stats[nodeCfg.NodeID].LastSeen.indexOf('1970') < 0" class="text-right">{{stats[nodeCfg.NodeID].LastSeen | asDate | date:'short'}}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"Idle": "Idle",
|
"Idle": "Idle",
|
||||||
"Ignore Permissions": "Ignore Permissions",
|
"Ignore Permissions": "Ignore Permissions",
|
||||||
"Keep Versions": "Keep Versions",
|
"Keep Versions": "Keep Versions",
|
||||||
|
"Last seen": "Last seen",
|
||||||
"Latest Release": "Latest Release",
|
"Latest Release": "Latest Release",
|
||||||
"Local Discovery": "Local Discovery",
|
"Local Discovery": "Local Discovery",
|
||||||
"Local Discovery Port": "Local Discovery Port",
|
"Local Discovery Port": "Local Discovery Port",
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
||||||
"Max Outstanding Requests": "Max Outstanding Requests",
|
"Max Outstanding Requests": "Max Outstanding Requests",
|
||||||
"Maximum Age": "Maximum Age",
|
"Maximum Age": "Maximum Age",
|
||||||
|
"Never": "Never",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"No File Versioning": "No File Versioning",
|
"No File Versioning": "No File Versioning",
|
||||||
"Node ID": "Node ID",
|
"Node ID": "Node ID",
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lamport"
|
"github.com/syncthing/syncthing/lamport"
|
||||||
"github.com/syncthing/syncthing/protocol"
|
"github.com/syncthing/syncthing/protocol"
|
||||||
"github.com/syncthing/syncthing/scanner"
|
"github.com/syncthing/syncthing/scanner"
|
||||||
|
"github.com/syncthing/syncthing/stats"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ type Model struct {
|
|||||||
repoFiles map[string]*files.Set // repo -> files
|
repoFiles map[string]*files.Set // repo -> files
|
||||||
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
|
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
|
||||||
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
|
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
|
||||||
|
nodeStatRefs map[protocol.NodeID]*stats.NodeStatisticsReference // nodeID -> statsRef
|
||||||
rmut sync.RWMutex // protects the above
|
rmut sync.RWMutex // protects the above
|
||||||
|
|
||||||
repoState map[string]repoState // repo -> state
|
repoState map[string]repoState // repo -> state
|
||||||
@ -114,6 +116,7 @@ func NewModel(indexDir string, cfg *config.Configuration, nodeName, clientName,
|
|||||||
repoFiles: make(map[string]*files.Set),
|
repoFiles: make(map[string]*files.Set),
|
||||||
repoNodes: make(map[string][]protocol.NodeID),
|
repoNodes: make(map[string][]protocol.NodeID),
|
||||||
nodeRepos: make(map[protocol.NodeID][]string),
|
nodeRepos: make(map[protocol.NodeID][]string),
|
||||||
|
nodeStatRefs: make(map[protocol.NodeID]*stats.NodeStatisticsReference),
|
||||||
repoState: make(map[string]repoState),
|
repoState: make(map[string]repoState),
|
||||||
repoStateChanged: make(map[string]time.Time),
|
repoStateChanged: make(map[string]time.Time),
|
||||||
protoConn: make(map[protocol.NodeID]protocol.Connection),
|
protoConn: make(map[protocol.NodeID]protocol.Connection),
|
||||||
@ -122,6 +125,10 @@ func NewModel(indexDir string, cfg *config.Configuration, nodeName, clientName,
|
|||||||
sentLocalVer: make(map[protocol.NodeID]map[string]uint64),
|
sentLocalVer: make(map[protocol.NodeID]map[string]uint64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, node := range cfg.Nodes {
|
||||||
|
m.nodeStatRefs[node.NodeID] = stats.NewNodeStatisticsReference(db, node.NodeID)
|
||||||
|
}
|
||||||
|
|
||||||
var timeout = 20 * 60 // seconds
|
var timeout = 20 * 60 // seconds
|
||||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||||
it, err := strconv.Atoi(t)
|
it, err := strconv.Atoi(t)
|
||||||
@ -199,6 +206,15 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns statistics about each node
|
||||||
|
func (m *Model) NodeStatistics() map[string]stats.NodeStatistics {
|
||||||
|
var res = make(map[string]stats.NodeStatistics)
|
||||||
|
for _, node := range m.cfg.Nodes {
|
||||||
|
res[node.NodeID.String()] = m.nodeStatRefs[node.NodeID].GetStatistics()
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the completion status, in percent, for the given node and repo.
|
// Returns the completion status, in percent, for the given node and repo.
|
||||||
func (m *Model) Completion(node protocol.NodeID, repo string) float64 {
|
func (m *Model) Completion(node protocol.NodeID, repo string) float64 {
|
||||||
var tot int64
|
var tot int64
|
||||||
@ -535,6 +551,9 @@ func (cf cFiler) CurrentFile(file string) protocol.FileInfo {
|
|||||||
func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
|
func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
|
||||||
m.pmut.RLock()
|
m.pmut.RLock()
|
||||||
_, ok := m.protoConn[nodeID]
|
_, ok := m.protoConn[nodeID]
|
||||||
|
if ok {
|
||||||
|
m.nodeStatRefs[nodeID].WasSeen()
|
||||||
|
}
|
||||||
m.pmut.RUnlock()
|
m.pmut.RUnlock()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
@ -563,6 +582,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
|
|||||||
fs := m.repoFiles[repo]
|
fs := m.repoFiles[repo]
|
||||||
go sendIndexes(protoConn, repo, fs)
|
go sendIndexes(protoConn, repo, fs)
|
||||||
}
|
}
|
||||||
|
m.nodeStatRefs[nodeID].WasSeen()
|
||||||
m.rmut.RUnlock()
|
m.rmut.RUnlock()
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
}
|
}
|
||||||
|
17
stats/debug.go
Executable file
17
stats/debug.go
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||||
|
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debug = strings.Contains(os.Getenv("STTRACE"), "stats") || os.Getenv("STTRACE") == "all"
|
||||||
|
l = logger.DefaultLogger
|
||||||
|
)
|
10
stats/leveldb.go
Executable file
10
stats/leveldb.go
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||||
|
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stats
|
||||||
|
|
||||||
|
// Same key space as files/leveldb.go keyType* constants
|
||||||
|
const (
|
||||||
|
keyTypeNodeStatistic = iota + 30
|
||||||
|
)
|
102
stats/node.go
Executable file
102
stats/node.go
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||||
|
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/protocol"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodeStatisticTypeLastSeen = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var nodeStatisticsTypes = []byte{
|
||||||
|
nodeStatisticTypeLastSeen,
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeStatistics struct {
|
||||||
|
LastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeStatisticsReference struct {
|
||||||
|
db *leveldb.DB
|
||||||
|
node protocol.NodeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNodeStatisticsReference(db *leveldb.DB, node protocol.NodeID) *NodeStatisticsReference {
|
||||||
|
return &NodeStatisticsReference{
|
||||||
|
db: db,
|
||||||
|
node: node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NodeStatisticsReference) key(stat byte) []byte {
|
||||||
|
k := make([]byte, 1+1+32)
|
||||||
|
k[0] = keyTypeNodeStatistic
|
||||||
|
k[1] = stat
|
||||||
|
copy(k[1+1:], s.node[:])
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NodeStatisticsReference) GetLastSeen() time.Time {
|
||||||
|
value, err := s.db.Get(s.key(nodeStatisticTypeLastSeen), nil)
|
||||||
|
if err != nil {
|
||||||
|
if err != leveldb.ErrNotFound {
|
||||||
|
l.Warnln("NodeStatisticsReference: Failed loading last seen value for", s.node, ":", err)
|
||||||
|
}
|
||||||
|
return time.Unix(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
rtime := time.Time{}
|
||||||
|
err = rtime.UnmarshalBinary(value)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("NodeStatisticsReference: Failed parsing last seen value for", s.node, ":", err)
|
||||||
|
return time.Unix(0, 0)
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
l.Debugln("stats.NodeStatisticsReference.GetLastSeen:", s.node, rtime)
|
||||||
|
}
|
||||||
|
return rtime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NodeStatisticsReference) WasSeen() {
|
||||||
|
if debug {
|
||||||
|
l.Debugln("stats.NodeStatisticsReference.WasSeen:", s.node)
|
||||||
|
}
|
||||||
|
value, err := time.Now().MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("NodeStatisticsReference: Failed serializing last seen value for", s.node, ":", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.db.Put(s.key(nodeStatisticTypeLastSeen), value, nil)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Failed serializing last seen value for", s.node, ":", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never called, maybe because it's worth while to keep the data
|
||||||
|
// or maybe because we have no easy way of knowing that a node has been removed.
|
||||||
|
func (s *NodeStatisticsReference) Delete() error {
|
||||||
|
for _, stype := range nodeStatisticsTypes {
|
||||||
|
err := s.db.Delete(s.key(stype), nil)
|
||||||
|
if debug && err == nil {
|
||||||
|
l.Debugln("stats.NodeStatisticsReference.Delete:", s.node, stype)
|
||||||
|
}
|
||||||
|
if err != nil && err != leveldb.ErrNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NodeStatisticsReference) GetStatistics() NodeStatistics {
|
||||||
|
return NodeStatistics{
|
||||||
|
LastSeen: s.GetLastSeen(),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user