@ -1,225 +0,0 @@ | |||||
package main | |||||
import ( | |||||
"bytes" | |||||
"encoding/json" | |||||
"fmt" | |||||
"os" | |||||
"reflect" | |||||
"sync" | |||||
"time" | |||||
. "github.com/tendermint/go-common" | |||||
"github.com/tendermint/go-crypto" | |||||
// register rpc and event types with go- | |||||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||||
// "github.com/tendermint/tendermint/types" | |||||
client "github.com/tendermint/tendermint/rpc/client" | |||||
"github.com/gorilla/websocket" | |||||
"github.com/rcrowley/go-metrics" | |||||
) | |||||
//------------------------------------------------------ | |||||
// Connect to all validators for a blockchain | |||||
type Blockchain struct { | |||||
ID string | |||||
Validators []Validator | |||||
} | |||||
type Validator struct { | |||||
ID string | |||||
PubKey crypto.PubKey | |||||
IP string | |||||
Port int | |||||
} | |||||
//------------------------------------------------------ | |||||
// Generic system to subscribe to events and record their frequency | |||||
// Metrics for a given event | |||||
type EventMetric struct { | |||||
ID string `json:"id"` | |||||
Started time.Time `json:"start_time"` | |||||
LastHeard time.Time `json:"last_heard"` | |||||
MinDuration int64 `json:"min_duration"` | |||||
MaxDuration int64 `json:"max_duration"` | |||||
// tracks event count and rate | |||||
meter metrics.Meter | |||||
// filled in from the Meter | |||||
Count int64 `json:"count"` | |||||
Rate1 float64 `json:"rate_1"` | |||||
Rate5 float64 `json:"rate_5"` | |||||
Rate15 float64 `json:"rate_15"` | |||||
RateMean float64 `json:"rate_mean"` | |||||
// XXX: move this | |||||
// latency for node itself (not related to event) | |||||
Latency float64 `json:"latency_mean"` | |||||
} | |||||
// Each node gets an event meter to track events for that node | |||||
type EventMeter struct { | |||||
QuitService | |||||
wsc *client.WSClient | |||||
mtx sync.Mutex | |||||
events map[string]*EventMetric | |||||
// to record latency | |||||
timer metrics.Timer | |||||
lastPing time.Time | |||||
receivedPong bool | |||||
} | |||||
func NewEventMeter(addr string) *EventMeter { | |||||
em := &EventMeter{ | |||||
wsc: client.NewWSClient(addr), | |||||
events: make(map[string]*EventMetric), | |||||
timer: metrics.NewTimer(), | |||||
receivedPong: true, | |||||
} | |||||
em.QuitService = *NewQuitService(nil, "EventMeter", em) | |||||
return em | |||||
} | |||||
func (em *EventMeter) OnStart() error { | |||||
em.QuitService.OnStart() | |||||
if err := em.wsc.OnStart(); err != nil { | |||||
return err | |||||
} | |||||
em.wsc.Conn.SetPongHandler(func(m string) error { | |||||
// NOTE: https://github.com/gorilla/websocket/issues/97 | |||||
em.mtx.Lock() | |||||
defer em.mtx.Unlock() | |||||
em.receivedPong = true | |||||
em.timer.UpdateSince(em.lastPing) | |||||
return nil | |||||
}) | |||||
go em.receiveRoutine() | |||||
return nil | |||||
} | |||||
func (em *EventMeter) OnStop() { | |||||
em.wsc.OnStop() | |||||
em.QuitService.OnStop() | |||||
} | |||||
func (em *EventMeter) Subscribe(eventid string) error { | |||||
em.mtx.Lock() | |||||
defer em.mtx.Unlock() | |||||
if _, ok := em.events[eventid]; ok { | |||||
return fmt.Errorf("Subscription already exists") | |||||
} | |||||
if err := em.wsc.Subscribe(eventid); err != nil { | |||||
return err | |||||
} | |||||
em.events[eventid] = &EventMetric{ | |||||
Started: time.Now(), | |||||
MinDuration: 1 << 62, | |||||
meter: metrics.NewMeter(), | |||||
} | |||||
return nil | |||||
} | |||||
func (em *EventMeter) Unsubscribe(eventid string) error { | |||||
em.mtx.Lock() | |||||
defer em.mtx.Unlock() | |||||
if err := em.wsc.Unsubscribe(eventid); err != nil { | |||||
return err | |||||
} | |||||
// XXX: should we persist or save this info first? | |||||
delete(em.events, eventid) | |||||
return nil | |||||
} | |||||
//------------------------------------------------------ | |||||
func (em *EventMeter) receiveRoutine() { | |||||
logTicker := time.NewTicker(time.Second * 3) | |||||
pingTicker := time.NewTicker(time.Second * 1) | |||||
for { | |||||
select { | |||||
case <-logTicker.C: | |||||
em.mtx.Lock() | |||||
for _, metric := range em.events { | |||||
metric.Count = metric.meter.Count() | |||||
metric.Rate1 = metric.meter.Rate1() | |||||
metric.Rate5 = metric.meter.Rate5() | |||||
metric.Rate15 = metric.meter.Rate15() | |||||
metric.RateMean = metric.meter.RateMean() | |||||
metric.Latency = em.timer.Mean() | |||||
b, err := json.Marshal(metric) | |||||
if err != nil { | |||||
// TODO | |||||
log.Error(err.Error()) | |||||
continue | |||||
} | |||||
var out bytes.Buffer | |||||
json.Indent(&out, b, "", "\t") | |||||
out.WriteTo(os.Stdout) | |||||
} | |||||
em.mtx.Unlock() | |||||
case <-pingTicker.C: | |||||
em.mtx.Lock() | |||||
// ping to record latency | |||||
if !em.receivedPong { | |||||
// XXX: why is the pong taking so long? should we stop the conn? | |||||
em.mtx.Unlock() | |||||
continue | |||||
} | |||||
em.lastPing = time.Now() | |||||
em.receivedPong = false | |||||
err := em.wsc.Conn.WriteMessage(websocket.PingMessage, []byte{}) | |||||
if err != nil { | |||||
log.Error("Failed to write ping message on websocket", "error", err) | |||||
em.wsc.Stop() | |||||
return | |||||
} | |||||
em.mtx.Unlock() | |||||
case r := <-em.wsc.ResultsCh: | |||||
em.mtx.Lock() | |||||
switch r := r.(type) { | |||||
case *ctypes.ResultEvent: | |||||
id, _ := r.Event, r.Data | |||||
metric, ok := em.events[id] | |||||
if !ok { | |||||
// we already unsubscribed, or got an unexpected event | |||||
continue | |||||
} | |||||
last := metric.LastHeard | |||||
metric.LastHeard = time.Now() | |||||
metric.meter.Mark(1) | |||||
dur := int64(metric.LastHeard.Sub(last)) | |||||
if dur < metric.MinDuration { | |||||
metric.MinDuration = dur | |||||
} | |||||
if !last.IsZero() && dur > metric.MaxDuration { | |||||
metric.MaxDuration = dur | |||||
} | |||||
default: | |||||
log.Error("Unknown result event type", "type", reflect.TypeOf(r)) | |||||
} | |||||
em.mtx.Unlock() | |||||
case <-em.Quit: | |||||
break | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,152 @@ | |||||
package handlers | |||||
import ( | |||||
"fmt" | |||||
"sort" | |||||
"sync" | |||||
"github.com/tendermint/go-event-meter" | |||||
"github.com/tendermint/go-wire" | |||||
"github.com/tendermint/netmon/types" | |||||
) | |||||
type NetMonResultInterface interface{} | |||||
type NetMonResult struct { | |||||
Result NetMonResultInterface | |||||
} | |||||
// for wire.readReflect | |||||
var _ = wire.RegisterInterface( | |||||
struct{ NetMonResultInterface }{}, | |||||
wire.ConcreteType{&types.ChainAndValidatorIDs{}, 0x01}, | |||||
wire.ConcreteType{&types.ChainStatus{}, 0x02}, | |||||
wire.ConcreteType{&types.Validator{}, 0x03}, | |||||
wire.ConcreteType{&eventmeter.EventMetric{}, 0x04}, | |||||
) | |||||
//--------------------------------------------- | |||||
// global state and backend functions | |||||
type TendermintNetwork struct { | |||||
mtx sync.Mutex | |||||
Chains map[string]*types.ChainStatus `json:"blockchains"` | |||||
ValSets map[string]*types.ValidatorSet `json:"validator_sets"` | |||||
} | |||||
// TODO: populate validator sets | |||||
func NewTendermintNetwork(chains ...*types.ChainStatus) *TendermintNetwork { | |||||
network := &TendermintNetwork{ | |||||
Chains: make(map[string]*types.ChainStatus), | |||||
ValSets: make(map[string]*types.ValidatorSet), | |||||
} | |||||
for _, chain := range chains { | |||||
network.Chains[chain.Config.ID] = chain | |||||
} | |||||
return network | |||||
} | |||||
//------------ | |||||
// RPC funcs | |||||
func (tn *TendermintNetwork) Status() (*types.ChainAndValidatorIDs, error) { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
chains := make([]string, len(tn.Chains)) | |||||
valSets := make([]string, len(tn.ValSets)) | |||||
i := 0 | |||||
for chain, _ := range tn.Chains { | |||||
chains[i] = chain | |||||
i += 1 | |||||
} | |||||
i = 0 | |||||
for valset, _ := range tn.ValSets { | |||||
valSets[i] = valset | |||||
i += 1 | |||||
} | |||||
sort.StringSlice(chains).Sort() | |||||
sort.StringSlice(valSets).Sort() | |||||
return &types.ChainAndValidatorIDs{ | |||||
ChainIDs: chains, | |||||
ValidatorSetIDs: valSets, | |||||
}, nil | |||||
} | |||||
func (tn *TendermintNetwork) GetChain(chainID string) (*types.ChainStatus, error) { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
chain, ok := tn.Chains[chainID] | |||||
if !ok { | |||||
return nil, fmt.Errorf("Unknown chain %s", chainID) | |||||
} | |||||
return chain, nil | |||||
} | |||||
func (tn *TendermintNetwork) GetValidatorSet(valSetID string) (*types.ValidatorSet, error) { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
valSet, ok := tn.ValSets[valSetID] | |||||
if !ok { | |||||
return nil, fmt.Errorf("Unknown validator set %s", valSetID) | |||||
} | |||||
return valSet, nil | |||||
} | |||||
func (tn *TendermintNetwork) GetValidator(valSetID, valID string) (*types.Validator, error) { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
valSet, ok := tn.ValSets[valSetID] | |||||
if !ok { | |||||
return nil, fmt.Errorf("Unknown validator set %s", valSetID) | |||||
} | |||||
val, err := valSet.Validator(valID) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return val, nil | |||||
} | |||||
func (tn *TendermintNetwork) StartMeter(chainID, valID, eventID string) error { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
val, err := tn.getChainVal(chainID, valID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return val.EventMeter().Subscribe(eventID, nil) | |||||
} | |||||
func (tn *TendermintNetwork) StopMeter(chainID, valID, eventID string) error { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
val, err := tn.getChainVal(chainID, valID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
return val.EventMeter().Unsubscribe(eventID) | |||||
} | |||||
func (tn *TendermintNetwork) GetMeter(chainID, valID, eventID string) (*eventmeter.EventMetric, error) { | |||||
tn.mtx.Lock() | |||||
defer tn.mtx.Unlock() | |||||
val, err := tn.getChainVal(chainID, valID) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return val.EventMeter().GetMetric(eventID) | |||||
} | |||||
func (tn *TendermintNetwork) getChainVal(chainID, valID string) (*types.ChainValidator, error) { | |||||
chain, ok := tn.Chains[chainID] | |||||
if !ok { | |||||
return nil, fmt.Errorf("Unknown chain %s", chainID) | |||||
} | |||||
val, err := chain.Config.GetValidatorByID(valID) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return val, nil | |||||
} |
@ -0,0 +1,77 @@ | |||||
package handlers | |||||
import ( | |||||
rpc "github.com/tendermint/go-rpc/server" | |||||
) | |||||
func Routes(network *TendermintNetwork) map[string]*rpc.RPCFunc { | |||||
return map[string]*rpc.RPCFunc{ | |||||
// subscribe/unsubscribe are reserved for websocket events. | |||||
// "subscribe": rpc.NewWSRPCFunc(Subscribe, []string{"event"}), | |||||
// "unsubscribe": rpc.NewWSRPCFunc(Unsubscribe, []string{"event"}), | |||||
"status": rpc.NewRPCFunc(StatusResult(network), ""), | |||||
"blockchain": rpc.NewRPCFunc(GetChainResult(network), "chain"), | |||||
"validator_set": rpc.NewRPCFunc(GetValidatorSetResult(network), "valsetID"), | |||||
"validator": rpc.NewRPCFunc(GetValidatorResult(network), "valSetID,valID"), | |||||
"start_meter": rpc.NewRPCFunc(network.StartMeter, "chainID,valID,event"), | |||||
"stop_meter": rpc.NewRPCFunc(network.StopMeter, "chainID,valID,event"), | |||||
"meter": rpc.NewRPCFunc(GetMeterResult(network), "chainID,valID,event"), | |||||
} | |||||
} | |||||
func StatusResult(network *TendermintNetwork) interface{} { | |||||
return func() (*NetMonResult, error) { | |||||
r, err := network.Status() | |||||
if err != nil { | |||||
return nil, err | |||||
} else { | |||||
return &NetMonResult{r}, nil | |||||
} | |||||
} | |||||
} | |||||
func GetChainResult(network *TendermintNetwork) interface{} { | |||||
return func(chain string) (*NetMonResult, error) { | |||||
r, err := network.GetChain(chain) | |||||
if err != nil { | |||||
return nil, err | |||||
} else { | |||||
return &NetMonResult{r}, nil | |||||
} | |||||
} | |||||
} | |||||
func GetValidatorSetResult(network *TendermintNetwork) interface{} { | |||||
return func(valSetID string) (*NetMonResult, error) { | |||||
r, err := network.GetValidatorSet(valSetID) | |||||
if err != nil { | |||||
return nil, err | |||||
} else { | |||||
return &NetMonResult{r}, nil | |||||
} | |||||
} | |||||
} | |||||
func GetValidatorResult(network *TendermintNetwork) interface{} { | |||||
return func(valSetID, valID string) (*NetMonResult, error) { | |||||
r, err := network.GetValidator(valSetID, valID) | |||||
if err != nil { | |||||
return nil, err | |||||
} else { | |||||
return &NetMonResult{r}, nil | |||||
} | |||||
} | |||||
} | |||||
func GetMeterResult(network *TendermintNetwork) interface{} { | |||||
return func(chainID, valID, eventID string) (*NetMonResult, error) { | |||||
r, err := network.GetMeter(chainID, valID, eventID) | |||||
if err != nil { | |||||
return nil, err | |||||
} else { | |||||
return &NetMonResult{r}, nil | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,119 @@ | |||||
package types | |||||
import ( | |||||
"fmt" | |||||
"sync" | |||||
"github.com/tendermint/go-event-meter" | |||||
"github.com/tendermint/go-crypto" | |||||
) | |||||
//--------------------------------------------- | |||||
// core types | |||||
// Known chain and validator set IDs (from which anything else can be found) | |||||
type ChainAndValidatorIDs struct { | |||||
ChainIDs []string `json:"chain_ids"` | |||||
ValidatorSetIDs []string `json:"validator_set_ids"` | |||||
} | |||||
// state of a chain | |||||
type ChainStatus struct { | |||||
Config *BlockchainConfig `json:"config"` | |||||
Status *BlockchainStatus `json:"status"` | |||||
} | |||||
// basic chain config | |||||
// threadsafe | |||||
type BlockchainConfig struct { | |||||
mtx sync.Mutex | |||||
ID string `json:"id"` | |||||
ValSetID string `json:"val_set_id"` | |||||
Validators []*ChainValidator `json:"validators"` | |||||
valIDMap map[string]int // map IDs to indices | |||||
} | |||||
func (bc *BlockchainConfig) PopulateValIDMap() { | |||||
bc.mtx.Lock() | |||||
defer bc.mtx.Unlock() | |||||
bc.valIDMap = make(map[string]int) | |||||
for i, v := range bc.Validators { | |||||
bc.valIDMap[v.ID] = i | |||||
} | |||||
} | |||||
func (bc *BlockchainConfig) GetValidatorByID(valID string) (*ChainValidator, error) { | |||||
bc.mtx.Lock() | |||||
defer bc.mtx.Unlock() | |||||
valIndex, ok := bc.valIDMap[valID] | |||||
if !ok { | |||||
return nil, fmt.Errorf("Unknown validator %s", valID) | |||||
} | |||||
return bc.Validators[valIndex], nil | |||||
} | |||||
// basic chain status/metrics | |||||
// threadsafe | |||||
type BlockchainStatus struct { | |||||
mtx sync.Mutex | |||||
Height int `json:"height"` | |||||
MeanBlockTime float64 `json:"mean_block_time"` | |||||
TxThroughput float64 `json:"tx_throughput"` | |||||
BlockchainSize int64 `json:"blockchain_size"` // how might we get StateSize ? | |||||
} | |||||
// validator on a chain | |||||
type ChainValidator struct { | |||||
*Validator | |||||
Addr string `json:"addr"` // do we want multiple addrs? | |||||
Index int `json:"index"` | |||||
em *eventmeter.EventMeter // holds a ws connection to the val | |||||
Latency float64 `json:"latency,omitempty"` | |||||
} | |||||
func (cv *ChainValidator) NewEventMeter() error { | |||||
em := eventmeter.NewEventMeter(fmt.Sprintf("ws://%s/websocket", cv.Addr)) | |||||
if err := em.Start(); err != nil { | |||||
return err | |||||
} | |||||
cv.em = em | |||||
return nil | |||||
} | |||||
func (cv *ChainValidator) EventMeter() *eventmeter.EventMeter { | |||||
return cv.em | |||||
} | |||||
// validator set (independent of chains) | |||||
type ValidatorSet struct { | |||||
Validators []*Validator `json:"validators"` | |||||
} | |||||
func (vs *ValidatorSet) Validator(valID string) (*Validator, error) { | |||||
for _, v := range vs.Validators { | |||||
if v.ID == valID { | |||||
return v, nil | |||||
} | |||||
} | |||||
return nil, fmt.Errorf("Unknwon validator %s", valID) | |||||
} | |||||
// validator (independent of chain) | |||||
type Validator struct { | |||||
ID string `json:"id"` | |||||
PubKey crypto.PubKey `json:"pub_key,omitempty"` | |||||
Chains []*ChainStatus `json:"chains,omitempty"` | |||||
} | |||||
func (v *Validator) Chain(chainID string) (*ChainStatus, error) { | |||||
for _, c := range v.Chains { | |||||
if c.Config.ID == chainID { | |||||
return c, nil | |||||
} | |||||
} | |||||
return nil, fmt.Errorf("Unknwon chain %s", chainID) | |||||
} |