github.com/minio/console@v1.4.1/web-app/src/screens/Console/Dashboard/Prometheus/utils.tsx (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 import React, { Fragment } from "react"; 18 import get from "lodash/get"; 19 import { IDashboardPanel, widgetType } from "./types"; 20 import { 21 getTimeFromTimestamp, 22 niceBytes, 23 niceDays, 24 representationNumber, 25 textToRGBColor, 26 units, 27 } from "../../../../common/utils"; 28 import { DiagnosticsIcon, HealIcon, UptimeIcon } from "mds"; 29 30 const colorsMain = [ 31 "#C4D4E9", 32 "#DCD1EE", 33 "#D1EEE7", 34 "#EEDED1", 35 "#AAF38F", 36 "#F9E6C5", 37 "#C83B51", 38 "#F4CECE", 39 "#D6D6D6", 40 ]; 41 42 const niceDaysFromNS = (seconds: string) => { 43 return niceDays(seconds, "ns"); 44 }; 45 46 const roundNumber = (value: string) => { 47 return parseInt(value).toString(10); 48 }; 49 50 export const panelsConfiguration: IDashboardPanel[] = [ 51 { 52 id: 1, 53 title: "Uptime", 54 data: "N/A", 55 type: widgetType.simpleWidget, 56 widgetIcon: <UptimeIcon />, 57 labelDisplayFunction: niceDays, 58 }, 59 { 60 id: 50, 61 title: "Capacity", 62 data: [], 63 dataOuter: [{ name: "outer", value: 100 }], 64 widgetConfiguration: { 65 outerChart: { 66 colorList: ["#9c9c9c"], 67 innerRadius: 0, 68 outerRadius: 0, 69 startAngle: 0, 70 endAngle: 0, 71 }, 72 innerChart: { 73 colorList: colorsMain, 74 innerRadius: 20, 75 outerRadius: 50, 76 startAngle: 90, 77 endAngle: -200, 78 }, 79 }, 80 type: widgetType.pieChart, 81 innerLabel: "N/A", 82 labelDisplayFunction: niceBytes, 83 }, 84 { 85 id: 51, 86 title: "Usable Capacity", 87 data: [], 88 dataOuter: [{ name: "outer", value: 100 }], 89 widgetConfiguration: { 90 outerChart: { 91 colorList: ["#9c9c9c"], 92 innerRadius: 0, 93 outerRadius: 0, 94 startAngle: 0, 95 endAngle: 0, 96 }, 97 innerChart: { 98 colorList: colorsMain, 99 innerRadius: 20, 100 outerRadius: 50, 101 startAngle: 90, 102 endAngle: -200, 103 }, 104 }, 105 type: widgetType.pieChart, 106 innerLabel: "N/A", 107 labelDisplayFunction: niceBytes, 108 }, 109 { 110 id: 68, 111 title: "Data Usage Growth", 112 data: [], 113 widgetConfiguration: [ 114 { 115 dataKey: "", 116 keyLabel: "", 117 lineColor: "#000", 118 fillColor: "#000", 119 }, 120 ], 121 type: widgetType.areaGraph, 122 yAxisFormatter: niceBytes, 123 xAxisFormatter: getTimeFromTimestamp, 124 }, 125 { 126 id: 52, 127 title: "Object size distribution", 128 data: [], 129 widgetConfiguration: [ 130 { 131 dataKey: "a", 132 color: "#2781B0", 133 background: { 134 fill: "#EEF1F4", 135 }, 136 greatestColor: "#081C42", 137 }, 138 ], 139 customStructure: [ 140 { originTag: "LESS_THAN_1024_B", displayTag: "Less than 1024B" }, 141 { 142 originTag: "BETWEEN_1024_B_AND_1_MB", 143 displayTag: "Between 1024B and 1MB", 144 }, 145 { 146 originTag: "BETWEEN_1_MB_AND_10_MB", 147 displayTag: "Between 1MB and 10MB", 148 }, 149 { 150 originTag: "BETWEEN_10_MB_AND_64_MB", 151 displayTag: "Between 10MB and 64MB", 152 }, 153 { 154 originTag: "BETWEEN_64_MB_AND_128_MB", 155 displayTag: "Between 64MB and 128MB", 156 }, 157 { 158 originTag: "BETWEEN_128_MB_AND_512_MB", 159 displayTag: "Between 128MB and 512MB", 160 }, 161 { 162 originTag: "GREATER_THAN_512_MB", 163 displayTag: "Greater than 512MB", 164 }, 165 ], 166 type: widgetType.barChart, 167 }, 168 { 169 id: 66, 170 title: "Buckets", 171 data: [], 172 innerLabel: "N/A", 173 type: widgetType.singleRep, 174 color: "#0071BC", 175 fillColor: "#ADD5E0", 176 }, 177 { 178 id: 44, 179 title: "Objects", 180 data: [], 181 innerLabel: "N/A", 182 type: widgetType.singleRep, 183 color: "#0071BC", 184 fillColor: "#ADD5E0", 185 }, 186 { 187 id: 63, 188 title: "API Data Received Rate", 189 data: [], 190 widgetConfiguration: [ 191 { 192 dataKey: "", 193 keyLabel: "", 194 lineColor: "#000", 195 fillColor: "#000", 196 strokeWidth: 3, 197 }, 198 ], 199 type: widgetType.linearGraph, 200 201 xAxisFormatter: getTimeFromTimestamp, 202 yAxisFormatter: niceBytes, 203 }, 204 { 205 id: 61, 206 title: "Total Open FDs", 207 data: [], 208 innerLabel: "N/A", 209 type: widgetType.singleRep, 210 color: "#22B573", 211 fillColor: "#A6E8C4", 212 }, 213 { 214 id: 62, 215 title: "Total Goroutines", 216 data: [], 217 innerLabel: "N/A", 218 type: widgetType.singleRep, 219 color: "#F7655E", 220 fillColor: "#F4CECE", 221 }, 222 { 223 id: 77, 224 title: "Node CPU Usage", 225 data: [], 226 widgetConfiguration: [ 227 { 228 dataKey: "", 229 keyLabel: "", 230 lineColor: "#000", 231 fillColor: "#000", 232 }, 233 ], 234 type: widgetType.linearGraph, 235 236 yAxisFormatter: roundNumber, 237 xAxisFormatter: getTimeFromTimestamp, 238 }, 239 { 240 id: 60, 241 title: "API Request Rate", 242 data: [], 243 widgetConfiguration: [ 244 { 245 dataKey: "", 246 keyLabel: "", 247 lineColor: "#000", 248 fillColor: "#000", 249 }, 250 ], 251 type: widgetType.linearGraph, 252 yAxisFormatter: roundNumber, 253 xAxisFormatter: getTimeFromTimestamp, 254 }, 255 { 256 id: 70, 257 title: "API Data Sent Rate", 258 data: [], 259 widgetConfiguration: [ 260 { 261 dataKey: "", 262 keyLabel: "", 263 lineColor: "#000", 264 fillColor: "#000", 265 }, 266 ], 267 type: widgetType.linearGraph, 268 269 xAxisFormatter: getTimeFromTimestamp, 270 yAxisFormatter: niceBytes, 271 }, 272 { 273 id: 17, 274 title: "Internode Data Transfer", 275 data: [], 276 widgetConfiguration: [ 277 { 278 dataKey: "", 279 keyLabel: "", 280 lineColor: "#000", 281 fillColor: "#000", 282 }, 283 ], 284 type: widgetType.linearGraph, 285 286 yAxisFormatter: niceBytes, 287 xAxisFormatter: getTimeFromTimestamp, 288 }, 289 { 290 id: 73, 291 title: "Node IO", 292 data: [], 293 widgetConfiguration: [ 294 { 295 dataKey: "", 296 keyLabel: "", 297 lineColor: "#000", 298 fillColor: "#000", 299 }, 300 ], 301 type: widgetType.linearGraph, 302 303 yAxisFormatter: niceBytes, 304 xAxisFormatter: getTimeFromTimestamp, 305 }, 306 { 307 id: 80, 308 title: "Time Since Last Heal Activity", 309 data: "N/A", 310 type: widgetType.simpleWidget, 311 widgetIcon: <HealIcon />, 312 labelDisplayFunction: niceDaysFromNS, 313 }, 314 { 315 id: 81, 316 title: "Time Since Last Scan Activity", 317 data: "N/A", 318 type: widgetType.simpleWidget, 319 widgetIcon: <DiagnosticsIcon />, 320 labelDisplayFunction: niceDaysFromNS, 321 }, 322 { 323 id: 71, 324 title: "API Request Error Rate", 325 data: [], 326 widgetConfiguration: [ 327 { 328 dataKey: "", 329 keyLabel: "", 330 lineColor: "#000", 331 fillColor: "#000", 332 }, 333 ], 334 type: widgetType.linearGraph, 335 336 xAxisFormatter: getTimeFromTimestamp, 337 }, 338 { 339 id: 76, 340 title: "Node Memory Usage", 341 data: [], 342 widgetConfiguration: [ 343 { 344 dataKey: "", 345 keyLabel: "", 346 lineColor: "#000", 347 fillColor: "#000", 348 }, 349 ], 350 type: widgetType.linearGraph, 351 352 xAxisFormatter: getTimeFromTimestamp, 353 yAxisFormatter: niceBytes, 354 }, 355 { 356 id: 74, 357 title: "Drive Used Capacity", 358 data: [], 359 widgetConfiguration: [ 360 { 361 dataKey: "", 362 keyLabel: "", 363 lineColor: "#000", 364 fillColor: "#000", 365 }, 366 ], 367 type: widgetType.linearGraph, 368 369 xAxisFormatter: getTimeFromTimestamp, 370 yAxisFormatter: niceBytes, 371 }, 372 { 373 id: 82, 374 title: "Drives Free Inodes", 375 data: [], 376 widgetConfiguration: [ 377 { 378 dataKey: "", 379 keyLabel: "", 380 lineColor: "#000", 381 fillColor: "#000", 382 }, 383 ], 384 type: widgetType.linearGraph, 385 386 disableYAxis: true, 387 xAxisFormatter: getTimeFromTimestamp, 388 }, 389 { 390 id: 11, 391 title: "Node Syscalls", 392 data: [], 393 widgetConfiguration: [ 394 { 395 dataKey: "", 396 keyLabel: "", 397 lineColor: "#000", 398 fillColor: "#000", 399 }, 400 ], 401 type: widgetType.linearGraph, 402 yAxisFormatter: roundNumber, 403 xAxisFormatter: getTimeFromTimestamp, 404 }, 405 { 406 id: 8, 407 title: "Node File Descriptors", 408 data: [], 409 widgetConfiguration: [ 410 { 411 dataKey: "", 412 keyLabel: "", 413 lineColor: "#000", 414 fillColor: "#000", 415 }, 416 ], 417 type: widgetType.linearGraph, 418 yAxisFormatter: roundNumber, 419 xAxisFormatter: getTimeFromTimestamp, 420 }, 421 { 422 id: 500, 423 mergedPanels: [ 424 { 425 id: 53, 426 title: "Online", 427 data: "N/A", 428 type: widgetType.singleValue, 429 }, 430 { 431 id: 69, 432 title: "Offline", 433 data: "N/A", 434 type: widgetType.singleValue, 435 }, 436 ], 437 title: "Servers", 438 }, 439 { 440 id: 501, 441 mergedPanels: [ 442 { 443 id: 9, 444 title: "Online", 445 data: "N/A", 446 type: widgetType.singleValue, 447 }, 448 { 449 id: 78, 450 title: "Offline", 451 data: "N/A", 452 type: widgetType.singleValue, 453 }, 454 ], 455 title: "Drives", 456 }, 457 { 458 id: 502, 459 mergedPanels: [ 460 { 461 id: 65, 462 title: "Upload", 463 data: "N/A", 464 type: widgetType.singleValue, 465 466 labelDisplayFunction: niceBytes, 467 }, 468 { 469 id: 64, 470 title: "Download", 471 data: "N/A", 472 type: widgetType.singleValue, 473 474 labelDisplayFunction: niceBytes, 475 }, 476 ], 477 title: "Network", 478 }, 479 ]; 480 481 const calculateMainValue = (elements: any[], metricCalc: string) => { 482 if (elements.length === 0) { 483 return ["", "0"]; 484 } 485 486 switch (metricCalc) { 487 case "mean": 488 const sumValues = elements.reduce((accumulator, currValue) => { 489 return accumulator + parseFloat(currValue[1]); 490 }, 0); 491 492 const mean = Math.floor(sumValues / elements.length); 493 494 return ["", mean.toString()]; 495 default: 496 const sortResult = elements.sort( 497 (value1: any[], value2: any[]) => value1[0] - value2[0], 498 ); 499 500 return sortResult[sortResult.length - 1]; 501 } 502 }; 503 504 const constructLabelNames = (metrics: any, legendFormat: string) => { 505 const keysToReplace = Object.keys(metrics); 506 const expToReplace = new RegExp(`{{(${keysToReplace.join("|")})}}`, "g"); 507 508 let replacedLegend = legendFormat.replace(expToReplace, (matchItem) => { 509 const nwMatchItem = matchItem.replace(/({{|}})/g, ""); 510 return metrics[nwMatchItem]; 511 }); 512 513 const countVarsOpen = (replacedLegend.match(/{{/g) || []).length; 514 const countVarsClose = (replacedLegend.match(/}}/g) || []).length; 515 516 let cleanLegend = replacedLegend.replace(/{{(.*?)}}/g, ""); 517 518 if ( 519 countVarsOpen === countVarsClose && 520 countVarsOpen !== 0 && 521 countVarsClose !== 0 522 ) { 523 keysToReplace.forEach((element) => { 524 replacedLegend = replacedLegend.replace(element, metrics[element]); 525 }); 526 527 cleanLegend = replacedLegend; 528 } 529 530 // In case not all the legends were replaced, we remove the placeholders. 531 return cleanLegend; 532 }; 533 534 export const widgetDetailsToPanel = ( 535 payloadData: any, 536 panelItem: IDashboardPanel, 537 ) => { 538 if (!payloadData) { 539 return panelItem; 540 } 541 542 const typeOfPayload = payloadData.type; 543 544 switch (panelItem.type) { 545 case widgetType.singleValue: 546 case widgetType.simpleWidget: 547 if (typeOfPayload === "stat" || typeOfPayload === "singlestat") { 548 // We sort values & get the last value 549 let elements = get(payloadData, "targets[0].result[0].values", []); 550 551 if (elements === null) { 552 elements = []; 553 } 554 555 const metricCalc = get( 556 payloadData, 557 "options.reduceOptions.calcs[0]", 558 "lastNotNull", 559 ); 560 561 const valueDisplay = calculateMainValue(elements, metricCalc); 562 563 const data = panelItem.labelDisplayFunction 564 ? panelItem.labelDisplayFunction(valueDisplay[1]) 565 : valueDisplay[1]; 566 567 return { 568 ...panelItem, 569 data, 570 }; 571 } 572 break; 573 case widgetType.pieChart: 574 if (typeOfPayload === "gauge") { 575 const metricCalc = get( 576 payloadData, 577 "options.reduceOptions.calcs[0]", 578 "lastNotNull", 579 ); 580 581 let chartSeries = get(payloadData, "targets", []).filter( 582 (seriesItem: any) => seriesItem !== null, 583 ); 584 585 const values = chartSeries.map((chartTarget: any) => { 586 const resultMap = 587 chartTarget.result && Array.isArray(chartTarget.result) 588 ? chartTarget.result 589 : []; 590 591 const values = resultMap.map((elementValue: any) => { 592 const values = get(elementValue, "values", []); 593 const metricKeyItem = Object.keys(elementValue.metric); 594 const sortResult = values.sort( 595 (value1: any[], value2: any[]) => 596 parseInt(value1[0][1]) - parseInt(value2[0][1]), 597 ); 598 599 const metricName = elementValue.metric[metricKeyItem[0]]; 600 const value = sortResult[sortResult.length - 1]; 601 return { 602 name: metricName, 603 value: parseInt(value[1]), 604 legend: chartTarget.legendFormat, 605 }; 606 }); 607 608 return values; 609 }); 610 611 const firstTarget = 612 chartSeries[0].result && chartSeries[0].result.length > 0 613 ? chartSeries[0].result[0].values 614 : []; 615 616 const totalValues = calculateMainValue(firstTarget, metricCalc); 617 618 const innerLabel = panelItem.labelDisplayFunction 619 ? panelItem.labelDisplayFunction(totalValues[1]) 620 : totalValues[1]; 621 622 return { 623 ...panelItem, 624 data: values, 625 innerLabel, 626 }; 627 } 628 break; 629 case widgetType.linearGraph: 630 case widgetType.areaGraph: 631 if (typeOfPayload === "graph") { 632 let targets = get(payloadData, "targets", []); 633 if (targets === null) { 634 targets = []; 635 } 636 637 const series: any[] = []; 638 const plotValues: any[] = []; 639 640 targets.forEach( 641 ( 642 targetMaster: { legendFormat: string; result: any[] }, 643 index: number, 644 ) => { 645 // Add a new serie to plot variables in case it is not from multiple values 646 let results = get(targetMaster, "result", []); 647 const legendFormat = targetMaster.legendFormat; 648 if (results === null) { 649 results = []; 650 } 651 652 results.forEach((itemVals: { metric: object; values: any[] }) => { 653 // Label Creation 654 const labelName = constructLabelNames( 655 itemVals.metric, 656 legendFormat, 657 ); 658 const keyName = `key_${index}${labelName}`; 659 660 // series creation with recently created label 661 series.push({ 662 dataKey: keyName, 663 keyLabel: labelName, 664 lineColor: "", 665 fillColor: "", 666 }); 667 668 // we iterate over values and create elements 669 let values = get(itemVals, "values", []); 670 if (values === null) { 671 values = []; 672 } 673 674 values.forEach((valInfo: any[]) => { 675 const itemIndex = plotValues.findIndex( 676 (element) => element.name === valInfo[0], 677 ); 678 679 // Element not exists yet 680 if (itemIndex === -1) { 681 let itemToPush: any = { name: valInfo[0] }; 682 itemToPush[keyName] = valInfo[1]; 683 684 plotValues.push(itemToPush); 685 } else { 686 plotValues[itemIndex][keyName] = valInfo[1]; 687 } 688 }); 689 }); 690 }, 691 ); 692 693 const sortedSeries = series.sort((series1: any, series2: any) => { 694 if (series1.keyLabel < series2.keyLabel) { 695 return -1; 696 } 697 if (series1.keyLabel > series2.keyLabel) { 698 return 1; 699 } 700 return 0; 701 }); 702 703 const seriesWithColors = sortedSeries.map( 704 (serialC: any, index: number) => { 705 return { 706 ...serialC, 707 lineColor: colorsMain[index] || textToRGBColor(serialC.keyLabel), 708 fillColor: colorsMain[index] || textToRGBColor(serialC.keyLabel), 709 }; 710 }, 711 ); 712 713 const sortedVals = plotValues.sort( 714 (value1: any, value2: any) => value1.name - value2.name, 715 ); 716 717 return { 718 ...panelItem, 719 widgetConfiguration: seriesWithColors, 720 data: sortedVals, 721 }; 722 } 723 break; 724 case widgetType.barChart: 725 if (typeOfPayload === "bargauge") { 726 let chartBars = get(payloadData, "targets[0].result", []); 727 728 if (chartBars === null) { 729 chartBars = []; 730 } 731 732 const sortFunction = (value1: any[], value2: any[]) => 733 value1[0] - value2[0]; 734 735 let values = []; 736 if (panelItem.customStructure) { 737 values = panelItem.customStructure.map((structureItem) => { 738 const metricTake = chartBars.find((element: any) => { 739 return element.metric.range === structureItem.originTag; 740 }); 741 742 const elements = get(metricTake, "values", []); 743 744 const sortResult = elements.sort(sortFunction); 745 const lastValue = sortResult[sortResult.length - 1] || ["", "0"]; 746 747 return { 748 name: structureItem.displayTag, 749 a: parseInt(lastValue[1]), 750 }; 751 }); 752 } else { 753 // If no configuration is set, we construct the series for bar chart and return the element 754 values = chartBars.map((elementValue: any) => { 755 const metricKeyItem = Object.keys(elementValue.metric); 756 757 const metricName = elementValue.metric[metricKeyItem[0]]; 758 759 const elements = get(elementValue, "values", []); 760 761 const sortResult = elements.sort(sortFunction); 762 const lastValue = sortResult[sortResult.length - 1] || ["", "0"]; 763 return { name: metricName, a: parseInt(lastValue[1]) }; 764 }); 765 } 766 767 return { 768 ...panelItem, 769 data: values, 770 }; 771 } 772 break; 773 case widgetType.singleRep: 774 if (typeOfPayload === "stat") { 775 // We sort values & get the last value 776 let elements = get(payloadData, "targets[0].result[0].values", []); 777 if (elements === null) { 778 elements = []; 779 } 780 const metricCalc = get( 781 payloadData, 782 "options.reduceOptions.calcs[0]", 783 "lastNotNull", 784 ); 785 786 const valueDisplay = calculateMainValue(elements, metricCalc); 787 788 const sortResult = elements.sort( 789 (value1: any[], value2: any[]) => value1[0] - value2[0], 790 ); 791 792 let valuesForBackground = []; 793 794 if (sortResult.length === 1) { 795 valuesForBackground.push({ value: 0 }); 796 } 797 798 sortResult.forEach((eachVal: any) => { 799 valuesForBackground.push({ value: parseInt(eachVal[1]) }); 800 }); 801 802 const innerLabel = panelItem.labelDisplayFunction 803 ? panelItem.labelDisplayFunction(valueDisplay[1]) 804 : valueDisplay[1]; 805 806 return { 807 ...panelItem, 808 data: valuesForBackground, 809 innerLabel, 810 }; 811 } 812 break; 813 } 814 815 return panelItem; 816 }; 817 818 const verifyNumeric = (item: string) => { 819 return !isNaN(parseFloat(item)); 820 }; 821 822 export const splitSizeMetric = (val: string) => { 823 const splittedText = val.split(" "); 824 // Value is not a size metric, we return as common string 825 826 const singleValue = () => { 827 let vl = val; 828 829 if (verifyNumeric(val)) { 830 vl = representationNumber(parseFloat(val)); 831 } 832 return <Fragment>{vl}</Fragment>; 833 }; 834 835 if (splittedText.length !== 2) { 836 return singleValue(); 837 } 838 839 if (!units.includes(splittedText[1])) { 840 return singleValue(); 841 } 842 843 return ( 844 <span className="commonValue"> 845 {splittedText[0]} 846 <span className="unitText">{splittedText[1]}</span> 847 </span> 848 ); 849 };