Browse Source

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
Jakob Borg 11 years ago
parent
commit
00b662b53a
10 changed files with 186 additions and 6 deletions
  1. 0 0
      auto/gui.files.go
  2. 8 1
      cmd/syncthing/gui.go
  3. 1 0
      cmd/syncthing/main.go
  4. 16 0
      gui/app.js
  5. 5 0
      gui/index.html
  6. 2 0
      gui/lang-en.json
  7. 25 5
      model/model.go
  8. 17 0
      stats/debug.go
  9. 10 0
      stats/leveldb.go
  10. 102 0
      stats/node.go

File diff suppressed because it is too large
+ 0 - 0
auto/gui.files.go


+ 8 - 1
cmd/syncthing/gui.go

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

+ 1 - 0
cmd/syncthing/main.go

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

+ 5 - 0
gui/index.html

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

+ 2 - 0
gui/lang-en.json

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

+ 25 - 5
model/model.go

@@ -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 - 0
stats/debug.go

@@ -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 - 0
stats/leveldb.go

@@ -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 - 0
stats/node.go

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

Some files were not shown because too many files changed in this diff