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