diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index d21e43bb..3aa40e02 100644 --- a/cmd/syncthing/gui.go +++ b/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" diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 200b29a2..a2349a91 100644 --- a/cmd/syncthing/main.go +++ b/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) diff --git a/model/model.go b/model/model.go index ae8517fe..4c5b41d8 100644 --- a/model/model.go +++ b/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() } diff --git a/stats/debug.go b/stats/debug.go new file mode 100755 index 00000000..ed5e3db9 --- /dev/null +++ b/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 +) diff --git a/stats/leveldb.go b/stats/leveldb.go new file mode 100755 index 00000000..52748b80 --- /dev/null +++ b/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 +) diff --git a/stats/node.go b/stats/node.go new file mode 100755 index 00000000..053fa449 --- /dev/null +++ b/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(), + } +}