github.com/dominant-strategies/go-quai@v0.28.2/quaistats/quaistats.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 quaistats implements the network stats reporting service. 18 package quaistats 19 20 import ( 21 "bytes" 22 "context" 23 "crypto/sha256" 24 "encoding/hex" 25 "encoding/json" 26 "errors" 27 "fmt" 28 "io/ioutil" 29 "math/big" 30 "net" 31 "net/http" 32 "runtime" 33 "strconv" 34 "strings" 35 "sync" 36 "time" 37 38 "github.com/dgrijalva/jwt-go" 39 40 lru "github.com/hashicorp/golang-lru" 41 "github.com/shirou/gopsutil/cpu" 42 "github.com/shirou/gopsutil/mem" 43 "github.com/shirou/gopsutil/process" 44 45 "os/exec" 46 47 "github.com/dominant-strategies/go-quai/common" 48 "github.com/dominant-strategies/go-quai/consensus" 49 "github.com/dominant-strategies/go-quai/core" 50 "github.com/dominant-strategies/go-quai/core/types" 51 "github.com/dominant-strategies/go-quai/eth/downloader" 52 ethproto "github.com/dominant-strategies/go-quai/eth/protocols/eth" 53 "github.com/dominant-strategies/go-quai/event" 54 "github.com/dominant-strategies/go-quai/log" 55 "github.com/dominant-strategies/go-quai/node" 56 "github.com/dominant-strategies/go-quai/p2p" 57 "github.com/dominant-strategies/go-quai/params" 58 "github.com/dominant-strategies/go-quai/rpc" 59 ) 60 61 const ( 62 // chainHeadChanSize is the size of channel listening to ChainHeadEvent. 63 chainHeadChanSize = 10 64 chainSideChanSize = 10 65 66 // reportInterval is the time interval between two reports. 67 reportInterval = 15 68 69 c_alpha = 8 70 c_statsErrorValue = int64(-1) 71 72 // Max number of stats objects to send in one batch 73 c_queueBatchSize uint64 = 10 74 75 // Number of blocks to include in one batch of transactions 76 c_txBatchSize uint64 = 10 77 78 // Seconds that we want to iterate over (3600s = 1 hr) 79 c_windowSize uint64 = 3600 80 81 // Max number of objects to keep in queue 82 c_maxQueueSize int = 100 83 ) 84 85 var ( 86 chainID9000 = big.NewInt(9000) 87 chainID12000 = big.NewInt(12000) 88 chainID15000 = big.NewInt(15000) 89 chainID17000 = big.NewInt(17000) 90 chainID1337 = big.NewInt(1337) 91 ) 92 93 // backend encompasses the bare-minimum functionality needed for quaistats reporting 94 type backend interface { 95 SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription 96 SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription 97 SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription 98 CurrentHeader() *types.Header 99 TotalLogS(header *types.Header) *big.Int 100 HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) 101 Stats() (pending int, queued int) 102 Downloader() *downloader.Downloader 103 ChainConfig() *params.ChainConfig 104 ProcessingState() bool 105 } 106 107 // fullNodeBackend encompasses the functionality necessary for a full node 108 // reporting to quaistats 109 type fullNodeBackend interface { 110 backend 111 BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) 112 BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) 113 CurrentBlock() *types.Block 114 } 115 116 // Service implements an Quai netstats reporting daemon that pushes local 117 // chain statistics up to a monitoring server. 118 type Service struct { 119 server *p2p.Server // Peer-to-peer server to retrieve networking infos 120 backend backend 121 engine consensus.Engine // Consensus engine to retrieve variadic block fields 122 123 node string // Name of the node to display on the monitoring page 124 pass string // Password to authorize access to the monitoring page 125 host string // Remote address of the monitoring service 126 sendfullstats bool // Whether the node is sending full stats or not 127 128 pongCh chan struct{} // Pong notifications are fed into this channel 129 headSub event.Subscription 130 131 transactionStatsQueue *StatsQueue 132 detailStatsQueue *StatsQueue 133 appendTimeStatsQueue *StatsQueue 134 135 // After handling a block and potentially adding to the queues, it will notify the sendStats goroutine 136 // that stats are ready to be sent 137 statsReadyCh chan struct{} 138 139 blockLookupCache *lru.Cache 140 141 chainID *big.Int 142 143 instanceDir string // Path to the node's instance directory 144 } 145 146 // StatsQueue is a thread-safe queue designed for managing and processing stats data. 147 // 148 // The primary objective of the StatsQueue is to provide a safe mechanism for enqueuing, 149 // dequeuing, and requeuing stats objects concurrently across multiple goroutines. 150 // 151 // Key Features: 152 // - Enqueue: Allows adding an item to the end of the queue. 153 // - Dequeue: Removes and returns the item from the front of the queue. 154 // - RequeueFront: Adds an item back to the front of the queue, useful for failed processing attempts. 155 // 156 // Concurrent Access: 157 // - The internal state of the queue is protected by a mutex to prevent data races and ensure 158 // that the operations are atomic. As a result, it's safe to use across multiple goroutines 159 // without external synchronization. 160 type StatsQueue struct { 161 data []interface{} 162 mutex sync.Mutex 163 } 164 165 func NewStatsQueue() *StatsQueue { 166 return &StatsQueue{ 167 data: make([]interface{}, 0), 168 } 169 } 170 171 func (q *StatsQueue) Enqueue(item interface{}) { 172 q.mutex.Lock() 173 defer q.mutex.Unlock() 174 175 if len(q.data) >= c_maxQueueSize { 176 q.dequeue() 177 } 178 179 q.data = append(q.data, item) 180 } 181 182 func (q *StatsQueue) Dequeue() interface{} { 183 q.mutex.Lock() 184 defer q.mutex.Unlock() 185 186 if len(q.data) == 0 { 187 return nil 188 } 189 190 return q.dequeue() 191 } 192 193 func (q *StatsQueue) dequeue() interface{} { 194 item := q.data[0] 195 q.data = q.data[1:] 196 return item 197 } 198 199 func (q *StatsQueue) EnqueueFront(item interface{}) { 200 q.mutex.Lock() 201 defer q.mutex.Unlock() 202 203 if len(q.data) >= c_maxQueueSize { 204 q.dequeue() 205 } 206 207 q.data = append([]interface{}{item}, q.data...) 208 } 209 210 func (q *StatsQueue) EnqueueFrontBatch(items []interface{}) { 211 q.mutex.Lock() 212 defer q.mutex.Unlock() 213 214 // Iterate backwards through the items and add them to the front of the queue 215 // Going backwards means oldest items are not added in case of overflow 216 for i := len(items) - 1; i >= 0; i-- { 217 if len(q.data) >= c_maxQueueSize { 218 break 219 } 220 q.data = append([]interface{}{items[i]}, q.data...) 221 } 222 } 223 224 func (q *StatsQueue) Size() int { 225 q.mutex.Lock() 226 defer q.mutex.Unlock() 227 228 return len(q.data) 229 } 230 231 // parseEthstatsURL parses the netstats connection url. 232 // URL argument should be of the form <nodename:secret@host:port> 233 // If non-erroring, the returned slice contains 3 elements: [nodename, pass, host] 234 func parseEthstatsURL(url string) (parts []string, err error) { 235 err = fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url) 236 237 hostIndex := strings.LastIndex(url, "@") 238 if hostIndex == -1 || hostIndex == len(url)-1 { 239 return nil, err 240 } 241 preHost, host := url[:hostIndex], url[hostIndex+1:] 242 243 passIndex := strings.LastIndex(preHost, ":") 244 if passIndex == -1 { 245 return []string{preHost, "", host}, nil 246 } 247 nodename, pass := preHost[:passIndex], "" 248 if passIndex != len(preHost)-1 { 249 pass = preHost[passIndex+1:] 250 } 251 252 return []string{nodename, pass, host}, nil 253 } 254 255 // New returns a monitoring service ready for stats reporting. 256 func New(node *node.Node, backend backend, engine consensus.Engine, url string, sendfullstats bool) error { 257 parts, err := parseEthstatsURL(url) 258 if err != nil { 259 return err 260 } 261 262 chainID := backend.ChainConfig().ChainID 263 var durationLimit *big.Int 264 265 switch { 266 case chainID.Cmp(chainID9000) == 0: 267 durationLimit = params.DurationLimit 268 case chainID.Cmp(chainID12000) == 0: 269 durationLimit = params.GardenDurationLimit 270 case chainID.Cmp(chainID15000) == 0: 271 durationLimit = params.OrchardDurationLimit 272 case chainID.Cmp(chainID17000) == 0: 273 durationLimit = params.LighthouseDurationLimit 274 case chainID.Cmp(chainID1337) == 0: 275 durationLimit = params.LocalDurationLimit 276 default: 277 durationLimit = params.DurationLimit 278 } 279 280 durationLimitInt := durationLimit.Uint64() 281 282 c_blocksPerWindow := c_windowSize / durationLimitInt 283 284 blockLookupCache, _ := lru.New(int(c_blocksPerWindow * 2)) 285 286 quaistats := &Service{ 287 backend: backend, 288 engine: engine, 289 server: node.Server(), 290 node: parts[0], 291 pass: parts[1], 292 host: parts[2], 293 pongCh: make(chan struct{}), 294 chainID: backend.ChainConfig().ChainID, 295 transactionStatsQueue: NewStatsQueue(), 296 detailStatsQueue: NewStatsQueue(), 297 appendTimeStatsQueue: NewStatsQueue(), 298 statsReadyCh: make(chan struct{}), 299 sendfullstats: sendfullstats, 300 blockLookupCache: blockLookupCache, 301 instanceDir: node.InstanceDir(), 302 } 303 304 node.RegisterLifecycle(quaistats) 305 return nil 306 } 307 308 // Start implements node.Lifecycle, starting up the monitoring and reporting daemon. 309 func (s *Service) Start() error { 310 // Subscribe to chain events to execute updates on 311 chainHeadCh := make(chan core.ChainHeadEvent, chainHeadChanSize) 312 313 s.headSub = s.backend.SubscribeChainHeadEvent(chainHeadCh) 314 315 go s.loopBlocks(chainHeadCh) 316 go s.loopSender(s.initializeURLMap()) 317 318 log.Info("Stats daemon started") 319 return nil 320 } 321 322 // Stop implements node.Lifecycle, terminating the monitoring and reporting daemon. 323 func (s *Service) Stop() error { 324 s.headSub.Unsubscribe() 325 log.Info("Stats daemon stopped") 326 return nil 327 } 328 329 func (s *Service) loopBlocks(chainHeadCh chan core.ChainHeadEvent) { 330 defer func() { 331 if r := recover(); r != nil { 332 log.Error("Stats process crashed", "error", r) 333 go s.loopBlocks(chainHeadCh) 334 } 335 }() 336 337 quitCh := make(chan struct{}) 338 339 go func() { 340 for { 341 select { 342 case head := <-chainHeadCh: 343 // Directly handle the block 344 go s.handleBlock(head.Block) 345 case <-s.headSub.Err(): 346 close(quitCh) 347 return 348 } 349 } 350 }() 351 352 // Wait for the goroutine to signal completion or error 353 <-quitCh 354 } 355 356 // loop keeps trying to connect to the netstats server, reporting chain events 357 // until termination. 358 func (s *Service) loopSender(urlMap map[string]string) { 359 defer func() { 360 if r := recover(); r != nil { 361 fmt.Println("Stats process crashed with error:", r) 362 go s.loopSender(urlMap) 363 } 364 }() 365 366 // Start a goroutine that exhausts the subscriptions to avoid events piling up 367 var ( 368 quitCh = make(chan struct{}) 369 ) 370 371 nodeStatsMod := 0 372 373 errTimer := time.NewTimer(0) 374 defer errTimer.Stop() 375 var authJwt = "" 376 // Loop reporting until termination 377 for { 378 select { 379 case <-quitCh: 380 return 381 case <-errTimer.C: 382 // If we don't have a JWT or it's expired, get a new one 383 isJwtExpiredResult, jwtIsExpiredErr := s.isJwtExpired(authJwt) 384 if authJwt == "" || isJwtExpiredResult || jwtIsExpiredErr != nil { 385 log.Info("Trying to login to quaistats") 386 var err error 387 authJwt, err = s.login2(urlMap["login"]) 388 if err != nil { 389 log.Warn("Stats login failed", "err", err) 390 errTimer.Reset(10 * time.Second) 391 continue 392 } 393 } 394 395 errs := make(map[string]error) 396 397 // Authenticate the client with the server 398 for key, url := range urlMap { 399 switch key { 400 case "login": 401 continue 402 case "nodeStats": 403 if errs[key] = s.reportNodeStats(url, 0, authJwt); errs[key] != nil { 404 log.Warn("Initial stats report failed for "+key, "err", errs[key]) 405 errTimer.Reset(0) 406 continue 407 } 408 case "blockTransactionStats": 409 if errs[key] = s.sendTransactionStats(url, authJwt); errs[key] != nil { 410 log.Warn("Initial stats report failed for "+key, "err", errs[key]) 411 errTimer.Reset(0) 412 continue 413 } 414 case "blockDetailStats": 415 if errs[key] = s.sendDetailStats(url, authJwt); errs[key] != nil { 416 log.Warn("Initial stats report failed for "+key, "err", errs[key]) 417 errTimer.Reset(0) 418 continue 419 } 420 case "blockAppendTime": 421 if errs[key] = s.sendAppendTimeStats(url, authJwt); errs[key] != nil { 422 log.Warn("Initial stats report failed for "+key, "err", errs[key]) 423 errTimer.Reset(0) 424 continue 425 } 426 } 427 } 428 429 // Keep sending status updates until the connection breaks 430 fullReport := time.NewTicker(reportInterval * time.Second) 431 432 var noErrs = true 433 for noErrs { 434 var err error 435 select { 436 case <-quitCh: 437 fullReport.Stop() 438 return 439 440 case <-fullReport.C: 441 nodeStatsMod ^= 1 442 if err = s.reportNodeStats(urlMap["nodeStats"], nodeStatsMod, authJwt); err != nil { 443 noErrs = false 444 log.Warn("nodeStats full stats report failed", "err", err) 445 } 446 case <-s.statsReadyCh: 447 if url, ok := urlMap["blockTransactionStats"]; ok { 448 if err := s.sendTransactionStats(url, authJwt); err != nil { 449 noErrs = false 450 errTimer.Reset(0) 451 log.Warn("blockTransactionStats stats report failed", "err", err) 452 } 453 } 454 if url, ok := urlMap["blockDetailStats"]; ok { 455 if err := s.sendDetailStats(url, authJwt); err != nil { 456 noErrs = false 457 errTimer.Reset(0) 458 log.Warn("blockDetailStats stats report failed", "err", err) 459 } 460 } 461 if url, ok := urlMap["blockAppendTime"]; ok { 462 if err := s.sendAppendTimeStats(url, authJwt); err != nil { 463 noErrs = false 464 errTimer.Reset(0) 465 log.Warn("blockAppendTime stats report failed", "err", err) 466 } 467 } 468 } 469 errTimer.Reset(0) 470 } 471 fullReport.Stop() 472 } 473 } 474 } 475 476 func (s *Service) initializeURLMap() map[string]string { 477 return map[string]string{ 478 "blockTransactionStats": fmt.Sprintf("http://%s/stats/blockTransactionStats", s.host), 479 "blockAppendTime": fmt.Sprintf("http://%s/stats/blockAppendTime", s.host), 480 "blockDetailStats": fmt.Sprintf("http://%s/stats/blockDetailStats", s.host), 481 "nodeStats": fmt.Sprintf("http://%s/stats/nodeStats", s.host), 482 "login": fmt.Sprintf("http://%s/auth/login", s.host), 483 } 484 } 485 486 func (s *Service) handleBlock(block *types.Block) { 487 // Cache Block 488 log.Trace("Handling block", "detailsQueueSize", s.detailStatsQueue.Size(), "appendTimeQueueSize", s.appendTimeStatsQueue.Size(), "transactionQueueSize", s.transactionStatsQueue.Size(), "blockNumber", block.NumberU64()) 489 490 s.cacheBlock(block) 491 492 if s.sendfullstats { 493 dtlStats := s.assembleBlockDetailStats(block) 494 if dtlStats != nil { 495 s.detailStatsQueue.Enqueue(dtlStats) 496 } 497 } 498 499 appStats := s.assembleBlockAppendTimeStats(block) 500 if appStats != nil { 501 s.appendTimeStatsQueue.Enqueue(appStats) 502 } 503 504 if block.NumberU64()%c_txBatchSize == 0 && s.sendfullstats && block.Header().Location().Context() == common.ZONE_CTX { 505 txStats := s.assembleBlockTransactionStats(block) 506 if txStats != nil { 507 s.transactionStatsQueue.Enqueue(txStats) 508 } 509 } 510 511 // After handling a block and potentially adding to the queues, notify the sendStats goroutine 512 // that stats are ready to be sent 513 s.statsReadyCh <- struct{}{} 514 } 515 516 func (s *Service) reportNodeStats(url string, mod int, authJwt string) error { 517 if url == "" { 518 log.Warn("node stats url is empty") 519 return errors.New("node stats connection is empty") 520 } 521 522 isRegion := strings.Contains(s.instanceDir, "region") 523 isPrime := strings.Contains(s.instanceDir, "prime") 524 525 if isRegion || isPrime { 526 log.Debug("Skipping node stats for region or prime. Filtered out on backend") 527 return nil 528 } 529 530 log.Trace("Quai Stats Instance Dir", "path", s.instanceDir+"/../..") 531 532 // Don't send if dirSize < 1 533 // Get disk usage (as a percentage) 534 diskUsage, err := dirSize(s.instanceDir + "/../..") 535 if err != nil { 536 log.Warn("Error calculating directory sizes:", "error", err) 537 diskUsage = c_statsErrorValue 538 } 539 540 diskSize, err := diskTotalSize() 541 if err != nil { 542 log.Warn("Error calculating disk size:", "error", err) 543 diskUsage = c_statsErrorValue 544 } 545 546 diskUsagePercent := float64(c_statsErrorValue) 547 if diskSize > 0 { 548 diskUsagePercent = float64(diskUsage) / float64(diskSize) 549 } else { 550 log.Warn("Error calculating disk usage percent: disk size is 0") 551 } 552 553 // Usage in your main function 554 ramUsage, err := getQuaiRAMUsage() 555 if err != nil { 556 log.Warn("Error getting Quai RAM usage:", "error", err) 557 return err 558 } 559 var ramUsagePercent, ramFreePercent, ramAvailablePercent float64 560 if vmStat, err := mem.VirtualMemory(); err == nil { 561 ramUsagePercent = float64(ramUsage) / float64(vmStat.Total) 562 ramFreePercent = float64(vmStat.Free) / float64(vmStat.Total) 563 ramAvailablePercent = float64(vmStat.Available) / float64(vmStat.Total) 564 } else { 565 log.Warn("Error getting RAM stats:", "error", err) 566 return err 567 } 568 569 // Get CPU usage 570 cpuUsageQuai, err := getQuaiCPUUsage() 571 if err != nil { 572 log.Warn("Error getting Quai CPU percent usage:", "error", err) 573 return err 574 } else { 575 cpuUsageQuai /= float64(100) 576 } 577 578 var cpuFree float32 579 if cpuUsageTotal, err := cpu.Percent(0, false); err == nil { 580 cpuFree = 1 - float32(cpuUsageTotal[0]/float64(100)) 581 } else { 582 log.Warn("Error getting CPU free:", "error", err) 583 return err 584 } 585 586 currentHeader := s.backend.CurrentHeader() 587 588 if currentHeader == nil { 589 log.Warn("Current header is nil") 590 return errors.New("current header is nil") 591 } 592 // Get current block number 593 currentBlockHeight := currentHeader.NumberArray() 594 595 // Get location 596 location := currentHeader.Location() 597 598 // Get the first non-loopback MAC address 599 var macAddress string 600 interfaces, err := net.Interfaces() 601 if err == nil { 602 for _, interf := range interfaces { 603 if interf.HardwareAddr != nil && len(interf.HardwareAddr.String()) > 0 && (interf.Flags&net.FlagLoopback) == 0 { 604 macAddress = interf.HardwareAddr.String() 605 break 606 } 607 } 608 } else { 609 log.Warn("Error getting MAC address:", err) 610 return err 611 } 612 613 // Hash the MAC address 614 var hashedMAC string 615 if macAddress != "" { 616 hash := sha256.Sum256([]byte(macAddress)) 617 hashedMAC = hex.EncodeToString(hash[:]) 618 } 619 620 // Assemble the new node stats 621 log.Trace("Sending node details to quaistats") 622 623 document := map[string]interface{}{ 624 "id": s.node, 625 "nodeStats": &nodeStats{ 626 Name: s.node, 627 Timestamp: big.NewInt(time.Now().Unix()), // Current timestamp 628 RAMUsage: int64(ramUsage), 629 RAMUsagePercent: float32(ramUsagePercent), 630 RAMFreePercent: float32(ramFreePercent), 631 RAMAvailablePercent: float32(ramAvailablePercent), 632 CPUUsagePercent: float32(cpuUsageQuai), 633 CPUFree: float32(cpuFree), 634 DiskUsageValue: int64(diskUsage), 635 DiskUsagePercent: float32(diskUsagePercent), 636 CurrentBlockNumber: currentBlockHeight, 637 RegionLocation: location.Region(), 638 ZoneLocation: location.Zone(), 639 NodeStatsMod: mod, 640 HashedMAC: hashedMAC, 641 }, 642 } 643 644 jsonData, err := json.Marshal(document) 645 if err != nil { 646 log.Error("Failed to marshal node stats", "err", err) 647 return err 648 } 649 650 // Create a new HTTP request 651 req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) 652 if err != nil { 653 log.Error("Failed to create new HTTP request", "err", err) 654 return err 655 } 656 657 // Set headers 658 req.Header.Set("Content-Type", "application/json") 659 req.Header.Set("Authorization", "Bearer "+authJwt) 660 661 // Send the request using the default HTTP client 662 resp, err := http.DefaultClient.Do(req) 663 if err != nil { 664 log.Error("Failed to send node stats", "err", err) 665 return err 666 } 667 defer resp.Body.Close() 668 669 if resp.StatusCode != http.StatusOK { 670 body, err := ioutil.ReadAll(resp.Body) 671 if err != nil { 672 log.Error("Failed to response body", "err", err) 673 return err 674 } 675 log.Error("Received non-OK response", "status", resp.Status, "body", string(body)) 676 return errors.New("Received non-OK response: " + resp.Status) 677 } 678 log.Trace("Successfully sent node stats to quaistats") 679 return nil 680 } 681 682 func (s *Service) sendTransactionStats(url string, authJwt string) error { 683 if len(s.transactionStatsQueue.data) == 0 { 684 return nil 685 } 686 statsBatch := make([]*blockTransactionStats, 0, c_queueBatchSize) 687 688 for i := 0; i < int(c_queueBatchSize) && len(s.transactionStatsQueue.data) > 0; i++ { 689 stat := s.transactionStatsQueue.Dequeue() 690 if stat == nil { 691 break 692 } 693 statsBatch = append(statsBatch, stat.(*blockTransactionStats)) 694 } 695 696 if len(statsBatch) == 0 { 697 return nil 698 } 699 700 err := s.report(url, "blockTransactionStats", statsBatch, authJwt) 701 if err != nil && strings.Contains(err.Error(), "Received non-OK response") { 702 log.Warn("Failed to send transaction stats, requeuing stats", "err", err) 703 // Re-enqueue the failed stats from end to beginning 704 tempSlice := make([]interface{}, len(statsBatch)) 705 for i, item := range statsBatch { 706 tempSlice[len(statsBatch)-1-i] = item 707 } 708 s.transactionStatsQueue.EnqueueFrontBatch(tempSlice) 709 return err 710 } else if err != nil { 711 log.Warn("Failed to send transaction stats", "err", err) 712 return err 713 } 714 return nil 715 } 716 717 func (s *Service) sendDetailStats(url string, authJwt string) error { 718 if len(s.detailStatsQueue.data) == 0 { 719 return nil 720 } 721 statsBatch := make([]*blockDetailStats, 0, c_queueBatchSize) 722 723 for i := 0; i < int(c_queueBatchSize) && s.detailStatsQueue.Size() > 0; i++ { 724 stat := s.detailStatsQueue.Dequeue() 725 if stat == nil { 726 break 727 } 728 statsBatch = append(statsBatch, stat.(*blockDetailStats)) 729 } 730 731 if len(statsBatch) == 0 { 732 return nil 733 } 734 735 err := s.report(url, "blockDetailStats", statsBatch, authJwt) 736 if err != nil && strings.Contains(err.Error(), "Received non-OK response") { 737 log.Warn("Failed to send detail stats, requeuing stats", "err", err) 738 // Re-enqueue the failed stats from end to beginning 739 tempSlice := make([]interface{}, len(statsBatch)) 740 for i, item := range statsBatch { 741 tempSlice[len(statsBatch)-1-i] = item 742 } 743 s.detailStatsQueue.EnqueueFrontBatch(tempSlice) 744 return err 745 } else if err != nil { 746 log.Warn("Failed to send detail stats", "err", err) 747 return err 748 } 749 return nil 750 } 751 752 func (s *Service) sendAppendTimeStats(url string, authJwt string) error { 753 if len(s.appendTimeStatsQueue.data) == 0 { 754 return nil 755 } 756 757 statsBatch := make([]*blockAppendTime, 0, c_queueBatchSize) 758 759 for i := 0; i < int(c_queueBatchSize) && s.appendTimeStatsQueue.Size() > 0; i++ { 760 stat := s.appendTimeStatsQueue.Dequeue() 761 if stat == nil { 762 break 763 } 764 statsBatch = append(statsBatch, stat.(*blockAppendTime)) 765 } 766 767 if len(statsBatch) == 0 { 768 return nil 769 } 770 771 err := s.report(url, "blockAppendTime", statsBatch, authJwt) 772 if err != nil && strings.Contains(err.Error(), "Received non-OK response") { 773 log.Warn("Failed to send append time stats, requeuing stats", "err", err) 774 // Re-enqueue the failed stats from end to beginning 775 tempSlice := make([]interface{}, len(statsBatch)) 776 for i, item := range statsBatch { 777 tempSlice[len(statsBatch)-1-i] = item 778 } 779 s.appendTimeStatsQueue.EnqueueFrontBatch(tempSlice) 780 return err 781 } else if err != nil { 782 log.Warn("Failed to send append time stats", "err", err) 783 return err 784 } 785 return nil 786 } 787 788 func (s *Service) report(url string, dataType string, stats interface{}, authJwt string) error { 789 if url == "" { 790 log.Warn(dataType + " url is empty") 791 return errors.New(dataType + " url is empty") 792 } 793 794 if stats == nil { 795 log.Warn(dataType + " stats are nil") 796 return errors.New(dataType + " stats are nil") 797 } 798 799 log.Trace("Sending " + dataType + " stats to quaistats") 800 801 document := map[string]interface{}{ 802 "id": s.node, 803 dataType: stats, 804 } 805 806 jsonData, err := json.Marshal(document) 807 if err != nil { 808 log.Error("Failed to marshal "+dataType+" stats", "err", err) 809 return err 810 } 811 812 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) 813 if err != nil { 814 log.Error("Failed to create new request for "+dataType+" stats", "err", err) 815 return err 816 } 817 818 // Add headers 819 req.Header.Set("Content-Type", "application/json") 820 req.Header.Set("Authorization", "Bearer "+authJwt) // Add this line for the Authorization header 821 822 client := &http.Client{} 823 resp, err := client.Do(req) 824 if err != nil { 825 log.Error("Failed to send "+dataType+" stats", "err", err) 826 return err 827 } 828 defer resp.Body.Close() 829 830 if resp.StatusCode != http.StatusOK { 831 body, err := ioutil.ReadAll(resp.Body) 832 if err != nil { 833 log.Error("Failed to response body", "err", err) 834 return err 835 } 836 log.Error("Received non-OK response", "status", resp.Status, "body", string(body)) 837 return errors.New("Received non-OK response: " + resp.Status) 838 } 839 log.Trace("Successfully sent " + dataType + " stats to quaistats") 840 return nil 841 } 842 843 type cachedBlock struct { 844 number uint64 845 parentHash common.Hash 846 txCount uint64 847 time uint64 848 } 849 850 // nodeInfo is the collection of meta information about a node that is displayed 851 // on the monitoring page. 852 type nodeInfo struct { 853 Name string `json:"name"` 854 Node string `json:"node"` 855 Port int `json:"port"` 856 Network string `json:"net"` 857 Protocol string `json:"protocol"` 858 API string `json:"api"` 859 Os string `json:"os"` 860 OsVer string `json:"os_v"` 861 Client string `json:"client"` 862 History bool `json:"canUpdateHistory"` 863 Chain string `json:"chain"` 864 ChainID uint64 `json:"chainId"` 865 } 866 867 // authMsg is the authentication infos needed to login to a monitoring server. 868 type authMsg struct { 869 ID string `json:"id"` 870 Info nodeInfo `json:"info"` 871 Secret loginSecret `json:"secret"` 872 } 873 874 type loginSecret struct { 875 Name string `json:"name"` 876 Password string `json:"password"` 877 } 878 879 type Credentials struct { 880 Name string `json:"name"` 881 Password string `json:"password"` 882 } 883 884 type AuthResponse struct { 885 Success bool `json:"success"` 886 Token string `json:"token"` 887 } 888 889 func (s *Service) login2(url string) (string, error) { 890 // Substitute with your actual service address and port 891 892 infos := s.server.NodeInfo() 893 894 var protocols []string 895 for _, proto := range s.server.Protocols { 896 protocols = append(protocols, fmt.Sprintf("%s/%d", proto.Name, proto.Version)) 897 } 898 var network string 899 if info := infos.Protocols["eth"]; info != nil { 900 network = fmt.Sprintf("%d", info.(*ethproto.NodeInfo).Network) 901 } 902 903 var secretUser string 904 if s.sendfullstats { 905 secretUser = "admin" 906 } else { 907 secretUser = s.node 908 } 909 910 auth := &authMsg{ 911 ID: s.node, 912 Info: nodeInfo{ 913 Name: s.node, 914 Node: infos.Name, 915 Port: infos.Ports.Listener, 916 Network: network, 917 Protocol: strings.Join(protocols, ", "), 918 API: "No", 919 Os: runtime.GOOS, 920 OsVer: runtime.GOARCH, 921 Client: "0.1.1", 922 History: true, 923 Chain: common.NodeLocation.Name(), 924 ChainID: s.chainID.Uint64(), 925 }, 926 Secret: loginSecret{ 927 Name: secretUser, 928 Password: s.pass, 929 }, 930 } 931 932 authJson, err := json.Marshal(auth) 933 if err != nil { 934 return "", err 935 } 936 937 resp, err := http.Post(url, "application/json", bytes.NewBuffer(authJson)) 938 if err != nil { 939 return "", err 940 } 941 defer resp.Body.Close() 942 943 body, err := ioutil.ReadAll(resp.Body) 944 if err != nil { 945 log.Error("Failed to response body", "err", err) 946 return "", err 947 } 948 949 var authResponse AuthResponse 950 err = json.Unmarshal(body, &authResponse) 951 if err != nil { 952 return "", err 953 } 954 955 if authResponse.Success { 956 return authResponse.Token, nil 957 } 958 959 return "", fmt.Errorf("login failed") 960 } 961 962 // isJwtExpired checks if the JWT token is expired 963 func (s *Service) isJwtExpired(authJwt string) (bool, error) { 964 if authJwt == "" { 965 return false, errors.New("token is nil") 966 } 967 968 parts := strings.Split(authJwt, ".") 969 if len(parts) != 3 { 970 return false, errors.New("invalid token") 971 } 972 973 claims := jwt.MapClaims{} 974 _, _, err := new(jwt.Parser).ParseUnverified(authJwt, claims) 975 if err != nil { 976 return false, err 977 } 978 979 if exp, ok := claims["exp"].(float64); ok { 980 return time.Now().Unix() >= int64(exp), nil 981 } 982 983 return false, errors.New("exp claim not found in token") 984 } 985 986 // Trusted Only 987 type blockTransactionStats struct { 988 Timestamp *big.Int `json:"timestamp"` 989 TotalNoTransactions1h uint64 `json:"totalNoTransactions1h"` 990 TPS1m uint64 `json:"tps1m"` 991 TPS1hr uint64 `json:"tps1hr"` 992 Chain string `json:"chain"` 993 } 994 995 // Trusted Only 996 type blockDetailStats struct { 997 Timestamp *big.Int `json:"timestamp"` 998 ZoneHeight uint64 `json:"zoneHeight"` 999 RegionHeight uint64 `json:"regionHeight"` 1000 PrimeHeight uint64 `json:"primeHeight"` 1001 Chain string `json:"chain"` 1002 Entropy string `json:"entropy"` 1003 Difficulty string `json:"difficulty"` 1004 } 1005 1006 // Everyone sends every block 1007 type blockAppendTime struct { 1008 AppendTime time.Duration `json:"appendTime"` 1009 BlockNumber *big.Int `json:"number"` 1010 Chain string `json:"chain"` 1011 } 1012 1013 type nodeStats struct { 1014 Name string `json:"name"` 1015 Timestamp *big.Int `json:"timestamp"` 1016 RAMUsage int64 `json:"ramUsage"` 1017 RAMUsagePercent float32 `json:"ramUsagePercent"` 1018 RAMFreePercent float32 `json:"ramFreePercent"` 1019 RAMAvailablePercent float32 `json:"ramAvailablePercent"` 1020 CPUUsagePercent float32 `json:"cpuPercent"` 1021 CPUFree float32 `json:"cpuFree"` 1022 DiskUsagePercent float32 `json:"diskUsagePercent"` 1023 DiskUsageValue int64 `json:"diskUsageValue"` 1024 CurrentBlockNumber []*big.Int `json:"currentBlockNumber"` 1025 RegionLocation int `json:"regionLocation"` 1026 ZoneLocation int `json:"zoneLocation"` 1027 NodeStatsMod int `json:"nodeStatsMod"` 1028 HashedMAC string `json:"hashedMAC"` 1029 } 1030 1031 type tps struct { 1032 TPS1m uint64 1033 TPS1hr uint64 1034 TotalNumberTransactions1h uint64 1035 } 1036 1037 type BatchObject struct { 1038 TotalNoTransactions uint64 1039 OldestBlockTime uint64 1040 } 1041 1042 func (s *Service) cacheBlock(block *types.Block) cachedBlock { 1043 currentBlock := cachedBlock{ 1044 number: block.NumberU64(), 1045 parentHash: block.ParentHash(), 1046 txCount: uint64(len(block.Transactions())), 1047 time: block.Time(), 1048 } 1049 s.blockLookupCache.Add(block.Hash(), currentBlock) 1050 return currentBlock 1051 } 1052 1053 func (s *Service) calculateTPS(block *types.Block) *tps { 1054 var totalTransactions1h uint64 1055 var totalTransactions1m uint64 1056 var currentBlock interface{} 1057 var ok bool 1058 1059 fullNodeBackend := s.backend.(fullNodeBackend) 1060 withinMinute := true 1061 1062 currentBlock, ok = s.blockLookupCache.Get(block.Hash()) 1063 if !ok { 1064 currentBlock = s.cacheBlock(block) 1065 } 1066 1067 for { 1068 // If the current block is nil or the block is older than the window size, break 1069 if currentBlock == nil || currentBlock.(cachedBlock).time+c_windowSize < block.Time() { 1070 break 1071 } 1072 1073 // Add the number of transactions in the block to the total 1074 totalTransactions1h += currentBlock.(cachedBlock).txCount 1075 if withinMinute && currentBlock.(cachedBlock).time+60 > block.Time() { 1076 totalTransactions1m += currentBlock.(cachedBlock).txCount 1077 } else { 1078 withinMinute = false 1079 } 1080 1081 // If the current block is the genesis block, break 1082 if currentBlock.(cachedBlock).number == 1 { 1083 break 1084 } 1085 1086 // Get the parent block 1087 parentHash := currentBlock.(cachedBlock).parentHash 1088 currentBlock, ok = s.blockLookupCache.Get(parentHash) 1089 if !ok { 1090 1091 // If the parent block is not cached, get it from the full node backend and cache it 1092 fullBlock, fullBlockOk := fullNodeBackend.BlockByHash(context.Background(), parentHash) 1093 if fullBlockOk != nil { 1094 log.Error("Error getting block hash", "hash", parentHash.String()) 1095 return &tps{} 1096 } 1097 currentBlock = s.cacheBlock(fullBlock) 1098 } 1099 } 1100 1101 // Catches if we get to genesis block and are still within the window 1102 if currentBlock.(cachedBlock).number == 1 && withinMinute { 1103 delta := block.Time() - currentBlock.(cachedBlock).time 1104 return &tps{ 1105 TPS1m: totalTransactions1m / delta, 1106 TPS1hr: totalTransactions1h / delta, 1107 TotalNumberTransactions1h: totalTransactions1h, 1108 } 1109 } else if currentBlock.(cachedBlock).number == 1 { 1110 delta := block.Time() - currentBlock.(cachedBlock).time 1111 return &tps{ 1112 TPS1m: totalTransactions1m / 60, 1113 TPS1hr: totalTransactions1h / delta, 1114 TotalNumberTransactions1h: totalTransactions1h, 1115 } 1116 } 1117 1118 return &tps{ 1119 TPS1m: totalTransactions1m / 60, 1120 TPS1hr: totalTransactions1h / c_windowSize, 1121 TotalNumberTransactions1h: totalTransactions1h, 1122 } 1123 } 1124 1125 func (s *Service) assembleBlockDetailStats(block *types.Block) *blockDetailStats { 1126 if block == nil { 1127 return nil 1128 } 1129 header := block.Header() 1130 difficulty := header.Difficulty().String() 1131 1132 // Assemble and return the block stats 1133 return &blockDetailStats{ 1134 Timestamp: new(big.Int).SetUint64(header.Time()), 1135 ZoneHeight: header.NumberU64(2), 1136 RegionHeight: header.NumberU64(1), 1137 PrimeHeight: header.NumberU64(0), 1138 Chain: common.NodeLocation.Name(), 1139 Entropy: common.BigBitsToBits(s.backend.TotalLogS(block.Header())).String(), 1140 Difficulty: difficulty, 1141 } 1142 } 1143 1144 func (s *Service) assembleBlockAppendTimeStats(block *types.Block) *blockAppendTime { 1145 if block == nil { 1146 return nil 1147 } 1148 header := block.Header() 1149 appendTime := block.GetAppendTime() 1150 1151 log.Info("Raw Block Append Time", "appendTime", appendTime.Microseconds()) 1152 1153 // Assemble and return the block stats 1154 return &blockAppendTime{ 1155 AppendTime: appendTime, 1156 BlockNumber: header.Number(), 1157 Chain: common.NodeLocation.Name(), 1158 } 1159 } 1160 1161 func (s *Service) assembleBlockTransactionStats(block *types.Block) *blockTransactionStats { 1162 if block == nil { 1163 return nil 1164 } 1165 header := block.Header() 1166 tps := s.calculateTPS(block) 1167 if tps == nil { 1168 return nil 1169 } 1170 1171 // Assemble and return the block stats 1172 return &blockTransactionStats{ 1173 Timestamp: new(big.Int).SetUint64(header.Time()), 1174 TotalNoTransactions1h: tps.TotalNumberTransactions1h, 1175 TPS1m: tps.TPS1m, 1176 TPS1hr: tps.TPS1hr, 1177 Chain: common.NodeLocation.Name(), 1178 } 1179 } 1180 1181 func getQuaiCPUUsage() (float64, error) { 1182 // 'ps' command options might vary depending on your OS 1183 cmd := exec.Command("ps", "aux") 1184 numCores := runtime.NumCPU() 1185 1186 output, err := cmd.Output() 1187 if err != nil { 1188 return 0, err 1189 } 1190 1191 lines := strings.Split(string(output), "\n") 1192 var totalCpuUsage float64 1193 var cpuUsage float64 1194 for _, line := range lines { 1195 if strings.Contains(line, "go-quai") { 1196 fields := strings.Fields(line) 1197 if len(fields) > 2 { 1198 // Assuming %CPU is the third column, command is the eleventh 1199 cpuUsage, err = strconv.ParseFloat(fields[2], 64) 1200 if err != nil { 1201 return 0, err 1202 } 1203 totalCpuUsage += cpuUsage 1204 } 1205 } 1206 } 1207 1208 if totalCpuUsage == 0 { 1209 return 0, errors.New("quai process not found") 1210 } 1211 1212 return totalCpuUsage / float64(numCores), nil 1213 } 1214 1215 func getQuaiRAMUsage() (uint64, error) { 1216 // Get a list of all running processes 1217 processes, err := process.Processes() 1218 if err != nil { 1219 return 0, err 1220 } 1221 1222 var totalRam uint64 1223 1224 // Debug: log number of processes 1225 log.Trace("Number of processes", "number", len(processes)) 1226 1227 for _, p := range processes { 1228 cmdline, err := p.Cmdline() 1229 if err != nil { 1230 // Debug: log error 1231 log.Trace("Error getting process cmdline", "error", err) 1232 continue 1233 } 1234 1235 if strings.Contains(cmdline, "go-quai") { 1236 memInfo, err := p.MemoryInfo() 1237 if err != nil { 1238 return 0, err 1239 } 1240 totalRam += memInfo.RSS 1241 } 1242 } 1243 1244 if totalRam == 0 { 1245 return 0, errors.New("go-quai process not found") 1246 } 1247 1248 return totalRam, nil 1249 } 1250 1251 // dirSize returns the size of a directory in bytes. 1252 func dirSize(path string) (int64, error) { 1253 var cmd *exec.Cmd 1254 if runtime.GOOS == "darwin" { 1255 cmd = exec.Command("du", "-sk", path) 1256 } else if runtime.GOOS == "linux" { 1257 cmd = exec.Command("du", "-bs", path) 1258 } else { 1259 return -1, errors.New("unsupported OS") 1260 } 1261 // Execute command 1262 output, err := cmd.Output() 1263 if err != nil { 1264 return -1, err 1265 } 1266 1267 // Split the output and parse the size. 1268 sizeStr := strings.Split(string(output), "\t")[0] 1269 size, err := strconv.ParseInt(sizeStr, 10, 64) 1270 if err != nil { 1271 return -1, err 1272 } 1273 1274 // If on macOS, convert size from kilobytes to bytes. 1275 if runtime.GOOS == "darwin" { 1276 size *= 1024 1277 } 1278 1279 return size, nil 1280 } 1281 1282 // diskTotalSize returns the total size of the disk in bytes. 1283 func diskTotalSize() (int64, error) { 1284 var cmd *exec.Cmd 1285 if runtime.GOOS == "darwin" { 1286 cmd = exec.Command("df", "-k", "/") 1287 } else if runtime.GOOS == "linux" { 1288 cmd = exec.Command("df", "--block-size=1K", "/") 1289 } else { 1290 return 0, errors.New("unsupported OS") 1291 } 1292 1293 output, err := cmd.Output() 1294 if err != nil { 1295 return 0, err 1296 } 1297 1298 lines := strings.Split(string(output), "\n") 1299 if len(lines) < 2 { 1300 return 0, errors.New("unexpected output from df command") 1301 } 1302 1303 fields := strings.Fields(lines[1]) 1304 if len(fields) < 2 { 1305 return 0, errors.New("unexpected output from df command") 1306 } 1307 1308 totalSize, err := strconv.ParseInt(fields[1], 10, 64) 1309 if err != nil { 1310 return 0, err 1311 } 1312 1313 return totalSize * 1024, nil // convert from kilobytes to bytes 1314 }