github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/ethstats/ethstats.go (about) 1 // This file is part of the go-sberex library. The go-sberex library is 2 // free software: you can redistribute it and/or modify it under the terms 3 // of the GNU Lesser General Public License as published by the Free 4 // Software Foundation, either version 3 of the License, or (at your option) 5 // any later version. 6 // 7 // The go-sberex library is distributed in the hope that it will be useful, 8 // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License <http://www.gnu.org/licenses/> for more details. 11 12 // Package ethstats implements the network stats reporting service. 13 package ethstats 14 15 import ( 16 "context" 17 "encoding/json" 18 "errors" 19 "fmt" 20 "math/big" 21 "net" 22 "regexp" 23 "runtime" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/Sberex/go-sberex/common" 29 "github.com/Sberex/go-sberex/common/mclock" 30 "github.com/Sberex/go-sberex/consensus" 31 "github.com/Sberex/go-sberex/core" 32 "github.com/Sberex/go-sberex/core/types" 33 "github.com/Sberex/go-sberex/eth" 34 "github.com/Sberex/go-sberex/event" 35 "github.com/Sberex/go-sberex/les" 36 "github.com/Sberex/go-sberex/log" 37 "github.com/Sberex/go-sberex/p2p" 38 "github.com/Sberex/go-sberex/rpc" 39 "golang.org/x/net/websocket" 40 ) 41 42 const ( 43 // historyUpdateRange is the number of blocks a node should report upon login or 44 // history request. 45 historyUpdateRange = 50 46 47 // txChanSize is the size of channel listening to TxPreEvent. 48 // The number is referenced from the size of tx pool. 49 txChanSize = 4096 50 // chainHeadChanSize is the size of channel listening to ChainHeadEvent. 51 chainHeadChanSize = 10 52 ) 53 54 type txPool interface { 55 // SubscribeTxPreEvent should return an event subscription of 56 // TxPreEvent and send events to the given channel. 57 SubscribeTxPreEvent(chan<- core.TxPreEvent) event.Subscription 58 } 59 60 type blockChain interface { 61 SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription 62 } 63 64 // Service implements an Sberex netstats reporting daemon that pushes local 65 // chain statistics up to a monitoring server. 66 type Service struct { 67 server *p2p.Server // Peer-to-peer server to retrieve networking infos 68 eth *eth.Sberex // Full Sberex service if monitoring a full node 69 les *les.LightSberex // Light Sberex service if monitoring a light node 70 engine consensus.Engine // Consensus engine to retrieve variadic block fields 71 72 node string // Name of the node to display on the monitoring page 73 pass string // Password to authorize access to the monitoring page 74 host string // Remote address of the monitoring service 75 76 pongCh chan struct{} // Pong notifications are fed into this channel 77 histCh chan []uint64 // History request block numbers are fed into this channel 78 } 79 80 // New returns a monitoring service ready for stats reporting. 81 func New(url string, ethServ *eth.Sberex, lesServ *les.LightSberex) (*Service, error) { 82 // Parse the netstats connection url 83 re := regexp.MustCompile("([^:@]*)(:([^@]*))?@(.+)") 84 parts := re.FindStringSubmatch(url) 85 if len(parts) != 5 { 86 return nil, fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url) 87 } 88 // Assemble and return the stats service 89 var engine consensus.Engine 90 if ethServ != nil { 91 engine = ethServ.Engine() 92 } else { 93 engine = lesServ.Engine() 94 } 95 return &Service{ 96 eth: ethServ, 97 les: lesServ, 98 engine: engine, 99 node: parts[1], 100 pass: parts[3], 101 host: parts[4], 102 pongCh: make(chan struct{}), 103 histCh: make(chan []uint64, 1), 104 }, nil 105 } 106 107 // Protocols implements node.Service, returning the P2P network protocols used 108 // by the stats service (nil as it doesn't use the devp2p overlay network). 109 func (s *Service) Protocols() []p2p.Protocol { return nil } 110 111 // APIs implements node.Service, returning the RPC API endpoints provided by the 112 // stats service (nil as it doesn't provide any user callable APIs). 113 func (s *Service) APIs() []rpc.API { return nil } 114 115 // Start implements node.Service, starting up the monitoring and reporting daemon. 116 func (s *Service) Start(server *p2p.Server) error { 117 s.server = server 118 go s.loop() 119 120 log.Info("Stats daemon started") 121 return nil 122 } 123 124 // Stop implements node.Service, terminating the monitoring and reporting daemon. 125 func (s *Service) Stop() error { 126 log.Info("Stats daemon stopped") 127 return nil 128 } 129 130 // loop keeps trying to connect to the netstats server, reporting chain events 131 // until termination. 132 func (s *Service) loop() { 133 // Subscribe to chain events to execute updates on 134 var blockchain blockChain 135 var txpool txPool 136 if s.eth != nil { 137 blockchain = s.eth.BlockChain() 138 txpool = s.eth.TxPool() 139 } else { 140 blockchain = s.les.BlockChain() 141 txpool = s.les.TxPool() 142 } 143 144 chainHeadCh := make(chan core.ChainHeadEvent, chainHeadChanSize) 145 headSub := blockchain.SubscribeChainHeadEvent(chainHeadCh) 146 defer headSub.Unsubscribe() 147 148 txEventCh := make(chan core.TxPreEvent, txChanSize) 149 txSub := txpool.SubscribeTxPreEvent(txEventCh) 150 defer txSub.Unsubscribe() 151 152 // Start a goroutine that exhausts the subsciptions to avoid events piling up 153 var ( 154 quitCh = make(chan struct{}) 155 headCh = make(chan *types.Block, 1) 156 txCh = make(chan struct{}, 1) 157 ) 158 go func() { 159 var lastTx mclock.AbsTime 160 161 HandleLoop: 162 for { 163 select { 164 // Notify of chain head events, but drop if too frequent 165 case head := <-chainHeadCh: 166 select { 167 case headCh <- head.Block: 168 default: 169 } 170 171 // Notify of new transaction events, but drop if too frequent 172 case <-txEventCh: 173 if time.Duration(mclock.Now()-lastTx) < time.Second { 174 continue 175 } 176 lastTx = mclock.Now() 177 178 select { 179 case txCh <- struct{}{}: 180 default: 181 } 182 183 // node stopped 184 case <-txSub.Err(): 185 break HandleLoop 186 case <-headSub.Err(): 187 break HandleLoop 188 } 189 } 190 close(quitCh) 191 }() 192 // Loop reporting until termination 193 for { 194 // Resolve the URL, defaulting to TLS, but falling back to none too 195 path := fmt.Sprintf("%s/api", s.host) 196 urls := []string{path} 197 198 if !strings.Contains(path, "://") { // url.Parse and url.IsAbs is unsuitable (https://github.com/golang/go/issues/19779) 199 urls = []string{"wss://" + path, "ws://" + path} 200 } 201 // Establish a websocket connection to the server on any supported URL 202 var ( 203 conf *websocket.Config 204 conn *websocket.Conn 205 err error 206 ) 207 for _, url := range urls { 208 if conf, err = websocket.NewConfig(url, "http://localhost/"); err != nil { 209 continue 210 } 211 conf.Dialer = &net.Dialer{Timeout: 5 * time.Second} 212 if conn, err = websocket.DialConfig(conf); err == nil { 213 break 214 } 215 } 216 if err != nil { 217 log.Warn("Stats server unreachable", "err", err) 218 time.Sleep(10 * time.Second) 219 continue 220 } 221 // Authenticate the client with the server 222 if err = s.login(conn); err != nil { 223 log.Warn("Stats login failed", "err", err) 224 conn.Close() 225 time.Sleep(10 * time.Second) 226 continue 227 } 228 go s.readLoop(conn) 229 230 // Send the initial stats so our node looks decent from the get go 231 if err = s.report(conn); err != nil { 232 log.Warn("Initial stats report failed", "err", err) 233 conn.Close() 234 continue 235 } 236 // Keep sending status updates until the connection breaks 237 fullReport := time.NewTicker(15 * time.Second) 238 239 for err == nil { 240 select { 241 case <-quitCh: 242 conn.Close() 243 return 244 245 case <-fullReport.C: 246 if err = s.report(conn); err != nil { 247 log.Warn("Full stats report failed", "err", err) 248 } 249 case list := <-s.histCh: 250 if err = s.reportHistory(conn, list); err != nil { 251 log.Warn("Requested history report failed", "err", err) 252 } 253 case head := <-headCh: 254 if err = s.reportBlock(conn, head); err != nil { 255 log.Warn("Block stats report failed", "err", err) 256 } 257 if err = s.reportPending(conn); err != nil { 258 log.Warn("Post-block transaction stats report failed", "err", err) 259 } 260 case <-txCh: 261 if err = s.reportPending(conn); err != nil { 262 log.Warn("Transaction stats report failed", "err", err) 263 } 264 } 265 } 266 // Make sure the connection is closed 267 conn.Close() 268 } 269 } 270 271 // readLoop loops as long as the connection is alive and retrieves data packets 272 // from the network socket. If any of them match an active request, it forwards 273 // it, if they themselves are requests it initiates a reply, and lastly it drops 274 // unknown packets. 275 func (s *Service) readLoop(conn *websocket.Conn) { 276 // If the read loop exists, close the connection 277 defer conn.Close() 278 279 for { 280 // Retrieve the next generic network packet and bail out on error 281 var msg map[string][]interface{} 282 if err := websocket.JSON.Receive(conn, &msg); err != nil { 283 log.Warn("Failed to decode stats server message", "err", err) 284 return 285 } 286 log.Trace("Received message from stats server", "msg", msg) 287 if len(msg["emit"]) == 0 { 288 log.Warn("Stats server sent non-broadcast", "msg", msg) 289 return 290 } 291 command, ok := msg["emit"][0].(string) 292 if !ok { 293 log.Warn("Invalid stats server message type", "type", msg["emit"][0]) 294 return 295 } 296 // If the message is a ping reply, deliver (someone must be listening!) 297 if len(msg["emit"]) == 2 && command == "node-pong" { 298 select { 299 case s.pongCh <- struct{}{}: 300 // Pong delivered, continue listening 301 continue 302 default: 303 // Ping routine dead, abort 304 log.Warn("Stats server pinger seems to have died") 305 return 306 } 307 } 308 // If the message is a history request, forward to the event processor 309 if len(msg["emit"]) == 2 && command == "history" { 310 // Make sure the request is valid and doesn't crash us 311 request, ok := msg["emit"][1].(map[string]interface{}) 312 if !ok { 313 log.Warn("Invalid stats history request", "msg", msg["emit"][1]) 314 s.histCh <- nil 315 continue // Ethstats sometime sends invalid history requests, ignore those 316 } 317 list, ok := request["list"].([]interface{}) 318 if !ok { 319 log.Warn("Invalid stats history block list", "list", request["list"]) 320 return 321 } 322 // Convert the block number list to an integer list 323 numbers := make([]uint64, len(list)) 324 for i, num := range list { 325 n, ok := num.(float64) 326 if !ok { 327 log.Warn("Invalid stats history block number", "number", num) 328 return 329 } 330 numbers[i] = uint64(n) 331 } 332 select { 333 case s.histCh <- numbers: 334 continue 335 default: 336 } 337 } 338 // Report anything else and continue 339 log.Info("Unknown stats message", "msg", msg) 340 } 341 } 342 343 // nodeInfo is the collection of metainformation about a node that is displayed 344 // on the monitoring page. 345 type nodeInfo struct { 346 Name string `json:"name"` 347 Node string `json:"node"` 348 Port int `json:"port"` 349 Network string `json:"net"` 350 Protocol string `json:"protocol"` 351 API string `json:"api"` 352 Os string `json:"os"` 353 OsVer string `json:"os_v"` 354 Client string `json:"client"` 355 History bool `json:"canUpdateHistory"` 356 } 357 358 // authMsg is the authentication infos needed to login to a monitoring server. 359 type authMsg struct { 360 Id string `json:"id"` 361 Info nodeInfo `json:"info"` 362 Secret string `json:"secret"` 363 } 364 365 // login tries to authorize the client at the remote server. 366 func (s *Service) login(conn *websocket.Conn) error { 367 // Construct and send the login authentication 368 infos := s.server.NodeInfo() 369 370 var network, protocol string 371 if info := infos.Protocols["eth"]; info != nil { 372 network = fmt.Sprintf("%d", info.(*eth.NodeInfo).Network) 373 protocol = fmt.Sprintf("eth/%d", eth.ProtocolVersions[0]) 374 } else { 375 network = fmt.Sprintf("%d", infos.Protocols["les"].(*les.NodeInfo).Network) 376 protocol = fmt.Sprintf("les/%d", les.ClientProtocolVersions[0]) 377 } 378 auth := &authMsg{ 379 Id: s.node, 380 Info: nodeInfo{ 381 Name: s.node, 382 Node: infos.Name, 383 Port: infos.Ports.Listener, 384 Network: network, 385 Protocol: protocol, 386 API: "No", 387 Os: runtime.GOOS, 388 OsVer: runtime.GOARCH, 389 Client: "0.1.1", 390 History: true, 391 }, 392 Secret: s.pass, 393 } 394 login := map[string][]interface{}{ 395 "emit": {"hello", auth}, 396 } 397 if err := websocket.JSON.Send(conn, login); err != nil { 398 return err 399 } 400 // Retrieve the remote ack or connection termination 401 var ack map[string][]string 402 if err := websocket.JSON.Receive(conn, &ack); err != nil || len(ack["emit"]) != 1 || ack["emit"][0] != "ready" { 403 return errors.New("unauthorized") 404 } 405 return nil 406 } 407 408 // report collects all possible data to report and send it to the stats server. 409 // This should only be used on reconnects or rarely to avoid overloading the 410 // server. Use the individual methods for reporting subscribed events. 411 func (s *Service) report(conn *websocket.Conn) error { 412 if err := s.reportLatency(conn); err != nil { 413 return err 414 } 415 if err := s.reportBlock(conn, nil); err != nil { 416 return err 417 } 418 if err := s.reportPending(conn); err != nil { 419 return err 420 } 421 if err := s.reportStats(conn); err != nil { 422 return err 423 } 424 return nil 425 } 426 427 // reportLatency sends a ping request to the server, measures the RTT time and 428 // finally sends a latency update. 429 func (s *Service) reportLatency(conn *websocket.Conn) error { 430 // Send the current time to the ethstats server 431 start := time.Now() 432 433 ping := map[string][]interface{}{ 434 "emit": {"node-ping", map[string]string{ 435 "id": s.node, 436 "clientTime": start.String(), 437 }}, 438 } 439 if err := websocket.JSON.Send(conn, ping); err != nil { 440 return err 441 } 442 // Wait for the pong request to arrive back 443 select { 444 case <-s.pongCh: 445 // Pong delivered, report the latency 446 case <-time.After(5 * time.Second): 447 // Ping timeout, abort 448 return errors.New("ping timed out") 449 } 450 latency := strconv.Itoa(int((time.Since(start) / time.Duration(2)).Nanoseconds() / 1000000)) 451 452 // Send back the measured latency 453 log.Trace("Sending measured latency to ethstats", "latency", latency) 454 455 stats := map[string][]interface{}{ 456 "emit": {"latency", map[string]string{ 457 "id": s.node, 458 "latency": latency, 459 }}, 460 } 461 return websocket.JSON.Send(conn, stats) 462 } 463 464 // blockStats is the information to report about individual blocks. 465 type blockStats struct { 466 Number *big.Int `json:"number"` 467 Hash common.Hash `json:"hash"` 468 ParentHash common.Hash `json:"parentHash"` 469 Timestamp *big.Int `json:"timestamp"` 470 Miner common.Address `json:"miner"` 471 GasUsed uint64 `json:"gasUsed"` 472 GasLimit uint64 `json:"gasLimit"` 473 Diff string `json:"difficulty"` 474 TotalDiff string `json:"totalDifficulty"` 475 Txs []txStats `json:"transactions"` 476 TxHash common.Hash `json:"transactionsRoot"` 477 Root common.Hash `json:"stateRoot"` 478 Uncles uncleStats `json:"uncles"` 479 } 480 481 // txStats is the information to report about individual transactions. 482 type txStats struct { 483 Hash common.Hash `json:"hash"` 484 } 485 486 // uncleStats is a custom wrapper around an uncle array to force serializing 487 // empty arrays instead of returning null for them. 488 type uncleStats []*types.Header 489 490 func (s uncleStats) MarshalJSON() ([]byte, error) { 491 if uncles := ([]*types.Header)(s); len(uncles) > 0 { 492 return json.Marshal(uncles) 493 } 494 return []byte("[]"), nil 495 } 496 497 // reportBlock retrieves the current chain head and repors it to the stats server. 498 func (s *Service) reportBlock(conn *websocket.Conn, block *types.Block) error { 499 // Gather the block details from the header or block chain 500 details := s.assembleBlockStats(block) 501 502 // Assemble the block report and send it to the server 503 log.Trace("Sending new block to ethstats", "number", details.Number, "hash", details.Hash) 504 505 stats := map[string]interface{}{ 506 "id": s.node, 507 "block": details, 508 } 509 report := map[string][]interface{}{ 510 "emit": {"block", stats}, 511 } 512 return websocket.JSON.Send(conn, report) 513 } 514 515 // assembleBlockStats retrieves any required metadata to report a single block 516 // and assembles the block stats. If block is nil, the current head is processed. 517 func (s *Service) assembleBlockStats(block *types.Block) *blockStats { 518 // Gather the block infos from the local blockchain 519 var ( 520 header *types.Header 521 td *big.Int 522 txs []txStats 523 uncles []*types.Header 524 ) 525 if s.eth != nil { 526 // Full nodes have all needed information available 527 if block == nil { 528 block = s.eth.BlockChain().CurrentBlock() 529 } 530 header = block.Header() 531 td = s.eth.BlockChain().GetTd(header.Hash(), header.Number.Uint64()) 532 533 txs = make([]txStats, len(block.Transactions())) 534 for i, tx := range block.Transactions() { 535 txs[i].Hash = tx.Hash() 536 } 537 uncles = block.Uncles() 538 } else { 539 // Light nodes would need on-demand lookups for transactions/uncles, skip 540 if block != nil { 541 header = block.Header() 542 } else { 543 header = s.les.BlockChain().CurrentHeader() 544 } 545 td = s.les.BlockChain().GetTd(header.Hash(), header.Number.Uint64()) 546 txs = []txStats{} 547 } 548 // Assemble and return the block stats 549 author, _ := s.engine.Author(header) 550 551 return &blockStats{ 552 Number: header.Number, 553 Hash: header.Hash(), 554 ParentHash: header.ParentHash, 555 Timestamp: header.Time, 556 Miner: author, 557 GasUsed: header.GasUsed, 558 GasLimit: header.GasLimit, 559 Diff: header.Difficulty.String(), 560 TotalDiff: td.String(), 561 Txs: txs, 562 TxHash: header.TxHash, 563 Root: header.Root, 564 Uncles: uncles, 565 } 566 } 567 568 // reportHistory retrieves the most recent batch of blocks and reports it to the 569 // stats server. 570 func (s *Service) reportHistory(conn *websocket.Conn, list []uint64) error { 571 // Figure out the indexes that need reporting 572 indexes := make([]uint64, 0, historyUpdateRange) 573 if len(list) > 0 { 574 // Specific indexes requested, send them back in particular 575 indexes = append(indexes, list...) 576 } else { 577 // No indexes requested, send back the top ones 578 var head int64 579 if s.eth != nil { 580 head = s.eth.BlockChain().CurrentHeader().Number.Int64() 581 } else { 582 head = s.les.BlockChain().CurrentHeader().Number.Int64() 583 } 584 start := head - historyUpdateRange + 1 585 if start < 0 { 586 start = 0 587 } 588 for i := uint64(start); i <= uint64(head); i++ { 589 indexes = append(indexes, i) 590 } 591 } 592 // Gather the batch of blocks to report 593 history := make([]*blockStats, len(indexes)) 594 for i, number := range indexes { 595 // Retrieve the next block if it's known to us 596 var block *types.Block 597 if s.eth != nil { 598 block = s.eth.BlockChain().GetBlockByNumber(number) 599 } else { 600 if header := s.les.BlockChain().GetHeaderByNumber(number); header != nil { 601 block = types.NewBlockWithHeader(header) 602 } 603 } 604 // If we do have the block, add to the history and continue 605 if block != nil { 606 history[len(history)-1-i] = s.assembleBlockStats(block) 607 continue 608 } 609 // Ran out of blocks, cut the report short and send 610 history = history[len(history)-i:] 611 break 612 } 613 // Assemble the history report and send it to the server 614 if len(history) > 0 { 615 log.Trace("Sending historical blocks to ethstats", "first", history[0].Number, "last", history[len(history)-1].Number) 616 } else { 617 log.Trace("No history to send to stats server") 618 } 619 stats := map[string]interface{}{ 620 "id": s.node, 621 "history": history, 622 } 623 report := map[string][]interface{}{ 624 "emit": {"history", stats}, 625 } 626 return websocket.JSON.Send(conn, report) 627 } 628 629 // pendStats is the information to report about pending transactions. 630 type pendStats struct { 631 Pending int `json:"pending"` 632 } 633 634 // reportPending retrieves the current number of pending transactions and reports 635 // it to the stats server. 636 func (s *Service) reportPending(conn *websocket.Conn) error { 637 // Retrieve the pending count from the local blockchain 638 var pending int 639 if s.eth != nil { 640 pending, _ = s.eth.TxPool().Stats() 641 } else { 642 pending = s.les.TxPool().Stats() 643 } 644 // Assemble the transaction stats and send it to the server 645 log.Trace("Sending pending transactions to ethstats", "count", pending) 646 647 stats := map[string]interface{}{ 648 "id": s.node, 649 "stats": &pendStats{ 650 Pending: pending, 651 }, 652 } 653 report := map[string][]interface{}{ 654 "emit": {"pending", stats}, 655 } 656 return websocket.JSON.Send(conn, report) 657 } 658 659 // nodeStats is the information to report about the local node. 660 type nodeStats struct { 661 Active bool `json:"active"` 662 Syncing bool `json:"syncing"` 663 Mining bool `json:"mining"` 664 Hashrate int `json:"hashrate"` 665 Peers int `json:"peers"` 666 GasPrice int `json:"gasPrice"` 667 Uptime int `json:"uptime"` 668 } 669 670 // reportPending retrieves various stats about the node at the networking and 671 // mining layer and reports it to the stats server. 672 func (s *Service) reportStats(conn *websocket.Conn) error { 673 // Gather the syncing and mining infos from the local miner instance 674 var ( 675 mining bool 676 hashrate int 677 syncing bool 678 gasprice int 679 ) 680 if s.eth != nil { 681 mining = s.eth.Miner().Mining() 682 hashrate = int(s.eth.Miner().HashRate()) 683 684 sync := s.eth.Downloader().Progress() 685 syncing = s.eth.BlockChain().CurrentHeader().Number.Uint64() >= sync.HighestBlock 686 687 price, _ := s.eth.ApiBackend.SuggestPrice(context.Background()) 688 gasprice = int(price.Uint64()) 689 } else { 690 sync := s.les.Downloader().Progress() 691 syncing = s.les.BlockChain().CurrentHeader().Number.Uint64() >= sync.HighestBlock 692 } 693 // Assemble the node stats and send it to the server 694 log.Trace("Sending node details to ethstats") 695 696 stats := map[string]interface{}{ 697 "id": s.node, 698 "stats": &nodeStats{ 699 Active: true, 700 Mining: mining, 701 Hashrate: hashrate, 702 Peers: s.server.PeerCount(), 703 GasPrice: gasprice, 704 Syncing: syncing, 705 Uptime: 100, 706 }, 707 } 708 report := map[string][]interface{}{ 709 "emit": {"stats", stats}, 710 } 711 return websocket.JSON.Send(conn, report) 712 }