github.com/evdatsion/aphelion-dpos-bft@v0.32.1/tools/tm-monitor/eventmeter/eventmeter.go (about) 1 // eventmeter - generic system to subscribe to events and record their frequency. 2 package eventmeter 3 4 import ( 5 "context" 6 "encoding/json" 7 "fmt" 8 "sync" 9 "time" 10 11 metrics "github.com/rcrowley/go-metrics" 12 13 "github.com/evdatsion/aphelion-dpos-bft/libs/events" 14 "github.com/evdatsion/aphelion-dpos-bft/libs/log" 15 client "github.com/evdatsion/aphelion-dpos-bft/rpc/lib/client" 16 ) 17 18 const ( 19 // Get ping/pong latency and call LatencyCallbackFunc with this period. 20 latencyPeriod = 1 * time.Second 21 22 // Check if the WS client is connected every 23 connectionCheckPeriod = 100 * time.Millisecond 24 ) 25 26 // EventMetric exposes metrics for an event. 27 type EventMetric struct { 28 ID string `json:"id"` 29 Started time.Time `json:"start_time"` 30 LastHeard time.Time `json:"last_heard"` 31 MinDuration int64 `json:"min_duration"` 32 MaxDuration int64 `json:"max_duration"` 33 34 // tracks event count and rate 35 meter metrics.Meter 36 37 // filled in from the Meter 38 Count int64 `json:"count"` 39 Rate1 float64 `json:"rate_1" amino:"unsafe"` 40 Rate5 float64 `json:"rate_5" amino:"unsafe"` 41 Rate15 float64 `json:"rate_15" amino:"unsafe"` 42 RateMean float64 `json:"rate_mean" amino:"unsafe"` 43 44 // so the event can have effects in the eventmeter's consumer. runs in a go 45 // routine. 46 callback EventCallbackFunc 47 } 48 49 func (metric *EventMetric) Copy() *EventMetric { 50 metricCopy := *metric 51 metricCopy.meter = metric.meter.Snapshot() 52 return &metricCopy 53 } 54 55 // called on GetMetric 56 func (metric *EventMetric) fillMetric() *EventMetric { 57 metric.Count = metric.meter.Count() 58 metric.Rate1 = metric.meter.Rate1() 59 metric.Rate5 = metric.meter.Rate5() 60 metric.Rate15 = metric.meter.Rate15() 61 metric.RateMean = metric.meter.RateMean() 62 return metric 63 } 64 65 // EventCallbackFunc is a closure to enable side effects from receiving an 66 // event. 67 type EventCallbackFunc func(em *EventMetric, data interface{}) 68 69 // EventUnmarshalFunc is a closure to get the query and data out of the raw 70 // JSON received over the RPC WebSocket. 71 type EventUnmarshalFunc func(b json.RawMessage) (string, events.EventData, error) 72 73 // LatencyCallbackFunc is a closure to enable side effects from receiving a latency. 74 type LatencyCallbackFunc func(meanLatencyNanoSeconds float64) 75 76 // DisconnectCallbackFunc is a closure to notify a consumer that the connection 77 // has died. 78 type DisconnectCallbackFunc func() 79 80 // EventMeter tracks events, reports latency and disconnects. 81 type EventMeter struct { 82 wsc *client.WSClient 83 84 mtx sync.Mutex 85 queryToMetricMap map[string]*EventMetric 86 87 unmarshalEvent EventUnmarshalFunc 88 latencyCallback LatencyCallbackFunc 89 disconnectCallback DisconnectCallbackFunc 90 subscribed bool 91 92 quit chan struct{} 93 94 logger log.Logger 95 } 96 97 func NewEventMeter(addr string, unmarshalEvent EventUnmarshalFunc) *EventMeter { 98 return &EventMeter{ 99 wsc: client.NewWSClient(addr, "/websocket", client.PingPeriod(1*time.Second)), 100 queryToMetricMap: make(map[string]*EventMetric), 101 unmarshalEvent: unmarshalEvent, 102 logger: log.NewNopLogger(), 103 } 104 } 105 106 // SetLogger lets you set your own logger. 107 func (em *EventMeter) SetLogger(l log.Logger) { 108 em.logger = l 109 em.wsc.SetLogger(l.With("module", "rpcclient")) 110 } 111 112 // String returns a string representation of event meter. 113 func (em *EventMeter) String() string { 114 return em.wsc.Address 115 } 116 117 // Start boots up event meter. 118 func (em *EventMeter) Start() error { 119 if err := em.wsc.Start(); err != nil { 120 return err 121 } 122 123 em.quit = make(chan struct{}) 124 go em.receiveRoutine() 125 go em.disconnectRoutine() 126 127 err := em.subscribe() 128 if err != nil { 129 return err 130 } 131 em.subscribed = true 132 return nil 133 } 134 135 // Stop stops event meter. 136 func (em *EventMeter) Stop() { 137 close(em.quit) 138 139 if em.wsc.IsRunning() { 140 em.wsc.Stop() 141 } 142 } 143 144 // Subscribe for the given query. Callback function will be called upon 145 // receiving an event. 146 func (em *EventMeter) Subscribe(query string, cb EventCallbackFunc) error { 147 em.mtx.Lock() 148 defer em.mtx.Unlock() 149 150 if err := em.wsc.Subscribe(context.TODO(), query); err != nil { 151 return err 152 } 153 154 metric := &EventMetric{ 155 meter: metrics.NewMeter(), 156 callback: cb, 157 } 158 em.queryToMetricMap[query] = metric 159 return nil 160 } 161 162 // Unsubscribe from the given query. 163 func (em *EventMeter) Unsubscribe(query string) error { 164 em.mtx.Lock() 165 defer em.mtx.Unlock() 166 167 return em.wsc.Unsubscribe(context.TODO(), query) 168 } 169 170 // GetMetric fills in the latest data for an query and return a copy. 171 func (em *EventMeter) GetMetric(query string) (*EventMetric, error) { 172 em.mtx.Lock() 173 defer em.mtx.Unlock() 174 metric, ok := em.queryToMetricMap[query] 175 if !ok { 176 return nil, fmt.Errorf("unknown query: %s", query) 177 } 178 return metric.fillMetric().Copy(), nil 179 } 180 181 // RegisterLatencyCallback allows you to set latency callback. 182 func (em *EventMeter) RegisterLatencyCallback(f LatencyCallbackFunc) { 183 em.mtx.Lock() 184 defer em.mtx.Unlock() 185 em.latencyCallback = f 186 } 187 188 // RegisterDisconnectCallback allows you to set disconnect callback. 189 func (em *EventMeter) RegisterDisconnectCallback(f DisconnectCallbackFunc) { 190 em.mtx.Lock() 191 defer em.mtx.Unlock() 192 em.disconnectCallback = f 193 } 194 195 /////////////////////////////////////////////////////////////////////////////// 196 // Private 197 198 func (em *EventMeter) subscribe() error { 199 for query := range em.queryToMetricMap { 200 if err := em.wsc.Subscribe(context.TODO(), query); err != nil { 201 return err 202 } 203 } 204 return nil 205 } 206 207 func (em *EventMeter) receiveRoutine() { 208 latencyTicker := time.NewTicker(latencyPeriod) 209 for { 210 select { 211 case resp := <-em.wsc.ResponsesCh: 212 if resp.Error != nil { 213 em.logger.Error("expected some event, got error", "err", resp.Error.Error()) 214 continue 215 } 216 query, data, err := em.unmarshalEvent(resp.Result) 217 if err != nil { 218 em.logger.Error("failed to unmarshal event", "err", err) 219 continue 220 } 221 if query != "" { // FIXME how can it be an empty string? 222 em.updateMetric(query, data) 223 } 224 case <-latencyTicker.C: 225 if em.wsc.IsActive() { 226 em.callLatencyCallback(em.wsc.PingPongLatencyTimer.Mean()) 227 } 228 case <-em.wsc.Quit(): 229 return 230 case <-em.quit: 231 return 232 } 233 } 234 } 235 236 func (em *EventMeter) disconnectRoutine() { 237 ticker := time.NewTicker(connectionCheckPeriod) 238 for { 239 select { 240 case <-ticker.C: 241 if em.wsc.IsReconnecting() && em.subscribed { // notify user about disconnect only once 242 em.callDisconnectCallback() 243 em.subscribed = false 244 } else if !em.wsc.IsReconnecting() && !em.subscribed { // resubscribe 245 em.subscribe() 246 em.subscribed = true 247 } 248 case <-em.wsc.Quit(): 249 return 250 case <-em.quit: 251 return 252 } 253 } 254 } 255 256 func (em *EventMeter) updateMetric(query string, data events.EventData) { 257 em.mtx.Lock() 258 defer em.mtx.Unlock() 259 260 metric, ok := em.queryToMetricMap[query] 261 if !ok { 262 // we already unsubscribed, or got an unexpected query 263 return 264 } 265 266 last := metric.LastHeard 267 metric.LastHeard = time.Now() 268 metric.meter.Mark(1) 269 dur := int64(metric.LastHeard.Sub(last)) 270 if dur < metric.MinDuration { 271 metric.MinDuration = dur 272 } 273 if !last.IsZero() && dur > metric.MaxDuration { 274 metric.MaxDuration = dur 275 } 276 277 if metric.callback != nil { 278 go metric.callback(metric.Copy(), data) 279 } 280 } 281 282 func (em *EventMeter) callDisconnectCallback() { 283 em.mtx.Lock() 284 if em.disconnectCallback != nil { 285 go em.disconnectCallback() 286 } 287 em.mtx.Unlock() 288 } 289 290 func (em *EventMeter) callLatencyCallback(meanLatencyNanoSeconds float64) { 291 em.mtx.Lock() 292 if em.latencyCallback != nil { 293 go em.latencyCallback(meanLatencyNanoSeconds) 294 } 295 em.mtx.Unlock() 296 }