github.com/minio/console@v1.4.1/api/admin_info.go (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero 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 // This program 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 Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 package api 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/http" 25 "net/url" 26 "regexp" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/minio/console/pkg/utils" 32 33 "github.com/go-openapi/runtime/middleware" 34 "github.com/minio/console/api/operations" 35 systemApi "github.com/minio/console/api/operations/system" 36 "github.com/minio/console/models" 37 ) 38 39 func registerAdminInfoHandlers(api *operations.ConsoleAPI) { 40 // return usage stats 41 api.SystemAdminInfoHandler = systemApi.AdminInfoHandlerFunc(func(params systemApi.AdminInfoParams, session *models.Principal) middleware.Responder { 42 infoResp, err := getAdminInfoResponse(session, params) 43 if err != nil { 44 return systemApi.NewAdminInfoDefault(err.Code).WithPayload(err.APIError) 45 } 46 return systemApi.NewAdminInfoOK().WithPayload(infoResp) 47 }) 48 // return single widget results 49 api.SystemDashboardWidgetDetailsHandler = systemApi.DashboardWidgetDetailsHandlerFunc(func(params systemApi.DashboardWidgetDetailsParams, _ *models.Principal) middleware.Responder { 50 infoResp, err := getAdminInfoWidgetResponse(params) 51 if err != nil { 52 return systemApi.NewDashboardWidgetDetailsDefault(err.Code).WithPayload(err.APIError) 53 } 54 return systemApi.NewDashboardWidgetDetailsOK().WithPayload(infoResp) 55 }) 56 } 57 58 type UsageInfo struct { 59 Buckets int64 60 Objects int64 61 Usage int64 62 DrivesUsage int64 63 Servers []*models.ServerProperties 64 EndpointNotReady bool 65 Backend *models.BackendProperties 66 } 67 68 // GetAdminInfo invokes admin info and returns a parsed `UsageInfo` structure 69 func GetAdminInfo(ctx context.Context, client MinioAdmin) (*UsageInfo, error) { 70 serverInfo, err := client.serverInfo(ctx) 71 if err != nil { 72 return nil, err 73 } 74 // we are trimming uint64 to int64 this will report an incorrect measurement for numbers greater than 75 // 9,223,372,036,854,775,807 76 77 backendType := serverInfo.Backend.Type 78 rrSCParity := serverInfo.Backend.RRSCParity 79 standardSCParity := serverInfo.Backend.StandardSCParity 80 onlineDrives := serverInfo.Backend.OnlineDisks 81 offlineDrives := serverInfo.Backend.OfflineDisks 82 83 var usedSpace int64 84 // serverArray contains the serverProperties which describe the servers in the network 85 var serverArray []*models.ServerProperties 86 for _, serv := range serverInfo.Servers { 87 drives := []*models.ServerDrives{} 88 89 for _, drive := range serv.Disks { 90 usedSpace += int64(drive.UsedSpace) 91 drives = append(drives, &models.ServerDrives{ 92 State: drive.State, 93 UUID: drive.UUID, 94 Endpoint: drive.Endpoint, 95 RootDisk: drive.RootDisk, 96 DrivePath: drive.DrivePath, 97 Healing: drive.Healing, 98 Model: drive.Model, 99 TotalSpace: int64(drive.TotalSpace), 100 UsedSpace: int64(drive.UsedSpace), 101 AvailableSpace: int64(drive.AvailableSpace), 102 }) 103 } 104 105 newServer := &models.ServerProperties{ 106 State: serv.State, 107 Endpoint: serv.Endpoint, 108 Uptime: serv.Uptime, 109 Version: serv.Version, 110 CommitID: serv.CommitID, 111 PoolNumber: int64(serv.PoolNumber), 112 Network: serv.Network, 113 Drives: drives, 114 } 115 116 serverArray = append(serverArray, newServer) 117 } 118 119 backendData := &models.BackendProperties{ 120 BackendType: string(backendType), 121 RrSCParity: int64(rrSCParity), 122 StandardSCParity: int64(standardSCParity), 123 OnlineDrives: int64(onlineDrives), 124 OfflineDrives: int64(offlineDrives), 125 } 126 return &UsageInfo{ 127 Buckets: int64(serverInfo.Buckets.Count), 128 Objects: int64(serverInfo.Objects.Count), 129 Usage: int64(serverInfo.Usage.Size), 130 DrivesUsage: usedSpace, 131 Servers: serverArray, 132 Backend: backendData, 133 }, nil 134 } 135 136 type Target struct { 137 Expr string 138 Interval string 139 LegendFormat string 140 Step int32 141 InitialTime int64 142 } 143 144 type ReduceOptions struct { 145 Calcs []string 146 } 147 148 type MetricOptions struct { 149 ReduceOptions ReduceOptions 150 } 151 152 type Metric struct { 153 ID int32 154 Title string 155 Type string 156 Options MetricOptions 157 Targets []Target 158 GridPos GridPos 159 MaxDataPoints int32 160 } 161 162 type GridPos struct { 163 H int32 164 W int32 165 X int32 166 Y int32 167 } 168 169 type WidgetLabel struct { 170 Name string 171 } 172 173 var labels = []WidgetLabel{ 174 {Name: "instance"}, 175 {Name: "drive"}, 176 {Name: "server"}, 177 {Name: "api"}, 178 } 179 180 var widgets = []Metric{ 181 { 182 ID: 1, 183 Title: "Uptime", 184 Type: "stat", 185 MaxDataPoints: 100, 186 GridPos: GridPos{ 187 H: 6, 188 W: 3, 189 X: 0, 190 Y: 0, 191 }, 192 Options: MetricOptions{ 193 ReduceOptions: ReduceOptions{ 194 Calcs: []string{ 195 "mean", 196 }, 197 }, 198 }, 199 Targets: []Target{ 200 { 201 Expr: `time() - max(minio_node_process_starttime_seconds{$__query})`, 202 LegendFormat: "{{instance}}", 203 Step: 60, 204 }, 205 }, 206 }, 207 { 208 ID: 65, 209 Title: "Total S3 Traffic Inbound", 210 Type: "stat", 211 MaxDataPoints: 100, 212 GridPos: GridPos{ 213 H: 3, 214 W: 3, 215 X: 3, 216 Y: 0, 217 }, 218 Options: MetricOptions{ 219 ReduceOptions: ReduceOptions{ 220 Calcs: []string{ 221 "last", 222 }, 223 }, 224 }, 225 Targets: []Target{ 226 { 227 Expr: `sum by (instance) (minio_s3_traffic_received_bytes{$__query})`, 228 LegendFormat: "{{instance}}", 229 Step: 60, 230 }, 231 }, 232 }, 233 { 234 ID: 50, 235 Title: "Current Usable Free Capacity", 236 Type: "gauge", 237 MaxDataPoints: 100, 238 GridPos: GridPos{ 239 H: 6, 240 W: 3, 241 X: 6, 242 Y: 0, 243 }, 244 Options: MetricOptions{ 245 ReduceOptions: ReduceOptions{ 246 Calcs: []string{ 247 "lastNotNull", 248 }, 249 }, 250 }, 251 Targets: []Target{ 252 { 253 Expr: `topk(1, sum(minio_cluster_capacity_usable_total_bytes{$__query}) by (instance))`, 254 LegendFormat: "Total Usable", 255 Step: 300, 256 }, 257 { 258 Expr: `topk(1, sum(minio_cluster_capacity_usable_free_bytes{$__query}) by (instance))`, 259 LegendFormat: "Usable Free", 260 Step: 300, 261 }, 262 { 263 Expr: `topk(1, sum(minio_cluster_capacity_usable_total_bytes{$__query}) by (instance)) - topk(1, sum(minio_cluster_capacity_usable_free_bytes{$__query}) by (instance))`, 264 LegendFormat: "Used Space", 265 Step: 300, 266 }, 267 }, 268 }, 269 { 270 ID: 51, 271 Title: "Current Usable Total Bytes", 272 Type: "gauge", 273 MaxDataPoints: 100, 274 GridPos: GridPos{ 275 H: 6, 276 W: 3, 277 X: 6, 278 Y: 0, 279 }, 280 Options: MetricOptions{ 281 ReduceOptions: ReduceOptions{ 282 Calcs: []string{ 283 "lastNotNull", 284 }, 285 }, 286 }, 287 Targets: []Target{ 288 { 289 Expr: `topk(1, sum(minio_cluster_capacity_usable_total_bytes{$__query}) by (instance))`, 290 LegendFormat: "", 291 Step: 300, 292 }, 293 }, 294 }, 295 { 296 ID: 68, 297 Title: "Data Usage Growth", 298 Type: "graph", 299 GridPos: GridPos{ 300 H: 6, 301 W: 7, 302 X: 9, 303 Y: 0, 304 }, 305 Targets: []Target{ 306 { 307 Expr: `minio_cluster_usage_total_bytes{$__query}`, 308 LegendFormat: "Used Capacity", 309 InitialTime: -180, 310 Step: 10, 311 }, 312 }, 313 }, 314 { 315 ID: 52, 316 Title: "Object size distribution", 317 Type: "bargauge", 318 GridPos: GridPos{ 319 H: 6, 320 W: 5, 321 X: 16, 322 Y: 0, 323 }, 324 Options: MetricOptions{ 325 ReduceOptions: ReduceOptions{ 326 Calcs: []string{ 327 "mean", 328 }, 329 }, 330 }, 331 Targets: []Target{ 332 { 333 Expr: `minio_cluster_objects_size_distribution{$__query}`, 334 LegendFormat: "{{range}}", 335 Step: 300, 336 }, 337 }, 338 }, 339 { 340 ID: 61, 341 Title: "Total Open FDs", 342 Type: "stat", 343 MaxDataPoints: 100, 344 GridPos: GridPos{ 345 H: 3, 346 W: 3, 347 X: 21, 348 Y: 0, 349 }, 350 Options: MetricOptions{ 351 ReduceOptions: ReduceOptions{ 352 Calcs: []string{ 353 "last", 354 }, 355 }, 356 }, 357 Targets: []Target{ 358 { 359 Expr: `sum(minio_node_file_descriptor_open_total{$__query})`, 360 LegendFormat: "", 361 Step: 60, 362 }, 363 }, 364 }, 365 { 366 ID: 64, 367 Title: "Total S3 Traffic Outbound", 368 Type: "stat", 369 MaxDataPoints: 100, 370 GridPos: GridPos{ 371 H: 3, 372 W: 3, 373 X: 3, 374 Y: 3, 375 }, 376 Options: MetricOptions{ 377 ReduceOptions: ReduceOptions{ 378 Calcs: []string{ 379 "last", 380 }, 381 }, 382 }, 383 Targets: []Target{ 384 { 385 Expr: `sum by (instance) (minio_s3_traffic_sent_bytes{$__query})`, 386 LegendFormat: "", 387 Step: 60, 388 }, 389 }, 390 }, 391 { 392 ID: 62, 393 Title: "Total Goroutines", 394 Type: "stat", 395 MaxDataPoints: 100, 396 GridPos: GridPos{ 397 H: 3, 398 W: 3, 399 X: 21, 400 Y: 3, 401 }, 402 Options: MetricOptions{ 403 ReduceOptions: ReduceOptions{ 404 Calcs: []string{ 405 "last", 406 }, 407 }, 408 }, 409 Targets: []Target{ 410 { 411 Expr: `sum without (server,instance) (minio_node_go_routine_total{$__query})`, 412 LegendFormat: "", 413 Step: 60, 414 }, 415 }, 416 }, 417 { 418 ID: 53, 419 Title: "Total Online Servers", 420 Type: "stat", 421 MaxDataPoints: 100, 422 GridPos: GridPos{ 423 H: 2, 424 W: 3, 425 X: 0, 426 Y: 6, 427 }, 428 Options: MetricOptions{ 429 ReduceOptions: ReduceOptions{ 430 Calcs: []string{ 431 "mean", 432 }, 433 }, 434 }, 435 Targets: []Target{ 436 { 437 Expr: `minio_cluster_nodes_online_total{$__query}`, 438 LegendFormat: "", 439 Step: 60, 440 }, 441 }, 442 }, 443 { 444 ID: 9, 445 Title: "Total Online Drives", 446 Type: "stat", 447 MaxDataPoints: 100, 448 GridPos: GridPos{ 449 H: 2, 450 W: 3, 451 X: 3, 452 Y: 6, 453 }, 454 Options: MetricOptions{ 455 ReduceOptions: ReduceOptions{ 456 Calcs: []string{ 457 "mean", 458 }, 459 }, 460 }, 461 Targets: []Target{ 462 { 463 Expr: `minio_cluster_drive_online_total{$__query}`, 464 LegendFormat: "Total online drives in MinIO Cluster", 465 Step: 60, 466 }, 467 }, 468 }, 469 { 470 ID: 66, 471 Title: "Number of Buckets", 472 Type: "stat", 473 MaxDataPoints: 5, 474 GridPos: GridPos{ 475 H: 3, 476 W: 3, 477 X: 6, 478 Y: 6, 479 }, 480 Options: MetricOptions{ 481 ReduceOptions: ReduceOptions{ 482 Calcs: []string{ 483 "lastNotNull", 484 }, 485 }, 486 }, 487 Targets: []Target{ 488 { 489 Expr: `minio_cluster_bucket_total{$__query}`, 490 LegendFormat: "", 491 Step: 100, 492 }, 493 }, 494 }, 495 { 496 ID: 63, 497 Title: "S3 API Data Received Rate ", 498 Type: "graph", 499 GridPos: GridPos{ 500 H: 6, 501 W: 7, 502 X: 9, 503 Y: 6, 504 }, 505 Targets: []Target{ 506 { 507 Expr: `sum by (server) (rate(minio_s3_traffic_received_bytes{$__query}[$__rate_interval]))`, 508 LegendFormat: "Data Received [{{server}}]", 509 }, 510 }, 511 }, 512 { 513 ID: 70, 514 Title: "S3 API Data Sent Rate ", 515 Type: "graph", 516 GridPos: GridPos{ 517 H: 6, 518 W: 8, 519 X: 16, 520 Y: 6, 521 }, 522 Targets: []Target{ 523 { 524 Expr: `sum by (server) (rate(minio_s3_traffic_sent_bytes{$__query}[$__rate_interval]))`, 525 LegendFormat: "Data Sent [{{server}}]", 526 }, 527 }, 528 }, 529 { 530 ID: 69, 531 Title: "Total Offline Servers", 532 Type: "stat", 533 MaxDataPoints: 100, 534 GridPos: GridPos{ 535 H: 2, 536 W: 3, 537 X: 0, 538 Y: 8, 539 }, 540 Options: MetricOptions{ 541 ReduceOptions: ReduceOptions{ 542 Calcs: []string{ 543 "mean", 544 }, 545 }, 546 }, 547 Targets: []Target{ 548 { 549 Expr: `minio_cluster_nodes_offline_total{$__query}`, 550 LegendFormat: "", 551 Step: 60, 552 }, 553 }, 554 }, 555 { 556 ID: 78, 557 Title: "Total Offline Drives", 558 Type: "stat", 559 MaxDataPoints: 100, 560 GridPos: GridPos{ 561 H: 2, 562 W: 3, 563 X: 3, 564 Y: 8, 565 }, 566 Options: MetricOptions{ 567 ReduceOptions: ReduceOptions{ 568 Calcs: []string{ 569 "mean", 570 }, 571 }, 572 }, 573 Targets: []Target{ 574 { 575 Expr: `minio_cluster_drive_offline_total{$__query}`, 576 LegendFormat: "", 577 Step: 60, 578 }, 579 }, 580 }, 581 { 582 ID: 44, 583 Title: "Number of Objects", 584 Type: "stat", 585 MaxDataPoints: 100, 586 GridPos: GridPos{ 587 H: 3, 588 W: 3, 589 X: 6, 590 Y: 9, 591 }, 592 Options: MetricOptions{ 593 ReduceOptions: ReduceOptions{ 594 Calcs: []string{ 595 "lastNotNull", 596 }, 597 }, 598 }, 599 Targets: []Target{ 600 { 601 Expr: `minio_cluster_usage_object_total{$__query}`, 602 LegendFormat: "", 603 }, 604 }, 605 }, 606 { 607 ID: 80, 608 Title: "Time Since Last Heal Activity", 609 Type: "stat", 610 MaxDataPoints: 100, 611 GridPos: GridPos{ 612 H: 2, 613 W: 3, 614 X: 0, 615 Y: 10, 616 }, 617 Options: MetricOptions{ 618 ReduceOptions: ReduceOptions{ 619 Calcs: []string{ 620 "last", 621 }, 622 }, 623 }, 624 Targets: []Target{ 625 { 626 Expr: `minio_heal_time_last_activity_nano_seconds{$__query}`, 627 LegendFormat: "{{server}}", 628 Step: 60, 629 }, 630 }, 631 }, 632 { 633 ID: 81, 634 Title: "Time Since Last Scan Activity", 635 Type: "stat", 636 MaxDataPoints: 100, 637 GridPos: GridPos{ 638 H: 2, 639 W: 3, 640 X: 3, 641 Y: 10, 642 }, 643 Options: MetricOptions{ 644 ReduceOptions: ReduceOptions{ 645 Calcs: []string{ 646 "last", 647 }, 648 }, 649 }, 650 Targets: []Target{ 651 { 652 Expr: `minio_usage_last_activity_nano_seconds{$__query}`, 653 LegendFormat: "{{server}}", 654 Step: 60, 655 }, 656 }, 657 }, 658 { 659 ID: 60, 660 Title: "S3 API Request Rate", 661 Type: "graph", 662 GridPos: GridPos{ 663 H: 10, 664 W: 12, 665 X: 0, 666 Y: 12, 667 }, 668 Targets: []Target{ 669 { 670 Expr: `sum by (server,api) (increase(minio_s3_requests_total{$__query}[$__rate_interval]))`, 671 LegendFormat: "{{server,api}}", 672 }, 673 }, 674 }, 675 { 676 ID: 71, 677 Title: "S3 API Request Error Rate", 678 Type: "graph", 679 GridPos: GridPos{ 680 H: 10, 681 W: 12, 682 X: 12, 683 Y: 12, 684 }, 685 Targets: []Target{ 686 { 687 Expr: `sum by (server,api) (increase(minio_s3_requests_errors_total{$__query}[$__rate_interval]))`, 688 LegendFormat: "{{server,api}}", 689 }, 690 }, 691 }, 692 { 693 ID: 17, 694 Title: "Internode Data Transfer", 695 Type: "graph", 696 GridPos: GridPos{ 697 H: 8, 698 W: 24, 699 X: 0, 700 Y: 22, 701 }, 702 Targets: []Target{ 703 { 704 Expr: `rate(minio_inter_node_traffic_sent_bytes{$__query}[$__rate_interval])`, 705 LegendFormat: "Internode Bytes Received [{{server}}]", 706 Step: 4, 707 }, 708 709 { 710 Expr: `rate(minio_inter_node_traffic_sent_bytes{$__query}[$__rate_interval])`, 711 LegendFormat: "Internode Bytes Received [{{server}}]", 712 }, 713 }, 714 }, 715 { 716 ID: 77, 717 Title: "Node CPU Usage", 718 Type: "graph", 719 GridPos: GridPos{ 720 H: 9, 721 W: 12, 722 X: 0, 723 Y: 30, 724 }, 725 Targets: []Target{ 726 { 727 Expr: `rate(minio_node_process_cpu_total_seconds{$__query}[$__rate_interval])`, 728 LegendFormat: "CPU Usage Rate [{{server}}]", 729 }, 730 }, 731 }, 732 { 733 ID: 76, 734 Title: "Node Memory Usage", 735 Type: "graph", 736 GridPos: GridPos{ 737 H: 9, 738 W: 12, 739 X: 12, 740 Y: 30, 741 }, 742 Targets: []Target{ 743 { 744 Expr: `minio_node_process_resident_memory_bytes{$__query}`, 745 LegendFormat: "Memory Used [{{server}}]", 746 }, 747 }, 748 }, 749 { 750 ID: 74, 751 Title: "Drive Used Capacity", 752 Type: "graph", 753 GridPos: GridPos{ 754 H: 8, 755 W: 12, 756 X: 0, 757 Y: 39, 758 }, 759 Targets: []Target{ 760 { 761 Expr: `minio_node_drive_used_bytes{$__query}`, 762 LegendFormat: "Used Capacity [{{server}}:{{drive}}]", 763 }, 764 }, 765 }, 766 { 767 ID: 82, 768 Title: "Drives Free Inodes", 769 Type: "graph", 770 GridPos: GridPos{ 771 H: 8, 772 W: 12, 773 X: 12, 774 Y: 39, 775 }, 776 Targets: []Target{ 777 { 778 Expr: `minio_node_drive_free_inodes{$__query}`, 779 LegendFormat: "Free Inodes [{{server}}:{{drive}}]", 780 }, 781 }, 782 }, 783 { 784 ID: 11, 785 Title: "Node Syscalls", 786 Type: "graph", 787 GridPos: GridPos{ 788 H: 9, 789 W: 12, 790 X: 0, 791 Y: 47, 792 }, 793 Targets: []Target{ 794 { 795 Expr: `rate(minio_node_syscall_read_total{$__query}[$__rate_interval])`, 796 LegendFormat: "Read Syscalls [{{server}}]", 797 Step: 60, 798 }, 799 800 { 801 Expr: `rate(minio_node_syscall_read_total{$__query}[$__rate_interval])`, 802 LegendFormat: "Read Syscalls [{{server}}]", 803 }, 804 }, 805 }, 806 { 807 ID: 8, 808 Title: "Node File Descriptors", 809 Type: "graph", 810 GridPos: GridPos{ 811 H: 9, 812 W: 12, 813 X: 12, 814 Y: 47, 815 }, 816 Targets: []Target{ 817 { 818 Expr: `minio_node_file_descriptor_open_total{$__query}`, 819 LegendFormat: "Open FDs [{{server}}]", 820 }, 821 }, 822 }, 823 { 824 ID: 73, 825 Title: "Node IO", 826 Type: "graph", 827 GridPos: GridPos{ 828 H: 8, 829 W: 24, 830 X: 0, 831 Y: 56, 832 }, 833 Targets: []Target{ 834 { 835 Expr: `rate(minio_node_io_rchar_bytes{$__query}[$__rate_interval])`, 836 LegendFormat: "Node RChar [{{server}}]", 837 }, 838 839 { 840 Expr: `rate(minio_node_io_wchar_bytes{$__query}[$__rate_interval])`, 841 LegendFormat: "Node WChar [{{server}}]", 842 }, 843 }, 844 }, 845 } 846 847 type Widget struct { 848 Title string 849 Type string 850 } 851 852 type DataResult struct { 853 Metric map[string]string `json:"metric"` 854 Values []interface{} `json:"values"` 855 } 856 857 type PromRespData struct { 858 ResultType string `json:"resultType"` 859 Result []DataResult `json:"result"` 860 } 861 862 type PromResp struct { 863 Status string `json:"status"` 864 Data PromRespData `json:"data"` 865 } 866 867 type LabelResponse struct { 868 Status string `json:"status"` 869 Data []string `json:"data"` 870 } 871 872 type LabelResults struct { 873 Label string 874 Response LabelResponse 875 } 876 877 // getAdminInfoResponse returns the response containing total buckets, objects and usage. 878 func getAdminInfoResponse(session *models.Principal, params systemApi.AdminInfoParams) (*models.AdminInfoResponse, *CodedAPIError) { 879 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 880 defer cancel() 881 prometheusURL := "" 882 883 if !*params.DefaultOnly { 884 promURL := getPrometheusURL() 885 if promURL != "" { 886 prometheusURL = promURL 887 } 888 } 889 890 mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session) 891 if err != nil { 892 return nil, ErrorWithContext(ctx, err) 893 } 894 895 sessionResp, err2 := getUsageWidgetsForDeployment(ctx, prometheusURL, AdminClient{Client: mAdmin}) 896 if err2 != nil { 897 return nil, ErrorWithContext(ctx, err2) 898 } 899 900 return sessionResp, nil 901 } 902 903 func getUsageWidgetsForDeployment(ctx context.Context, prometheusURL string, adminClient MinioAdmin) (*models.AdminInfoResponse, error) { 904 prometheusStatus := models.AdminInfoResponseAdvancedMetricsStatusAvailable 905 if prometheusURL == "" { 906 prometheusStatus = models.AdminInfoResponseAdvancedMetricsStatusNotConfigured 907 } 908 if prometheusURL != "" && !testPrometheusURL(ctx, prometheusURL) { 909 prometheusStatus = models.AdminInfoResponseAdvancedMetricsStatusUnavailable 910 } 911 sessionResp := &models.AdminInfoResponse{ 912 AdvancedMetricsStatus: prometheusStatus, 913 } 914 doneCh := make(chan error) 915 go func() { 916 defer close(doneCh) 917 // serialize output 918 usage, err := GetAdminInfo(ctx, adminClient) 919 if err != nil { 920 doneCh <- err 921 } 922 if usage != nil { 923 sessionResp.Buckets = usage.Buckets 924 sessionResp.Objects = usage.Objects 925 sessionResp.Usage = usage.Usage 926 sessionResp.Servers = usage.Servers 927 sessionResp.Backend = usage.Backend 928 } 929 }() 930 931 var wdgts []*models.Widget 932 if prometheusStatus == models.AdminInfoResponseAdvancedMetricsStatusAvailable { 933 // We will tell the frontend about a list of widgets so it can fetch the ones it wants 934 for _, m := range widgets { 935 wdgtResult := models.Widget{ 936 ID: m.ID, 937 Title: m.Title, 938 Type: m.Type, 939 } 940 if len(m.Options.ReduceOptions.Calcs) > 0 { 941 wdgtResult.Options = &models.WidgetOptions{ 942 ReduceOptions: &models.WidgetOptionsReduceOptions{ 943 Calcs: m.Options.ReduceOptions.Calcs, 944 }, 945 } 946 } 947 948 wdgts = append(wdgts, &wdgtResult) 949 } 950 sessionResp.Widgets = wdgts 951 } 952 953 // wait for mc admin info 954 err := <-doneCh 955 if err != nil { 956 return nil, err 957 } 958 959 return sessionResp, nil 960 } 961 962 func unmarshalPrometheus(ctx context.Context, httpClnt *http.Client, endpoint string, data interface{}) bool { 963 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) 964 if err != nil { 965 ErrorWithContext(ctx, fmt.Errorf("Unable to create the request to fetch labels from prometheus: %w", err)) 966 return true 967 } 968 969 prometheusBearer := getPrometheusAuthToken() 970 971 if prometheusBearer != "" { 972 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", prometheusBearer)) 973 } 974 975 resp, err := httpClnt.Do(req) 976 if err != nil { 977 ErrorWithContext(ctx, fmt.Errorf("Unable to fetch labels from prometheus: %w", err)) 978 return true 979 } 980 981 defer resp.Body.Close() 982 983 if resp.StatusCode != http.StatusOK { 984 ErrorWithContext(ctx, fmt.Errorf("Unexpected status code from prometheus (%s)", resp.Status)) 985 return true 986 } 987 988 if err = json.NewDecoder(resp.Body).Decode(data); err != nil { 989 ErrorWithContext(ctx, fmt.Errorf("Unexpected error from prometheus: %w", err)) 990 return true 991 } 992 993 return false 994 } 995 996 func testPrometheusURL(ctx context.Context, url string) bool { 997 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+"/-/healthy", nil) 998 if err != nil { 999 ErrorWithContext(ctx, fmt.Errorf("error Building Request: (%v)", err)) 1000 return false 1001 } 1002 1003 prometheusBearer := getPrometheusAuthToken() 1004 if prometheusBearer != "" { 1005 req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", prometheusBearer)) 1006 } 1007 1008 clientIP := utils.ClientIPFromContext(ctx) 1009 httpClnt := GetConsoleHTTPClient(clientIP) 1010 1011 response, err := httpClnt.Do(req) 1012 if err != nil { 1013 ErrorWithContext(ctx, fmt.Errorf("default Prometheus URL not reachable, trying root testing: (%v)", err)) 1014 newTestURL := req.URL.Scheme + "://" + req.URL.Host + "/-/healthy" 1015 req2, err := http.NewRequestWithContext(ctx, http.MethodGet, newTestURL, nil) 1016 if err != nil { 1017 ErrorWithContext(ctx, fmt.Errorf("error Building Root Request: (%v)", err)) 1018 return false 1019 } 1020 rootResponse, err := httpClnt.Do(req2) 1021 if err != nil { 1022 // URL & Root tests didn't work. Prometheus not reachable 1023 ErrorWithContext(ctx, fmt.Errorf("root Prometheus URL not reachable: (%v)", err)) 1024 return false 1025 } 1026 return rootResponse.StatusCode == http.StatusOK 1027 } 1028 return response.StatusCode == http.StatusOK 1029 } 1030 1031 func getAdminInfoWidgetResponse(params systemApi.DashboardWidgetDetailsParams) (*models.WidgetDetails, *CodedAPIError) { 1032 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 1033 defer cancel() 1034 prometheusURL := getPrometheusURL() 1035 prometheusJobID := getPrometheusJobID() 1036 prometheusExtraLabels := getPrometheusExtraLabels() 1037 1038 selector := fmt.Sprintf(`job="%s"`, prometheusJobID) 1039 if strings.TrimSpace(prometheusExtraLabels) != "" { 1040 selector = fmt.Sprintf(`job="%s",%s`, prometheusJobID, prometheusExtraLabels) 1041 } 1042 clientIP := getClientIP(params.HTTPRequest) 1043 ctx = context.WithValue(ctx, utils.ContextClientIP, clientIP) 1044 return getWidgetDetails(ctx, prometheusURL, selector, params.WidgetID, params.Step, params.Start, params.End) 1045 } 1046 1047 func getWidgetDetails(ctx context.Context, prometheusURL string, selector string, widgetID int32, step *int32, start *int64, end *int64) (*models.WidgetDetails, *CodedAPIError) { 1048 // We test if prometheus URL is reachable. this is meant to avoid unuseful calls and application hang. 1049 if !testPrometheusURL(ctx, prometheusURL) { 1050 return nil, ErrorWithContext(ctx, errors.New("prometheus URL is unreachable")) 1051 } 1052 clientIP := utils.ClientIPFromContext(ctx) 1053 httpClnt := GetConsoleHTTPClient(clientIP) 1054 1055 labelResultsCh := make(chan LabelResults) 1056 1057 for _, lbl := range labels { 1058 go func(lbl WidgetLabel) { 1059 endpoint := fmt.Sprintf("%s/api/v1/label/%s/values", prometheusURL, lbl.Name) 1060 1061 var response LabelResponse 1062 if unmarshalPrometheus(ctx, httpClnt, endpoint, &response) { 1063 return 1064 } 1065 1066 labelResultsCh <- LabelResults{Label: lbl.Name, Response: response} 1067 }(lbl) 1068 } 1069 1070 labelMap := make(map[string][]string) 1071 1072 // wait for as many goroutines that come back in less than 1 second 1073 LabelsWaitLoop: 1074 for { 1075 select { 1076 case <-time.After(1 * time.Second): 1077 break LabelsWaitLoop 1078 case res := <-labelResultsCh: 1079 labelMap[res.Label] = res.Response.Data 1080 if len(labelMap) >= len(labels) { 1081 break LabelsWaitLoop 1082 } 1083 } 1084 } 1085 1086 // launch a goroutines per widget 1087 1088 for _, m := range widgets { 1089 if m.ID != widgetID { 1090 continue 1091 } 1092 1093 var ( 1094 wg sync.WaitGroup 1095 targetResults = make([]*models.ResultTarget, len(m.Targets)) 1096 ) 1097 1098 // for each target we will launch another goroutine to fetch the values 1099 for idx, target := range m.Targets { 1100 wg.Add(1) 1101 go func(idx int, target Target, inStep *int32, inStart *int64, inEnd *int64) { 1102 defer wg.Done() 1103 1104 apiType := "query_range" 1105 now := time.Now() 1106 1107 var initTime int64 = -15 1108 1109 if target.InitialTime != 0 { 1110 initTime = target.InitialTime 1111 } 1112 1113 timeCalculated := time.Duration(initTime * int64(time.Minute)) 1114 1115 extraParamters := fmt.Sprintf("&start=%d&end=%d", now.Add(timeCalculated).Unix(), now.Unix()) 1116 1117 var step int32 = 60 1118 if target.Step > 0 { 1119 step = target.Step 1120 } 1121 if inStep != nil && *inStep > 0 { 1122 step = *inStep 1123 } 1124 if step > 0 { 1125 extraParamters = fmt.Sprintf("%s&step=%d", extraParamters, step) 1126 } 1127 1128 if inStart != nil && inEnd != nil { 1129 extraParamters = fmt.Sprintf("&start=%d&end=%d&step=%d", *inStart, *inEnd, *inStep) 1130 } 1131 1132 // replace the `$__rate_interval` global for step with unit (s for seconds) 1133 queryExpr := strings.ReplaceAll(target.Expr, "$__rate_interval", fmt.Sprintf("%ds", 240)) 1134 if strings.Contains(queryExpr, "$") { 1135 re := regexp.MustCompile(`\$([a-z]+)`) 1136 1137 for _, match := range re.FindAllStringSubmatch(queryExpr, -1) { 1138 if val, ok := labelMap[match[1]]; ok { 1139 queryExpr = strings.ReplaceAll(queryExpr, "$"+match[1], fmt.Sprintf("(%s)", strings.Join(val, "|"))) 1140 } 1141 } 1142 } 1143 1144 queryExpr = strings.ReplaceAll(queryExpr, "$__query", selector) 1145 endpoint := fmt.Sprintf("%s/api/v1/%s?query=%s%s", prometheusURL, apiType, url.QueryEscape(queryExpr), extraParamters) 1146 1147 var response PromResp 1148 if unmarshalPrometheus(ctx, httpClnt, endpoint, &response) { 1149 return 1150 } 1151 1152 targetResult := models.ResultTarget{ 1153 LegendFormat: target.LegendFormat, 1154 ResultType: response.Data.ResultType, 1155 } 1156 1157 for _, r := range response.Data.Result { 1158 targetResult.Result = append(targetResult.Result, &models.WidgetResult{ 1159 Metric: r.Metric, 1160 Values: r.Values, 1161 }) 1162 } 1163 1164 targetResults[idx] = &targetResult 1165 }(idx, target, step, start, end) 1166 } 1167 1168 wg.Wait() 1169 1170 wdgtResult := models.WidgetDetails{ 1171 ID: m.ID, 1172 Title: m.Title, 1173 Type: m.Type, 1174 } 1175 if len(m.Options.ReduceOptions.Calcs) > 0 { 1176 wdgtResult.Options = &models.WidgetDetailsOptions{ 1177 ReduceOptions: &models.WidgetDetailsOptionsReduceOptions{ 1178 Calcs: m.Options.ReduceOptions.Calcs, 1179 }, 1180 } 1181 } 1182 1183 for _, res := range targetResults { 1184 if res != nil { 1185 wdgtResult.Targets = append(wdgtResult.Targets, res) 1186 } 1187 } 1188 return &wdgtResult, nil 1189 } 1190 1191 return nil, &CodedAPIError{Code: 404, APIError: &models.APIError{Message: "Widget not found"}} 1192 }