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