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