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:
Jakob Borg 2014-08-25 15:52:59 +02:00
commit 00b662b53a
10 changed files with 189 additions and 9 deletions

File diff suppressed because one or more lines are too long

View File

@ -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 != "" {

View File

@ -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)

View File

@ -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',

View File

@ -342,6 +342,11 @@
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th> <th><span class="glyphicon glyphicon-tag"></span>&emsp;<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>&emsp;<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>

View File

@ -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",

View File

@ -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
View 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
View 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
View 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(),
}
}