package monitor
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
metrics "github.com/rcrowley/go-metrics"
|
|
tmtypes "github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// UptimeData stores data for how long network has been running.
|
|
type UptimeData struct {
|
|
StartTime time.Time `json:"start_time"`
|
|
Uptime float64 `json:"uptime" wire:"unsafe"` // percentage of time we've been healthy, ever
|
|
|
|
totalDownTime time.Duration // total downtime (only updated when we come back online)
|
|
wentDown time.Time
|
|
}
|
|
|
|
// Health describes the health of the network. Note that this applies only to
|
|
// the observed nodes, and not to the entire cluster, which may consist of
|
|
// thousands of machines. It may change in the future.
|
|
type Health int
|
|
|
|
const (
|
|
// FullHealth means all nodes online, synced, validators making blocks
|
|
FullHealth = Health(0)
|
|
// ModerateHealth means we're making blocks
|
|
ModerateHealth = Health(1)
|
|
// Dead means we're not making blocks due to all validators freezing or crashing
|
|
Dead = Health(2)
|
|
)
|
|
|
|
// Common statistics for network of nodes
|
|
type Network struct {
|
|
Height int64 `json:"height"`
|
|
|
|
AvgBlockTime float64 `json:"avg_block_time" wire:"unsafe"` // ms (avg over last minute)
|
|
blockTimeMeter metrics.Meter
|
|
AvgTxThroughput float64 `json:"avg_tx_throughput" wire:"unsafe"` // tx/s (avg over last minute)
|
|
txThroughputMeter metrics.Meter
|
|
AvgBlockLatency float64 `json:"avg_block_latency" wire:"unsafe"` // ms (avg over last minute)
|
|
blockLatencyMeter metrics.Meter
|
|
|
|
NumValidators int `json:"num_validators"`
|
|
NumNodesMonitored int `json:"num_nodes_monitored"`
|
|
NumNodesMonitoredOnline int `json:"num_nodes_monitored_online"`
|
|
|
|
Health Health `json:"health"`
|
|
|
|
UptimeData *UptimeData `json:"uptime_data"`
|
|
|
|
nodeStatusMap map[string]bool
|
|
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewNetwork() *Network {
|
|
return &Network{
|
|
blockTimeMeter: metrics.NewMeter(),
|
|
txThroughputMeter: metrics.NewMeter(),
|
|
blockLatencyMeter: metrics.NewMeter(),
|
|
Health: FullHealth,
|
|
UptimeData: &UptimeData{
|
|
StartTime: time.Now(),
|
|
Uptime: 100.0,
|
|
},
|
|
nodeStatusMap: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
func (n *Network) NewBlock(b tmtypes.Header) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if n.Height >= b.Height {
|
|
return
|
|
}
|
|
|
|
n.Height = b.Height
|
|
|
|
n.blockTimeMeter.Mark(1)
|
|
if n.blockTimeMeter.Rate1() > 0.0 {
|
|
n.AvgBlockTime = (1.0 / n.blockTimeMeter.Rate1()) * 1000 // 1/s to ms
|
|
} else {
|
|
n.AvgBlockTime = 0.0
|
|
}
|
|
n.txThroughputMeter.Mark(int64(b.NumTxs))
|
|
n.AvgTxThroughput = n.txThroughputMeter.Rate1()
|
|
}
|
|
|
|
func (n *Network) NewBlockLatency(l float64) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
n.blockLatencyMeter.Mark(int64(l))
|
|
n.AvgBlockLatency = n.blockLatencyMeter.Rate1() / 1000000.0 // ns to ms
|
|
}
|
|
|
|
// RecalculateUptime calculates uptime on demand.
|
|
func (n *Network) RecalculateUptime() {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
since := time.Since(n.UptimeData.StartTime)
|
|
uptime := since - n.UptimeData.totalDownTime
|
|
if n.Health != FullHealth {
|
|
uptime -= time.Since(n.UptimeData.wentDown)
|
|
}
|
|
n.UptimeData.Uptime = (float64(uptime) / float64(since)) * 100.0
|
|
}
|
|
|
|
// NodeIsDown is called when the node disconnects for whatever reason.
|
|
// Must be safe to call multiple times.
|
|
func (n *Network) NodeIsDown(name string) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if online, ok := n.nodeStatusMap[name]; !ok || online {
|
|
n.nodeStatusMap[name] = false
|
|
n.NumNodesMonitoredOnline--
|
|
n.UptimeData.wentDown = time.Now()
|
|
n.updateHealth()
|
|
}
|
|
}
|
|
|
|
// NodeIsOnline is called when connection to the node is restored.
|
|
// Must be safe to call multiple times.
|
|
func (n *Network) NodeIsOnline(name string) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if online, ok := n.nodeStatusMap[name]; ok && !online {
|
|
n.nodeStatusMap[name] = true
|
|
n.NumNodesMonitoredOnline++
|
|
n.UptimeData.totalDownTime += time.Since(n.UptimeData.wentDown)
|
|
n.updateHealth()
|
|
}
|
|
}
|
|
|
|
// NewNode is called when the new node is added to the monitor.
|
|
func (n *Network) NewNode(name string) {
|
|
n.NumNodesMonitored++
|
|
n.NumNodesMonitoredOnline++
|
|
}
|
|
|
|
// NodeDeleted is called when the node is deleted from under the monitor.
|
|
func (n *Network) NodeDeleted(name string) {
|
|
n.NumNodesMonitored--
|
|
n.NumNodesMonitoredOnline--
|
|
}
|
|
|
|
func (n *Network) updateHealth() {
|
|
// if we are connected to all validators, we're at full health
|
|
// TODO: make sure they're all at the same height (within a block)
|
|
// and all proposing (and possibly validating ) Alternatively, just
|
|
// check there hasn't been a new round in numValidators rounds
|
|
if n.NumValidators != 0 && n.NumNodesMonitoredOnline == n.NumValidators {
|
|
n.Health = FullHealth
|
|
} else if n.NumNodesMonitoredOnline > 0 && n.NumNodesMonitoredOnline <= n.NumNodesMonitored {
|
|
n.Health = ModerateHealth
|
|
} else {
|
|
n.Health = Dead
|
|
}
|
|
}
|
|
|
|
func (n *Network) UpdateNumValidatorsForHeight(num int, height int64) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if n.Height <= height {
|
|
n.NumValidators = num
|
|
}
|
|
}
|
|
|
|
func (n *Network) GetHealthString() string {
|
|
switch n.Health {
|
|
case FullHealth:
|
|
return "full"
|
|
case ModerateHealth:
|
|
return "moderate"
|
|
case Dead:
|
|
return "dead"
|
|
default:
|
|
return "undefined"
|
|
}
|
|
}
|
|
|
|
// Uptime returns network's uptime in percentages.
|
|
func (n *Network) Uptime() float64 {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
return n.UptimeData.Uptime
|
|
}
|
|
|
|
// StartTime returns time we started monitoring.
|
|
func (n *Network) StartTime() time.Time {
|
|
return n.UptimeData.StartTime
|
|
}
|