mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-12 16:26:37 +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 (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@ -24,7 +25,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
"code.google.com/p/go.crypto/bcrypt"
|
||||
"github.com/syncthing/syncthing/auto"
|
||||
"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/upgrade", restGetUpgrade)
|
||||
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
||||
getRestMux.HandleFunc("/rest/stats/node", withModel(m, restGetNodeStats))
|
||||
|
||||
// Debug endpoints, not for general use
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
encCfg := cfg
|
||||
if encCfg.GUI.Password != "" {
|
||||
|
@ -125,6 +125,7 @@ The following enviroment variables are interpreted by syncthing:
|
||||
- "net" (the main package; connections & network messages)
|
||||
- "model" (the model package)
|
||||
- "scanner" (the scanner package)
|
||||
- "stats" (the stats package)
|
||||
- "upnp" (the upnp package)
|
||||
- "xdr" (the xdr package)
|
||||
- "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.seenError = '';
|
||||
$scope.upgradeInfo = {};
|
||||
$scope.stats = {};
|
||||
|
||||
$http.get(urlbase+"/lang").success(function (langs) {
|
||||
// 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) {
|
||||
delete $scope.connections[arg.data.id];
|
||||
refreshNodeStats();
|
||||
});
|
||||
|
||||
$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() {
|
||||
refreshSystem();
|
||||
refreshConfig();
|
||||
refreshConnectionStats();
|
||||
refreshNodeStats();
|
||||
|
||||
$http.get(urlbase + '/version').success(function (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 () {
|
||||
return {
|
||||
restrict: 'C',
|
||||
|
@ -342,6 +342,11 @@
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -38,6 +38,7 @@
|
||||
"Idle": "Idle",
|
||||
"Ignore Permissions": "Ignore Permissions",
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Latest Release",
|
||||
"Local Discovery": "Local Discovery",
|
||||
"Local Discovery Port": "Local Discovery Port",
|
||||
@ -46,6 +47,7 @@
|
||||
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
||||
"Max Outstanding Requests": "Max Outstanding Requests",
|
||||
"Maximum Age": "Maximum Age",
|
||||
"Never": "Never",
|
||||
"No": "No",
|
||||
"No File Versioning": "No File Versioning",
|
||||
"Node ID": "Node ID",
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lamport"
|
||||
"github.com/syncthing/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/scanner"
|
||||
"github.com/syncthing/syncthing/stats"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
@ -72,11 +73,12 @@ type Model struct {
|
||||
clientName string
|
||||
clientVersion string
|
||||
|
||||
repoCfgs map[string]config.RepositoryConfiguration // repo -> cfg
|
||||
repoFiles map[string]*files.Set // repo -> files
|
||||
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
|
||||
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
|
||||
rmut sync.RWMutex // protects the above
|
||||
repoCfgs map[string]config.RepositoryConfiguration // repo -> cfg
|
||||
repoFiles map[string]*files.Set // repo -> files
|
||||
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
|
||||
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
|
||||
nodeStatRefs map[protocol.NodeID]*stats.NodeStatisticsReference // nodeID -> statsRef
|
||||
rmut sync.RWMutex // protects the above
|
||||
|
||||
repoState map[string]repoState // repo -> state
|
||||
repoStateChanged map[string]time.Time // repo -> time when state changed
|
||||
@ -114,6 +116,7 @@ func NewModel(indexDir string, cfg *config.Configuration, nodeName, clientName,
|
||||
repoFiles: make(map[string]*files.Set),
|
||||
repoNodes: make(map[string][]protocol.NodeID),
|
||||
nodeRepos: make(map[protocol.NodeID][]string),
|
||||
nodeStatRefs: make(map[protocol.NodeID]*stats.NodeStatisticsReference),
|
||||
repoState: make(map[string]repoState),
|
||||
repoStateChanged: make(map[string]time.Time),
|
||||
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),
|
||||
}
|
||||
|
||||
for _, node := range cfg.Nodes {
|
||||
m.nodeStatRefs[node.NodeID] = stats.NewNodeStatisticsReference(db, node.NodeID)
|
||||
}
|
||||
|
||||
var timeout = 20 * 60 // seconds
|
||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||
it, err := strconv.Atoi(t)
|
||||
@ -199,6 +206,15 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
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.
|
||||
func (m *Model) Completion(node protocol.NodeID, repo string) float64 {
|
||||
var tot int64
|
||||
@ -535,6 +551,9 @@ func (cf cFiler) CurrentFile(file string) protocol.FileInfo {
|
||||
func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
|
||||
m.pmut.RLock()
|
||||
_, ok := m.protoConn[nodeID]
|
||||
if ok {
|
||||
m.nodeStatRefs[nodeID].WasSeen()
|
||||
}
|
||||
m.pmut.RUnlock()
|
||||
return ok
|
||||
}
|
||||
@ -563,6 +582,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
|
||||
fs := m.repoFiles[repo]
|
||||
go sendIndexes(protoConn, repo, fs)
|
||||
}
|
||||
m.nodeStatRefs[nodeID].WasSeen()
|
||||
m.rmut.RUnlock()
|
||||
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