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