├── .travis.yml ├── GrafanaJMeterTemplate.json ├── LICENSE ├── README.md ├── build ├── Dockerfile └── test-framework │ ├── Dockerfile │ └── ansible-test.sh ├── cloudssky.jmx ├── deploy ├── crds │ ├── loadtest_v1alpha1_jmeter_cr.yaml │ └── loadtest_v1alpha1_jmeter_crd.yaml ├── operator.yaml ├── role.yaml ├── role_binding.yaml └── service_account.yaml ├── img ├── grafana_datasource.png └── test_progress.png ├── initialize_cluster.sh ├── jmeter-deploy.yaml ├── molecule ├── default │ ├── asserts.yml │ ├── molecule.yml │ ├── playbook.yml │ └── prepare.yml ├── test-cluster │ ├── molecule.yml │ └── playbook.yml └── test-local │ ├── molecule.yml │ ├── playbook.yml │ └── prepare.yml ├── roles └── jmeter │ ├── README.md │ ├── defaults │ └── main.yml │ ├── handlers │ └── main.yml │ ├── meta │ └── main.yml │ ├── tasks │ └── main.yml │ └── vars │ └── main.yml ├── start_test.sh └── watches.yaml /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: docker 3 | language: python 4 | install: 5 | - pip install docker molecule openshift 6 | script: 7 | - molecule test -s test-local 8 | -------------------------------------------------------------------------------- /GrafanaJMeterTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_JMETERDB", 5 | "label": "jmeterdb", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "influxdb", 9 | "pluginName": "InfluxDB" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "5.2.0" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "5.0.0" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "influxdb", 28 | "name": "InfluxDB", 29 | "version": "5.0.0" 30 | }, 31 | { 32 | "type": "panel", 33 | "id": "singlestat", 34 | "name": "Singlestat", 35 | "version": "5.0.0" 36 | } 37 | ], 38 | "annotations": { 39 | "list": [ 40 | { 41 | "builtIn": 1, 42 | "datasource": "-- Grafana --", 43 | "enable": true, 44 | "hide": true, 45 | "iconColor": "rgba(0, 211, 255, 1)", 46 | "name": "Annotations & Alerts", 47 | "type": "dashboard" 48 | }, 49 | { 50 | "datasource": "${DS_JMETERDB}", 51 | "enable": true, 52 | "iconColor": "rgb(237, 18, 18)", 53 | "iconSize": 17, 54 | "lineColor": "rgb(0, 21, 255)", 55 | "name": "Annotation", 56 | "query": "select text,tags,title from \"$retention\".\"events\" where application =~ /$app/ AND $timeFilter", 57 | "showLine": true, 58 | "tagsColumn": "tags", 59 | "textColumn": "text", 60 | "titleColumn": "title" 61 | } 62 | ] 63 | }, 64 | "editable": true, 65 | "gnetId": null, 66 | "graphTooltip": 1, 67 | "id": null, 68 | "iteration": 1552478455724, 69 | "links": [ 70 | { 71 | "asDropdown": true, 72 | "icon": "dashboard", 73 | "includeVars": true, 74 | "keepTime": true, 75 | "tags": [], 76 | "targetBlank": true, 77 | "tooltip": "", 78 | "type": "dashboards", 79 | "url": "" 80 | } 81 | ], 82 | "panels": [ 83 | { 84 | "collapsed": false, 85 | "gridPos": { 86 | "h": 1, 87 | "w": 24, 88 | "x": 0, 89 | "y": 0 90 | }, 91 | "id": 35, 92 | "panels": [], 93 | "repeat": null, 94 | "title": "Jmeter Metrics", 95 | "type": "row" 96 | }, 97 | { 98 | "cacheTimeout": null, 99 | "colorBackground": false, 100 | "colorValue": false, 101 | "colors": [ 102 | "rgba(245, 54, 54, 0.9)", 103 | "rgba(237, 129, 40, 0.89)", 104 | "rgba(50, 172, 45, 0.97)" 105 | ], 106 | "datasource": "${DS_JMETERDB}", 107 | "editable": true, 108 | "error": false, 109 | "format": "none", 110 | "gauge": { 111 | "maxValue": 100, 112 | "minValue": 0, 113 | "show": false, 114 | "thresholdLabels": false, 115 | "thresholdMarkers": true 116 | }, 117 | "gridPos": { 118 | "h": 4, 119 | "w": 8, 120 | "x": 0, 121 | "y": 1 122 | }, 123 | "id": 19, 124 | "interval": "$granularity", 125 | "links": [], 126 | "mappingType": 1, 127 | "mappingTypes": [ 128 | { 129 | "name": "value to text", 130 | "value": 1 131 | }, 132 | { 133 | "name": "range to text", 134 | "value": 2 135 | } 136 | ], 137 | "maxDataPoints": 100, 138 | "nullPointMode": "connected", 139 | "nullText": null, 140 | "postfix": " users", 141 | "postfixFontSize": "50%", 142 | "prefix": "", 143 | "prefixFontSize": "50%", 144 | "rangeMaps": [ 145 | { 146 | "from": "null", 147 | "text": "N/A", 148 | "to": "null" 149 | } 150 | ], 151 | "sparkline": { 152 | "fillColor": "rgba(31, 118, 189, 0.18)", 153 | "full": true, 154 | "lineColor": "rgb(31, 120, 193)", 155 | "show": true 156 | }, 157 | "tableColumn": "", 158 | "targets": [ 159 | { 160 | "dsType": "influxdb", 161 | "groupBy": [ 162 | { 163 | "params": [ 164 | "$granularity" 165 | ], 166 | "type": "time" 167 | }, 168 | { 169 | "params": [ 170 | "null" 171 | ], 172 | "type": "fill" 173 | } 174 | ], 175 | "measurement": "jmeter", 176 | "policy": "$retention", 177 | "query": "SELECT last(\"startedT\") FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND $timeFilter GROUP BY time($granularity) fill(null)", 178 | "refId": "A", 179 | "resultFormat": "time_series", 180 | "select": [ 181 | [ 182 | { 183 | "params": [ 184 | "meanAT" 185 | ], 186 | "type": "field" 187 | }, 188 | { 189 | "params": [], 190 | "type": "last" 191 | } 192 | ] 193 | ], 194 | "tags": [ 195 | { 196 | "key": "application", 197 | "operator": "=~", 198 | "value": "/$app$/" 199 | } 200 | ] 201 | } 202 | ], 203 | "thresholds": "", 204 | "title": "Active Users", 205 | "type": "singlestat", 206 | "valueFontSize": "80%", 207 | "valueMaps": [ 208 | { 209 | "op": "=", 210 | "text": "0", 211 | "value": "null" 212 | } 213 | ], 214 | "valueName": "current" 215 | }, 216 | { 217 | "cacheTimeout": null, 218 | "colorBackground": false, 219 | "colorValue": false, 220 | "colors": [ 221 | "rgba(245, 54, 54, 0.9)", 222 | "rgba(237, 129, 40, 0.89)", 223 | "rgba(50, 172, 45, 0.97)" 224 | ], 225 | "datasource": "${DS_JMETERDB}", 226 | "editable": true, 227 | "error": false, 228 | "format": "none", 229 | "gauge": { 230 | "maxValue": 100, 231 | "minValue": 0, 232 | "show": false, 233 | "thresholdLabels": false, 234 | "thresholdMarkers": true 235 | }, 236 | "gridPos": { 237 | "h": 4, 238 | "w": 8, 239 | "x": 8, 240 | "y": 1 241 | }, 242 | "id": 17, 243 | "interval": "", 244 | "links": [], 245 | "mappingType": 1, 246 | "mappingTypes": [ 247 | { 248 | "name": "value to text", 249 | "value": 1 250 | }, 251 | { 252 | "name": "range to text", 253 | "value": 2 254 | } 255 | ], 256 | "maxDataPoints": 100, 257 | "nullPointMode": "connected", 258 | "nullText": null, 259 | "postfix": " TPS", 260 | "postfixFontSize": "50%", 261 | "prefix": "", 262 | "prefixFontSize": "50%", 263 | "rangeMaps": [ 264 | { 265 | "from": "null", 266 | "text": "N/A", 267 | "to": "null" 268 | } 269 | ], 270 | "sparkline": { 271 | "fillColor": "rgba(31, 118, 189, 0.18)", 272 | "full": true, 273 | "lineColor": "rgb(31, 120, 193)", 274 | "show": true 275 | }, 276 | "tableColumn": "", 277 | "targets": [ 278 | { 279 | "dsType": "influxdb", 280 | "groupBy": [ 281 | { 282 | "params": [ 283 | "$granularity" 284 | ], 285 | "type": "time" 286 | } 287 | ], 288 | "measurement": "jmeter", 289 | "policy": "default", 290 | "query": "SELECT sum(\"hit\") / 30 FROM \"$retention\".\"jmeter\" WHERE \"application\" = '$app' AND \"transaction\" = 'all' AND $timeFilter GROUP BY time(30s)", 291 | "rawQuery": true, 292 | "refId": "A", 293 | "resultFormat": "time_series", 294 | "select": [ 295 | [ 296 | { 297 | "params": [ 298 | "hit" 299 | ], 300 | "type": "field" 301 | }, 302 | { 303 | "params": [], 304 | "type": "last" 305 | }, 306 | { 307 | "params": [ 308 | " / 5" 309 | ], 310 | "type": "math" 311 | } 312 | ] 313 | ], 314 | "tags": [ 315 | { 316 | "key": "application", 317 | "operator": "=~", 318 | "value": "/$app$/" 319 | }, 320 | { 321 | "condition": "AND", 322 | "key": "transaction", 323 | "operator": "=", 324 | "value": "all" 325 | } 326 | ] 327 | } 328 | ], 329 | "thresholds": "", 330 | "title": "Currents hits per Second", 331 | "type": "singlestat", 332 | "valueFontSize": "80%", 333 | "valueMaps": [ 334 | { 335 | "op": "=", 336 | "text": "0", 337 | "value": "null" 338 | } 339 | ], 340 | "valueName": "current" 341 | }, 342 | { 343 | "cacheTimeout": null, 344 | "colorBackground": false, 345 | "colorValue": true, 346 | "colors": [ 347 | "rgba(50, 172, 45, 0.97)", 348 | "rgba(237, 129, 40, 0.89)", 349 | "rgba(245, 54, 54, 0.9)" 350 | ], 351 | "datasource": "${DS_JMETERDB}", 352 | "editable": true, 353 | "error": false, 354 | "format": "percentunit", 355 | "gauge": { 356 | "maxValue": 100, 357 | "minValue": 0, 358 | "show": false, 359 | "thresholdLabels": false, 360 | "thresholdMarkers": true 361 | }, 362 | "gridPos": { 363 | "h": 4, 364 | "w": 8, 365 | "x": 16, 366 | "y": 1 367 | }, 368 | "id": 21, 369 | "interval": "$granularity", 370 | "links": [], 371 | "mappingType": 1, 372 | "mappingTypes": [ 373 | { 374 | "name": "value to text", 375 | "value": 1 376 | }, 377 | { 378 | "name": "range to text", 379 | "value": 2 380 | } 381 | ], 382 | "maxDataPoints": 100, 383 | "nullPointMode": "connected", 384 | "nullText": null, 385 | "postfix": "", 386 | "postfixFontSize": "50%", 387 | "prefix": "", 388 | "prefixFontSize": "50%", 389 | "rangeMaps": [ 390 | { 391 | "from": "null", 392 | "text": "N/A", 393 | "to": "null" 394 | } 395 | ], 396 | "sparkline": { 397 | "fillColor": "rgba(31, 118, 189, 0.18)", 398 | "full": true, 399 | "lineColor": "rgb(31, 120, 193)", 400 | "show": true 401 | }, 402 | "tableColumn": "", 403 | "targets": [ 404 | { 405 | "dsType": "influxdb", 406 | "groupBy": [], 407 | "measurement": "jmeter", 408 | "policy": "$retention", 409 | "query": "SELECT sum(\"countError\") / sum(\"count\") FROM \"$retention\".\"jmeter\" WHERE \"application\" =~ /$app$/ AND \"transaction\" = 'all' AND $timeFilter", 410 | "rawQuery": true, 411 | "refId": "B", 412 | "resultFormat": "time_series", 413 | "select": [ 414 | [ 415 | { 416 | "params": [ 417 | "countError" 418 | ], 419 | "type": "field" 420 | }, 421 | { 422 | "params": [], 423 | "type": "sum" 424 | }, 425 | { 426 | "params": [ 427 | " / sum(\"count\")" 428 | ], 429 | "type": "math" 430 | } 431 | ] 432 | ], 433 | "tags": [ 434 | { 435 | "key": "application", 436 | "operator": "=~", 437 | "value": "/$app$/" 438 | }, 439 | { 440 | "condition": "AND", 441 | "key": "transaction", 442 | "operator": "=", 443 | "value": "all" 444 | } 445 | ] 446 | } 447 | ], 448 | "thresholds": "0.1,0.2", 449 | "title": "% Errors", 450 | "type": "singlestat", 451 | "valueFontSize": "80%", 452 | "valueMaps": [ 453 | { 454 | "op": "=", 455 | "text": "0", 456 | "value": "null" 457 | } 458 | ], 459 | "valueName": "total" 460 | }, 461 | { 462 | "aliasColors": {}, 463 | "bars": false, 464 | "dashLength": 10, 465 | "dashes": false, 466 | "datasource": "${DS_JMETERDB}", 467 | "editable": true, 468 | "error": false, 469 | "fill": 1, 470 | "grid": {}, 471 | "gridPos": { 472 | "h": 4, 473 | "w": 8, 474 | "x": 0, 475 | "y": 5 476 | }, 477 | "id": 27, 478 | "interval": "$granularity", 479 | "legend": { 480 | "avg": false, 481 | "current": false, 482 | "max": false, 483 | "min": false, 484 | "show": false, 485 | "total": false, 486 | "values": false 487 | }, 488 | "lines": true, 489 | "linewidth": 1, 490 | "links": [], 491 | "nullPointMode": "null as zero", 492 | "percentage": false, 493 | "pointradius": 5, 494 | "points": false, 495 | "renderer": "flot", 496 | "seriesOverrides": [], 497 | "spaceLength": 10, 498 | "stack": false, 499 | "steppedLine": true, 500 | "targets": [ 501 | { 502 | "alias": "Hits", 503 | "dsType": "influxdb", 504 | "groupBy": [ 505 | { 506 | "params": [ 507 | "$granularity" 508 | ], 509 | "type": "time" 510 | }, 511 | { 512 | "params": [ 513 | "null" 514 | ], 515 | "type": "fill" 516 | } 517 | ], 518 | "measurement": "jmeter", 519 | "policy": "$retention", 520 | "query": "SELECT last(\"hit\") / 5 FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND $timeFilter GROUP BY time($granularity) fill(null)", 521 | "refId": "A", 522 | "resultFormat": "time_series", 523 | "select": [ 524 | [ 525 | { 526 | "params": [ 527 | "hit" 528 | ], 529 | "type": "field" 530 | }, 531 | { 532 | "params": [], 533 | "type": "last" 534 | }, 535 | { 536 | "params": [ 537 | " / 5" 538 | ], 539 | "type": "math" 540 | } 541 | ] 542 | ], 543 | "tags": [ 544 | { 545 | "key": "application", 546 | "operator": "=~", 547 | "value": "/$app$/" 548 | } 549 | ] 550 | } 551 | ], 552 | "thresholds": [], 553 | "timeFrom": null, 554 | "timeShift": null, 555 | "title": "Hits per Second", 556 | "tooltip": { 557 | "msResolution": false, 558 | "shared": true, 559 | "sort": 0, 560 | "value_type": "cumulative" 561 | }, 562 | "type": "graph", 563 | "xaxis": { 564 | "buckets": null, 565 | "mode": "time", 566 | "name": null, 567 | "show": false, 568 | "values": [] 569 | }, 570 | "yaxes": [ 571 | { 572 | "format": "short", 573 | "logBase": 1, 574 | "max": null, 575 | "min": 0, 576 | "show": true 577 | }, 578 | { 579 | "format": "short", 580 | "logBase": 1, 581 | "max": null, 582 | "min": null, 583 | "show": true 584 | } 585 | ], 586 | "yaxis": { 587 | "align": false, 588 | "alignLevel": null 589 | } 590 | }, 591 | { 592 | "cacheTimeout": null, 593 | "colorBackground": false, 594 | "colorValue": false, 595 | "colors": [ 596 | "rgba(245, 54, 54, 0.9)", 597 | "rgba(237, 129, 40, 0.89)", 598 | "rgba(50, 172, 45, 0.97)" 599 | ], 600 | "datasource": "${DS_JMETERDB}", 601 | "editable": true, 602 | "error": false, 603 | "format": "none", 604 | "gauge": { 605 | "maxValue": 100, 606 | "minValue": 0, 607 | "show": false, 608 | "thresholdLabels": false, 609 | "thresholdMarkers": true 610 | }, 611 | "gridPos": { 612 | "h": 4, 613 | "w": 8, 614 | "x": 8, 615 | "y": 5 616 | }, 617 | "id": 22, 618 | "interval": "$granularity", 619 | "links": [], 620 | "mappingType": 1, 621 | "mappingTypes": [ 622 | { 623 | "name": "value to text", 624 | "value": 1 625 | }, 626 | { 627 | "name": "range to text", 628 | "value": 2 629 | } 630 | ], 631 | "maxDataPoints": 100, 632 | "nullPointMode": "connected", 633 | "nullText": null, 634 | "postfix": "", 635 | "postfixFontSize": "50%", 636 | "prefix": "", 637 | "prefixFontSize": "50%", 638 | "rangeMaps": [ 639 | { 640 | "from": "null", 641 | "text": "N/A", 642 | "to": "null" 643 | } 644 | ], 645 | "sparkline": { 646 | "fillColor": "rgba(31, 118, 189, 0.18)", 647 | "full": true, 648 | "lineColor": "rgb(31, 120, 193)", 649 | "show": true 650 | }, 651 | "tableColumn": "", 652 | "targets": [ 653 | { 654 | "dsType": "influxdb", 655 | "groupBy": [], 656 | "measurement": "jmeter", 657 | "policy": "$retention", 658 | "query": "SELECT \"hit\" FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND \"statut\" = 'all' AND $timeFilter", 659 | "rawQuery": false, 660 | "refId": "A", 661 | "resultFormat": "time_series", 662 | "select": [ 663 | [ 664 | { 665 | "params": [ 666 | "hit" 667 | ], 668 | "type": "field" 669 | } 670 | ] 671 | ], 672 | "tags": [ 673 | { 674 | "key": "application", 675 | "operator": "=~", 676 | "value": "/$app$/" 677 | }, 678 | { 679 | "condition": "AND", 680 | "key": "statut", 681 | "operator": "=", 682 | "value": "all" 683 | } 684 | ] 685 | } 686 | ], 687 | "thresholds": "", 688 | "title": "Total Hits", 689 | "type": "singlestat", 690 | "valueFontSize": "80%", 691 | "valueMaps": [ 692 | { 693 | "op": "=", 694 | "text": "0", 695 | "value": "null" 696 | } 697 | ], 698 | "valueName": "total" 699 | }, 700 | { 701 | "aliasColors": {}, 702 | "bars": false, 703 | "dashLength": 10, 704 | "dashes": false, 705 | "datasource": "${DS_JMETERDB}", 706 | "editable": true, 707 | "error": false, 708 | "fill": 1, 709 | "grid": {}, 710 | "gridPos": { 711 | "h": 4, 712 | "w": 8, 713 | "x": 16, 714 | "y": 5 715 | }, 716 | "id": 28, 717 | "interval": "$granularity", 718 | "legend": { 719 | "avg": false, 720 | "current": false, 721 | "max": false, 722 | "min": false, 723 | "show": false, 724 | "total": false, 725 | "values": false 726 | }, 727 | "lines": true, 728 | "linewidth": 1, 729 | "links": [], 730 | "nullPointMode": "null", 731 | "percentage": false, 732 | "pointradius": 5, 733 | "points": false, 734 | "renderer": "flot", 735 | "seriesOverrides": [], 736 | "spaceLength": 10, 737 | "stack": false, 738 | "steppedLine": true, 739 | "targets": [ 740 | { 741 | "alias": "Errors", 742 | "dsType": "influxdb", 743 | "groupBy": [ 744 | { 745 | "params": [ 746 | "$granularity" 747 | ], 748 | "type": "time" 749 | }, 750 | { 751 | "params": [ 752 | "0" 753 | ], 754 | "type": "fill" 755 | } 756 | ], 757 | "measurement": "jmeter", 758 | "policy": "$retention", 759 | "query": "SELECT mean(\"countError\") / 5 FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND $timeFilter GROUP BY time($granularity) fill(0)", 760 | "rawQuery": false, 761 | "refId": "A", 762 | "resultFormat": "time_series", 763 | "select": [ 764 | [ 765 | { 766 | "params": [ 767 | "countError" 768 | ], 769 | "type": "field" 770 | }, 771 | { 772 | "params": [], 773 | "type": "mean" 774 | }, 775 | { 776 | "params": [ 777 | " / 5" 778 | ], 779 | "type": "math" 780 | } 781 | ] 782 | ], 783 | "tags": [ 784 | { 785 | "key": "application", 786 | "operator": "=~", 787 | "value": "/$app$/" 788 | } 789 | ] 790 | } 791 | ], 792 | "thresholds": [], 793 | "timeFrom": null, 794 | "timeShift": null, 795 | "title": "Errors per Second", 796 | "tooltip": { 797 | "msResolution": false, 798 | "shared": true, 799 | "sort": 0, 800 | "value_type": "cumulative" 801 | }, 802 | "type": "graph", 803 | "xaxis": { 804 | "buckets": null, 805 | "mode": "time", 806 | "name": null, 807 | "show": false, 808 | "values": [] 809 | }, 810 | "yaxes": [ 811 | { 812 | "format": "short", 813 | "logBase": 1, 814 | "max": null, 815 | "min": 0, 816 | "show": true 817 | }, 818 | { 819 | "format": "short", 820 | "logBase": 1, 821 | "max": null, 822 | "min": null, 823 | "show": true 824 | } 825 | ], 826 | "yaxis": { 827 | "align": false, 828 | "alignLevel": null 829 | } 830 | }, 831 | { 832 | "collapsed": false, 833 | "gridPos": { 834 | "h": 1, 835 | "w": 24, 836 | "x": 0, 837 | "y": 9 838 | }, 839 | "id": 36, 840 | "panels": [], 841 | "repeat": null, 842 | "title": "Application Metrics", 843 | "type": "row" 844 | }, 845 | { 846 | "aliasColors": {}, 847 | "bars": false, 848 | "dashLength": 10, 849 | "dashes": false, 850 | "datasource": "${DS_JMETERDB}", 851 | "editable": true, 852 | "error": false, 853 | "fill": 1, 854 | "grid": {}, 855 | "gridPos": { 856 | "h": 18, 857 | "w": 24, 858 | "x": 0, 859 | "y": 10 860 | }, 861 | "height": "", 862 | "id": 25, 863 | "interval": "$granularity", 864 | "legend": { 865 | "alignAsTable": true, 866 | "avg": true, 867 | "current": true, 868 | "hideEmpty": false, 869 | "max": true, 870 | "min": true, 871 | "rightSide": false, 872 | "show": true, 873 | "total": false, 874 | "values": true 875 | }, 876 | "lines": true, 877 | "linewidth": 1, 878 | "links": [], 879 | "minSpan": 24, 880 | "nullPointMode": "null", 881 | "percentage": false, 882 | "pointradius": 5, 883 | "points": false, 884 | "renderer": "flot", 885 | "seriesOverrides": [], 886 | "spaceLength": 10, 887 | "stack": false, 888 | "steppedLine": true, 889 | "targets": [ 890 | { 891 | "alias": "$tag_transaction", 892 | "dsType": "influxdb", 893 | "groupBy": [ 894 | { 895 | "params": [ 896 | "$granularity" 897 | ], 898 | "type": "time" 899 | }, 900 | { 901 | "params": [ 902 | "transaction" 903 | ], 904 | "type": "tag" 905 | } 906 | ], 907 | "hide": false, 908 | "measurement": "jmeter", 909 | "policy": "$retention", 910 | "query": "SELECT mean(\"avg\") FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND $timeFilter GROUP BY time($granularity), \"transaction\"", 911 | "rawQuery": false, 912 | "refId": "A", 913 | "resultFormat": "time_series", 914 | "select": [ 915 | [ 916 | { 917 | "params": [ 918 | "avg" 919 | ], 920 | "type": "field" 921 | }, 922 | { 923 | "params": [], 924 | "type": "mean" 925 | } 926 | ] 927 | ], 928 | "tags": [ 929 | { 930 | "key": "application", 931 | "operator": "=~", 932 | "value": "/$app$/" 933 | }, 934 | { 935 | "condition": "AND", 936 | "key": "statut", 937 | "operator": "=", 938 | "value": "all" 939 | } 940 | ] 941 | } 942 | ], 943 | "thresholds": [], 944 | "timeFrom": null, 945 | "timeShift": null, 946 | "title": "Average Response Time", 947 | "tooltip": { 948 | "msResolution": false, 949 | "shared": false, 950 | "sort": 0, 951 | "value_type": "cumulative" 952 | }, 953 | "transparent": false, 954 | "type": "graph", 955 | "xaxis": { 956 | "buckets": null, 957 | "mode": "time", 958 | "name": null, 959 | "show": true, 960 | "values": [] 961 | }, 962 | "yaxes": [ 963 | { 964 | "format": "ms", 965 | "logBase": 1, 966 | "max": null, 967 | "min": null, 968 | "show": true 969 | }, 970 | { 971 | "format": "short", 972 | "logBase": 1, 973 | "max": null, 974 | "min": null, 975 | "show": true 976 | } 977 | ], 978 | "yaxis": { 979 | "align": false, 980 | "alignLevel": null 981 | } 982 | }, 983 | { 984 | "aliasColors": {}, 985 | "bars": false, 986 | "dashLength": 10, 987 | "dashes": false, 988 | "datasource": "${DS_JMETERDB}", 989 | "editable": true, 990 | "error": false, 991 | "fill": 1, 992 | "grid": {}, 993 | "gridPos": { 994 | "h": 18, 995 | "w": 24, 996 | "x": 0, 997 | "y": 28 998 | }, 999 | "height": "", 1000 | "id": 26, 1001 | "interval": "$granularity", 1002 | "legend": { 1003 | "alignAsTable": true, 1004 | "avg": true, 1005 | "current": true, 1006 | "hideEmpty": false, 1007 | "max": true, 1008 | "min": true, 1009 | "rightSide": false, 1010 | "show": true, 1011 | "sort": "current", 1012 | "sortDesc": true, 1013 | "total": false, 1014 | "values": true 1015 | }, 1016 | "lines": true, 1017 | "linewidth": 1, 1018 | "links": [], 1019 | "minSpan": 24, 1020 | "nullPointMode": "null", 1021 | "percentage": false, 1022 | "pointradius": 5, 1023 | "points": false, 1024 | "renderer": "flot", 1025 | "seriesOverrides": [], 1026 | "spaceLength": 10, 1027 | "stack": false, 1028 | "steppedLine": true, 1029 | "targets": [ 1030 | { 1031 | "alias": "$tag_transaction", 1032 | "dsType": "influxdb", 1033 | "groupBy": [ 1034 | { 1035 | "params": [ 1036 | "30s" 1037 | ], 1038 | "type": "time" 1039 | }, 1040 | { 1041 | "params": [ 1042 | "transaction" 1043 | ], 1044 | "type": "tag" 1045 | } 1046 | ], 1047 | "measurement": "jmeter", 1048 | "policy": "$retention", 1049 | "query": "SELECT mean(\"count\") / 5 FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND \"transaction\" <> 'all' AND $timeFilter GROUP BY time($granularity), \"transaction\"", 1050 | "rawQuery": false, 1051 | "refId": "A", 1052 | "resultFormat": "time_series", 1053 | "select": [ 1054 | [ 1055 | { 1056 | "params": [ 1057 | "count" 1058 | ], 1059 | "type": "field" 1060 | }, 1061 | { 1062 | "params": [], 1063 | "type": "sum" 1064 | }, 1065 | { 1066 | "params": [ 1067 | " / 30" 1068 | ], 1069 | "type": "math" 1070 | } 1071 | ] 1072 | ], 1073 | "tags": [ 1074 | { 1075 | "key": "application", 1076 | "operator": "=~", 1077 | "value": "/$app$/" 1078 | }, 1079 | { 1080 | "condition": "AND", 1081 | "key": "transaction", 1082 | "operator": "<>", 1083 | "value": "all" 1084 | }, 1085 | { 1086 | "condition": "AND", 1087 | "key": "statut", 1088 | "operator": "=", 1089 | "value": "all" 1090 | } 1091 | ] 1092 | } 1093 | ], 1094 | "thresholds": [], 1095 | "timeFrom": null, 1096 | "timeShift": null, 1097 | "title": "Transaction Per Second", 1098 | "tooltip": { 1099 | "msResolution": false, 1100 | "shared": false, 1101 | "sort": 0, 1102 | "value_type": "cumulative" 1103 | }, 1104 | "transparent": false, 1105 | "type": "graph", 1106 | "xaxis": { 1107 | "buckets": null, 1108 | "mode": "time", 1109 | "name": null, 1110 | "show": true, 1111 | "values": [] 1112 | }, 1113 | "yaxes": [ 1114 | { 1115 | "format": "ops", 1116 | "label": "", 1117 | "logBase": 1, 1118 | "max": null, 1119 | "min": null, 1120 | "show": true 1121 | }, 1122 | { 1123 | "format": "short", 1124 | "logBase": 1, 1125 | "max": null, 1126 | "min": null, 1127 | "show": true 1128 | } 1129 | ], 1130 | "yaxis": { 1131 | "align": false, 1132 | "alignLevel": null 1133 | } 1134 | }, 1135 | { 1136 | "aliasColors": {}, 1137 | "bars": false, 1138 | "dashLength": 10, 1139 | "dashes": false, 1140 | "datasource": "${DS_JMETERDB}", 1141 | "editable": true, 1142 | "error": false, 1143 | "fill": 1, 1144 | "grid": {}, 1145 | "gridPos": { 1146 | "h": 18, 1147 | "w": 24, 1148 | "x": 0, 1149 | "y": 46 1150 | }, 1151 | "height": "", 1152 | "id": 29, 1153 | "interval": "$granularity", 1154 | "legend": { 1155 | "alignAsTable": true, 1156 | "avg": true, 1157 | "current": true, 1158 | "hideEmpty": false, 1159 | "max": true, 1160 | "min": true, 1161 | "rightSide": false, 1162 | "show": true, 1163 | "total": false, 1164 | "values": true 1165 | }, 1166 | "lines": true, 1167 | "linewidth": 1, 1168 | "links": [], 1169 | "minSpan": 24, 1170 | "nullPointMode": "null", 1171 | "percentage": false, 1172 | "pointradius": 5, 1173 | "points": false, 1174 | "renderer": "flot", 1175 | "seriesOverrides": [], 1176 | "spaceLength": 10, 1177 | "stack": false, 1178 | "steppedLine": true, 1179 | "targets": [ 1180 | { 1181 | "alias": "$tag_transaction", 1182 | "dsType": "influxdb", 1183 | "groupBy": [ 1184 | { 1185 | "params": [ 1186 | "30s" 1187 | ], 1188 | "type": "time" 1189 | }, 1190 | { 1191 | "params": [ 1192 | "transaction" 1193 | ], 1194 | "type": "tag" 1195 | }, 1196 | { 1197 | "params": [ 1198 | "null" 1199 | ], 1200 | "type": "fill" 1201 | } 1202 | ], 1203 | "measurement": "jmeter", 1204 | "policy": "$retention", 1205 | "query": "SELECT last(\"count\") / 30 FROM \"$retention\".\"jmeter\" WHERE \"application\" =~ /$app$/ AND \"statut\" = 'ko' AND $timeFilter GROUP BY time(30s), \"transaction\" fill(null)", 1206 | "rawQuery": false, 1207 | "refId": "B", 1208 | "resultFormat": "time_series", 1209 | "select": [ 1210 | [ 1211 | { 1212 | "params": [ 1213 | "count" 1214 | ], 1215 | "type": "field" 1216 | }, 1217 | { 1218 | "params": [], 1219 | "type": "sum" 1220 | }, 1221 | { 1222 | "params": [ 1223 | " / 30" 1224 | ], 1225 | "type": "math" 1226 | } 1227 | ] 1228 | ], 1229 | "tags": [ 1230 | { 1231 | "key": "application", 1232 | "operator": "=~", 1233 | "value": "/$app$/" 1234 | }, 1235 | { 1236 | "condition": "AND", 1237 | "key": "statut", 1238 | "operator": "=", 1239 | "value": "ko" 1240 | } 1241 | ] 1242 | } 1243 | ], 1244 | "thresholds": [], 1245 | "timeFrom": null, 1246 | "timeShift": null, 1247 | "title": "Error Per Second", 1248 | "tooltip": { 1249 | "msResolution": false, 1250 | "shared": false, 1251 | "sort": 0, 1252 | "value_type": "cumulative" 1253 | }, 1254 | "transparent": false, 1255 | "type": "graph", 1256 | "xaxis": { 1257 | "buckets": null, 1258 | "mode": "time", 1259 | "name": null, 1260 | "show": true, 1261 | "values": [] 1262 | }, 1263 | "yaxes": [ 1264 | { 1265 | "format": "ops", 1266 | "label": "", 1267 | "logBase": 1, 1268 | "max": null, 1269 | "min": null, 1270 | "show": true 1271 | }, 1272 | { 1273 | "format": "short", 1274 | "logBase": 1, 1275 | "max": null, 1276 | "min": null, 1277 | "show": true 1278 | } 1279 | ], 1280 | "yaxis": { 1281 | "align": false, 1282 | "alignLevel": null 1283 | } 1284 | }, 1285 | { 1286 | "aliasColors": {}, 1287 | "bars": false, 1288 | "dashLength": 10, 1289 | "dashes": false, 1290 | "datasource": "${DS_JMETERDB}", 1291 | "editable": true, 1292 | "error": false, 1293 | "fill": 1, 1294 | "grid": {}, 1295 | "gridPos": { 1296 | "h": 18, 1297 | "w": 24, 1298 | "x": 0, 1299 | "y": 64 1300 | }, 1301 | "height": "", 1302 | "id": 34, 1303 | "interval": "", 1304 | "legend": { 1305 | "alignAsTable": true, 1306 | "avg": true, 1307 | "current": true, 1308 | "hideEmpty": true, 1309 | "hideZero": true, 1310 | "max": true, 1311 | "min": true, 1312 | "rightSide": false, 1313 | "show": true, 1314 | "total": false, 1315 | "values": true 1316 | }, 1317 | "lines": true, 1318 | "linewidth": 1, 1319 | "links": [], 1320 | "minSpan": 24, 1321 | "nullPointMode": "null", 1322 | "percentage": false, 1323 | "pointradius": 1, 1324 | "points": false, 1325 | "renderer": "flot", 1326 | "seriesOverrides": [], 1327 | "spaceLength": 10, 1328 | "stack": false, 1329 | "steppedLine": true, 1330 | "targets": [ 1331 | { 1332 | "alias": "$tag_transaction - $tag_responseCode : $tag_responseMessage", 1333 | "dsType": "influxdb", 1334 | "groupBy": [ 1335 | { 1336 | "params": [ 1337 | "30s" 1338 | ], 1339 | "type": "time" 1340 | }, 1341 | { 1342 | "params": [ 1343 | "responseMessage" 1344 | ], 1345 | "type": "tag" 1346 | }, 1347 | { 1348 | "params": [ 1349 | "responseCode" 1350 | ], 1351 | "type": "tag" 1352 | }, 1353 | { 1354 | "params": [ 1355 | "transaction" 1356 | ], 1357 | "type": "tag" 1358 | }, 1359 | { 1360 | "params": [ 1361 | "null" 1362 | ], 1363 | "type": "fill" 1364 | } 1365 | ], 1366 | "measurement": "jmeter", 1367 | "policy": "$retention", 1368 | "query": "SELECT sum(\"count\") / 5 FROM \"jmeter\" WHERE \"application\" =~ /$app$/ AND \"statut\" = 'ko' AND $timeFilter GROUP BY time($granularity), \"responseCode\" fill(null)", 1369 | "rawQuery": false, 1370 | "refId": "B", 1371 | "resultFormat": "time_series", 1372 | "select": [ 1373 | [ 1374 | { 1375 | "params": [ 1376 | "count" 1377 | ], 1378 | "type": "field" 1379 | }, 1380 | { 1381 | "params": [], 1382 | "type": "sum" 1383 | }, 1384 | { 1385 | "params": [ 1386 | " / 30" 1387 | ], 1388 | "type": "math" 1389 | } 1390 | ] 1391 | ], 1392 | "tags": [ 1393 | { 1394 | "key": "application", 1395 | "operator": "=~", 1396 | "value": "/$app$/" 1397 | }, 1398 | { 1399 | "condition": "AND", 1400 | "key": "responseCode", 1401 | "operator": "!~", 1402 | "value": "/^0$|^$/" 1403 | }, 1404 | { 1405 | "condition": "AND", 1406 | "key": "transaction", 1407 | "operator": "=~", 1408 | "value": "/$transaction/" 1409 | } 1410 | ] 1411 | } 1412 | ], 1413 | "thresholds": [], 1414 | "timeFrom": null, 1415 | "timeShift": null, 1416 | "title": "Error detail", 1417 | "tooltip": { 1418 | "msResolution": false, 1419 | "shared": true, 1420 | "sort": 0, 1421 | "value_type": "cumulative" 1422 | }, 1423 | "transparent": false, 1424 | "type": "graph", 1425 | "xaxis": { 1426 | "buckets": null, 1427 | "mode": "time", 1428 | "name": null, 1429 | "show": true, 1430 | "values": [] 1431 | }, 1432 | "yaxes": [ 1433 | { 1434 | "format": "none", 1435 | "label": "", 1436 | "logBase": 1, 1437 | "max": null, 1438 | "min": null, 1439 | "show": true 1440 | }, 1441 | { 1442 | "format": "short", 1443 | "logBase": 1, 1444 | "max": null, 1445 | "min": null, 1446 | "show": true 1447 | } 1448 | ], 1449 | "yaxis": { 1450 | "align": false, 1451 | "alignLevel": null 1452 | } 1453 | } 1454 | ], 1455 | "refresh": "30s", 1456 | "schemaVersion": 16, 1457 | "style": "dark", 1458 | "tags": [], 1459 | "templating": { 1460 | "list": [ 1461 | { 1462 | "allValue": ".*", 1463 | "current": {}, 1464 | "datasource": "${DS_JMETERDB}", 1465 | "hide": 0, 1466 | "includeAll": false, 1467 | "label": null, 1468 | "multi": true, 1469 | "name": "app", 1470 | "options": [], 1471 | "query": "SHOW TAG VALUES FROM \"process\" WITH KEY = \"application\"", 1472 | "refresh": 2, 1473 | "regex": "", 1474 | "sort": 1, 1475 | "tagValuesQuery": "", 1476 | "tags": [], 1477 | "tagsQuery": "SHOW TAG VALUES FROM \"events\" WITH KEY = \"tags\"", 1478 | "type": "query", 1479 | "useTags": false 1480 | }, 1481 | { 1482 | "allFormat": "regex wildcard", 1483 | "auto": false, 1484 | "auto_count": 10, 1485 | "auto_min": "10s", 1486 | "current": { 1487 | "text": "1m", 1488 | "value": "1m" 1489 | }, 1490 | "datasource": "jmeterdb", 1491 | "hide": 0, 1492 | "includeAll": true, 1493 | "label": "", 1494 | "multi": false, 1495 | "multiFormat": "glob", 1496 | "name": "granularity", 1497 | "options": [ 1498 | { 1499 | "selected": true, 1500 | "text": "1m", 1501 | "value": "1m" 1502 | }, 1503 | { 1504 | "selected": false, 1505 | "text": "5m", 1506 | "value": "5m" 1507 | }, 1508 | { 1509 | "selected": false, 1510 | "text": "1h", 1511 | "value": "1h" 1512 | }, 1513 | { 1514 | "selected": false, 1515 | "text": "5s", 1516 | "value": "5s" 1517 | }, 1518 | { 1519 | "selected": false, 1520 | "text": "15s", 1521 | "value": "15s" 1522 | }, 1523 | { 1524 | "selected": false, 1525 | "text": "30s", 1526 | "value": "30s" 1527 | } 1528 | ], 1529 | "query": "1m,5m,1h,5s,15s,30s", 1530 | "refresh": 2, 1531 | "regex": "", 1532 | "type": "interval" 1533 | }, 1534 | { 1535 | "allValue": null, 1536 | "current": {}, 1537 | "datasource": "${DS_JMETERDB}", 1538 | "hide": 0, 1539 | "includeAll": false, 1540 | "label": null, 1541 | "multi": false, 1542 | "name": "retention", 1543 | "options": [], 1544 | "query": "SHOW RETENTION POLICIES ON \"jmeterdb\"", 1545 | "refresh": 1, 1546 | "regex": "", 1547 | "sort": 0, 1548 | "tagValuesQuery": null, 1549 | "tags": [], 1550 | "tagsQuery": null, 1551 | "type": "query", 1552 | "useTags": false 1553 | }, 1554 | { 1555 | "allValue": ".*", 1556 | "current": {}, 1557 | "datasource": "${DS_JMETERDB}", 1558 | "hide": 0, 1559 | "includeAll": true, 1560 | "label": null, 1561 | "multi": true, 1562 | "name": "transaction", 1563 | "options": [], 1564 | "query": "SHOW TAG VALUES FROM \"jmeter\" WITH KEY IN (\"transaction\",\"application\") where application =~ /$app/ and transaction !~ /all/", 1565 | "refresh": 2, 1566 | "regex": "", 1567 | "sort": 0, 1568 | "tagValuesQuery": null, 1569 | "tags": [], 1570 | "tagsQuery": null, 1571 | "type": "query", 1572 | "useTags": false 1573 | } 1574 | ] 1575 | }, 1576 | "time": { 1577 | "from": "now-30m", 1578 | "to": "now" 1579 | }, 1580 | "timepicker": { 1581 | "now": true, 1582 | "refresh_intervals": [ 1583 | "10s", 1584 | "30s", 1585 | "1m", 1586 | "5m", 1587 | "15m", 1588 | "30m", 1589 | "1h", 1590 | "2h", 1591 | "1d" 1592 | ], 1593 | "time_options": [ 1594 | "5m", 1595 | "15m", 1596 | "1h", 1597 | "6h", 1598 | "12h", 1599 | "24h", 1600 | "2d", 1601 | "7d", 1602 | "30d" 1603 | ] 1604 | }, 1605 | "timezone": "browser", 1606 | "title": "JMeter Metric Template", 1607 | "uid": "ltaas", 1608 | "version": 1 1609 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jmeter-operator 2 | 3 | This operator was created to simplify the process of deploying a Jmeter cluster on kubernetes. 4 | 5 | The operator is capable of creating the following resources: 6 | 7 | - Jmeter Master 8 | - Jmeter Slaves 9 | - InfluxDB for metrics storage (optional) 10 | - Grafana to visualize the load testing metrics (optional) 11 | - Grafana reporter module, this is used to generate PDF reports of your load tests (optional) 12 | 13 | This effort is based partially on https://github.com/kubernauts/jmeter-kubernetes, if you desire an alternative deployment experience, you can check the repo further. 14 | 15 | The steps to use this operator is as follows: 16 | 17 | (1.) Clone this repo "**git clone https://github.com/kubernauts/jmeter-operator.git**" 18 | 19 | (2.) Install the Jmeter CRD (custom resource definition): 20 | 21 | "**kubectl apply -f deploy/crds/loadtest_v1alpha1_jmeter_crd.yaml**". 22 | 23 | This is a cluster-wide scope operator, the reason for this is that multiple Jmeter clusters may be needed within an organization but there is nothing stopping you from using just one Jmeter deployment but it is strongly advised that you run it in a dedicated namespace which will we will as we proceed 24 | 25 | (3.) Confirm that the CRD has been installed "**kubectl get crd | grep jmeter" or "kubectl describe crd jmeters.loadtest.jmeter.com**" 26 | 27 | From "**_kubectl describe crd jmeters.loadtest.jmeter.com_**", you should below as part of the output: 28 | 29 | ``` 30 | Group: loadtest.jmeter.com 31 | Names: 32 | Kind: Jmeter 33 | List Kind: JmeterList 34 | Plural: jmeters 35 | Singular: jmeter 36 | Scope: Namespaced 37 | Subresources: 38 | Status: 39 | Version: v1alpha1 40 | Versions: 41 | Name: v1alpha1 42 | Served: true 43 | Storage: true 44 | Status: 45 | Accepted Names: 46 | Kind: Jmeter 47 | List Kind: JmeterList 48 | Plural: jmeters 49 | Singular: jmeter 50 | Conditions: 51 | ``` 52 | 53 | (4.) Deploy the Jmeter operator deployment "kubectl apply -f deploy/" , this is what will watch the API for any jmeter CRD objects, once it detects the jmeter CRD, it will proceed to process that request and create the necessary kubernetes objects 54 | 55 | Check the status for the operator deployment (this is deployed in kube-system namespace by default) 56 | 57 | kubectl -n kube-system get pods | grep jmeter 58 | 59 | ``` 60 | jmeter-operator-6f54d969c7-w4h4l 1/1 Running 0 2m 61 | 62 | ``` 63 | 64 | (5.) Create a namespace for the jmeter deployment: "**kubectl create namespace tqa**" 65 | 66 | (6.) Create a Jmeter deployment manifest (e.g jmeter-deploy.yaml), example is given below: 67 | 68 | ``` 69 | apiVersion: loadtest.jmeter.com/v1alpha1 70 | kind: Jmeter 71 | metadata: 72 | name: tqa-loadtest 73 | namespace: tqa 74 | spec: 75 | # Add fields here 76 | slave_size: 2 77 | jmeter_master_image: kubernautslabs/jmeter_master:latest 78 | jmeter_slave_image: kubernautslabs/jmeter_slave:latest 79 | grafana_server_root: / 80 | grafana_service_type: LoadBalancer 81 | grafana_image: grafana/grafana:5.2.0 82 | influxdb_image: influxdb 83 | grafana_install: "true" 84 | grafana_reporter_install: "false" 85 | grafana_reporter_image: kubernautslabs/jmeter-reporter:latest 86 | influxdb_install: "true" 87 | ``` 88 | 89 | Run "**kubectl create -f jmeter-deploy.yaml**". As you can see, you can enable optional features and also modify some parameters like service type for the Grafana deployment (e.g. you can set this to ClusterIP if you want to expose the service via an Ingress) and the container images. 90 | 91 | N.B -- The "grafana_service_type" controls the kind of service type for both Grafana and Grafana Reporter 92 | 93 | Confirm that the resources have been created: 94 | 95 | **kubectl -n tqa get jmeter** 96 | 97 | ``` 98 | NAME AGE 99 | tqa-loadtest 1m 100 | ``` 101 | 102 | **kubectl -n tqa get all** 103 | 104 | ``` 105 | NAME READY STATUS RESTARTS AGE 106 | pod/tqa-loadtest-grafana-dc9749dc9-4ggxg 1/1 Running 0 3m 107 | pod/tqa-loadtest-influxdb-78b6c859cd-jstps 1/1 Running 0 3m 108 | pod/tqa-loadtest-jmeter-master-66c648668-htdml 1/1 Running 0 3m 109 | pod/tqa-loadtest-jmeter-slaves-c4787d59-275bj 1/1 Running 0 3m 110 | pod/tqa-loadtest-jmeter-slaves-c4787d59-fptrp 1/1 Running 0 3m 111 | 112 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 113 | service/tqa-loadtest-grafana LoadBalancer 100.64.222.93 ac6d0c369489dxxxxxxxxxxx-198965xxxx.eu-central-1.elb.amazonaws.com 3000:30882/TCP 3m 114 | service/tqa-loadtest-influxdb ClusterIP 100.64.232.50 8083/TCP,8086/TCP,2003/TCP 3m 115 | service/tqa-loadtest-jmeter-slaves-svc ClusterIP None 1099/TCP,50000/TCP 3m 116 | 117 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 118 | deployment.apps/tqa-loadtest-grafana 1 1 1 1 3m 119 | deployment.apps/tqa-loadtest-influxdb 1 1 1 1 3m 120 | deployment.apps/tqa-loadtest-jmeter-master 1 1 1 1 3m 121 | deployment.apps/tqa-loadtest-jmeter-slaves 2 2 2 2 3m 122 | 123 | NAME DESIRED CURRENT READY AGE 124 | replicaset.apps/tqa-loadtest-grafana-dc9749dc9 1 1 1 3m 125 | replicaset.apps/tqa-loadtest-influxdb-78b6c859cd 1 1 1 3m 126 | replicaset.apps/tqa-loadtest-jmeter-master-66c648668 1 1 1 3m 127 | replicaset.apps/tqa-loadtest-jmeter-slaves-c4787d59 2 2 2 3m 128 | 129 | ``` 130 | 131 | (7.) The next step is entirely optional, they are just to make creating a Jmeter load test easier, the scripts (**[initialize_cluster.sh](./initialize_cluster.sh) and [start_test.sh](./start_test.sh)**) can be modified to suit your needs as you desire. 132 | 133 | The initialize_cluster.sh script will create the database name in InfluxDB (default name is 'jmeter') and also create the InfluxDB datasource in Grafana. 134 | 135 | The script will ask you about the namespace where the jmeter cluster was created (tqa) and then proceed to create the needed resources in InfluxDB and Grafana. 136 | 137 | ```./initialize_cluster.sh 138 | ./initialize_cluster.sh 139 | 140 | Enter the Jmeter Namespace: tqa 141 | Creating Influxdb jmeter Database 142 | Creating the Influxdb data source 143 | {"datasource":{"id":1,"orgId":1,"name":"jmeterdb","type":"influxdb","typeLogoUrl":"","access":"proxy","url":"http://tqa-loadtest-influxdb:8086","password":"admin","user":"admin","database":"jmeter","basicAuth":false,"basicAuthUser":"","basicAuthPassword":"","withCredentials":false,"isDefault":true,"secureJsonFields":{},"version":1,"readOnly":false},"id":1,"message":"Datasource added","name":"jmeterdb"} 144 | ``` 145 | 146 | (8.) You can access your Grafana now and confirm whether the datasource was created. 147 | 148 | ![](img/grafana_datasource.png) 149 | 150 | (9.) Run a sample jmeter test script (there is a sample test script cloudssky.jmx in this repo). This can be initiated by running "**./start_test.sh**". 151 | 152 | ``` 153 | ./start_test.sh 154 | Enter the Jmeter Namespace: tqa 155 | Enter path to the jmx file cloudssky.jmx 156 | Mar 17, 2019 10:37:19 AM java.util.prefs.FileSystemPreferences$1 run 157 | INFO: Created user preferences directory. 158 | Creating summariser 159 | Created the tree successfully using cloudssky.jmx 160 | Configuring remote engine: 100.96.1.107 161 | Configuring remote engine: 100.96.3.207 162 | Starting remote engines 163 | Starting the test @ Sun Mar 17 10:37:19 UTC 2019 (1552819039563) 164 | Remote engines have been started 165 | Waiting for possible Shutdown/StopTestNow/Heapdump message on port 4445 166 | ``` 167 | 168 | N.B - It is important that you configure your script with the appropriate InfluxDB service name (tqa-loadtest-influxdb as per this documentation), sample is shown below: 169 | 170 | ``` 171 | http://tqa-loadtest-influxdb:8086/write?db=jmeter 172 | ``` 173 | 174 | Normally you will set this via the Jmeter desktop application to make this easier. 175 | 176 | Otherwise the graphs on Grafana may not show anything! 177 | 178 | (10.) Import the sample jmeter Grafana dashboard (GrafanaJMeterTemplate.json) and select the InfluxDB datasource that was created. Check the progress of the test: 179 | 180 | ![](img/test_progress.png) 181 | 182 | To learn more about the Grafana reporter module and how to make use of it, you can check the following blog post: 183 | 184 | 185 | 186 | ``` 187 | 188 | ``` 189 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/operator-framework/ansible-operator:v0.5.0 2 | 3 | COPY roles/ ${HOME}/roles/ 4 | COPY watches.yaml ${HOME}/watches.yaml 5 | -------------------------------------------------------------------------------- /build/test-framework/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASEIMAGE 2 | FROM ${BASEIMAGE} 3 | USER 0 4 | RUN yum install -y python-devel gcc libffi-devel && pip install molecule 5 | ARG NAMESPACEDMAN 6 | ADD $NAMESPACEDMAN /namespaced.yaml 7 | ADD build/test-framework/ansible-test.sh /ansible-test.sh 8 | RUN chmod +x /ansible-test.sh 9 | USER 1001 10 | ADD . /opt/ansible/project 11 | -------------------------------------------------------------------------------- /build/test-framework/ansible-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export WATCH_NAMESPACE=${TEST_NAMESPACE} 3 | (/usr/local/bin/entrypoint)& 4 | trap "kill $!" SIGINT SIGTERM EXIT 5 | 6 | cd ${HOME}/project 7 | exec molecule test -s test-cluster 8 | -------------------------------------------------------------------------------- /cloudssky.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This test plan was created by the BlazeMeter converter v.1.1.307. Please contact support@blazemeter.com for further support. 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Accept 18 | text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 19 | 20 | 21 | Upgrade-Insecure-Requests 22 | 1 23 | 24 | 25 | User-Agent 26 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 27 | 28 | 29 | Accept-Language 30 | en-GB,en-US;q=0.9,en;q=0.8 31 | 32 | 33 | Accept-Encoding 34 | gzip, deflate, br 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | BASE_URL_1 43 | cloudssky.com 44 | = 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | true 59 | true 60 | 6 61 | 62 | 63 | 64 | 65 | 66 | 67 | true 68 | false 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | true 78 | 79 | 80 | 81 | true 82 | false 83 | 84 | 85 | 86 | continue 87 | 88 | false 89 | 20 90 | 91 | 2 92 | 2 93 | 1363247040000 94 | 1363247040000 95 | false 96 | 0 97 | 0 98 | 99 | 100 | 101 | 102 | 103 | 104 | ${BASE_URL_1} 105 | 106 | https 107 | 108 | en/ 109 | GET 110 | true 111 | false 112 | true 113 | false 114 | 115 | 116 | 117 | 118 | 119 | 120 | 0 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | ${BASE_URL_1} 129 | 130 | https 131 | 132 | en/services/consulting/ 133 | GET 134 | true 135 | false 136 | true 137 | false 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | Referer 147 | https://cloudssky.com/en/ 148 | 149 | 150 | 151 | 152 | 153 | 10179 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | ${BASE_URL_1} 162 | 163 | https 164 | 165 | en/human-cloud/index.html 166 | GET 167 | true 168 | false 169 | true 170 | false 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | Referer 180 | https://cloudssky.com/en/services/consulting/ 181 | 182 | 183 | 184 | 185 | 186 | 27526 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | ${BASE_URL_1} 195 | 196 | https 197 | 198 | en/solutions/amazon-web-services/ 199 | GET 200 | true 201 | false 202 | true 203 | false 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | Referer 213 | https://cloudssky.com/en/human-cloud/index.html 214 | 215 | 216 | 217 | 218 | 219 | 20020 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | influxdbMetricsSender 228 | org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender 229 | = 230 | 231 | 232 | influxdbUrl 233 | http://tqa-loadtest-influxdb:8086/write?db=jmeter 234 | = 235 | 236 | 237 | application 238 | cloudssky 239 | = 240 | 241 | 242 | measurement 243 | jmeter 244 | = 245 | 246 | 247 | summaryOnly 248 | false 249 | = 250 | 251 | 252 | samplersRegex 253 | .* 254 | = 255 | 256 | 257 | percentiles 258 | 90;95;99 259 | = 260 | 261 | 262 | testTitle 263 | Test name 264 | = 265 | 266 | 267 | eventTags 268 | 269 | = 270 | 271 | 272 | 273 | org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient 274 | 275 | 276 | 277 | 278 | 279 | true 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /deploy/crds/loadtest_v1alpha1_jmeter_cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: loadtest.jmeter.com/v1alpha1 2 | kind: Jmeter 3 | metadata: 4 | name: tqa 5 | namespace: tqa 6 | spec: 7 | # Add fields here 8 | slave_size: 2 9 | jmeter_master_image: kubernautslabs/jmeter_master:latest 10 | jmeter_slave_image: kubernautslabs/jmeter_slave:latest 11 | grafana_server_root: / 12 | grafana_service_type: LoadBalancer 13 | grafana_image: grafana/grafana:5.2.0 14 | influxdb_image: influxdb 15 | grafana_install: "true" 16 | grafana_reporter_install: "false" 17 | grafana_reporter_image: kubernautslabs/jmeter-reporter:latest 18 | influxdb_install: "true" 19 | -------------------------------------------------------------------------------- /deploy/crds/loadtest_v1alpha1_jmeter_crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: jmeters.loadtest.jmeter.com 5 | namespace: jmeter-loadtest 6 | spec: 7 | group: loadtest.jmeter.com 8 | names: 9 | kind: Jmeter 10 | listKind: JmeterList 11 | plural: jmeters 12 | singular: jmeter 13 | scope: Namespaced 14 | subresources: 15 | status: {} 16 | version: v1alpha1 17 | versions: 18 | - name: v1alpha1 19 | served: true 20 | storage: true 21 | -------------------------------------------------------------------------------- /deploy/operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: jmeter-operator 5 | namespace: kube-system 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: jmeter-operator 11 | template: 12 | metadata: 13 | labels: 14 | name: jmeter-operator 15 | spec: 16 | serviceAccountName: jmeter-operator 17 | containers: 18 | - name: jmeter-operator 19 | # Replace this with the built image name 20 | image: kubernautslabs/jmeter-operator:v0.0.1 21 | imagePullPolicy: Always 22 | env: 23 | - name: WATCH_NAMESPACE 24 | value: "" 25 | - name: POD_NAME 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.name 29 | - name: OPERATOR_NAME 30 | value: "jmeter-operator" -------------------------------------------------------------------------------- /deploy/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | creationTimestamp: null 5 | name: jmeter-operator 6 | namespace: kube-system 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - services 13 | - endpoints 14 | - persistentvolumeclaims 15 | - events 16 | - configmaps 17 | - secrets 18 | verbs: 19 | - '*' 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - namespaces 24 | verbs: 25 | - get 26 | - apiGroups: 27 | - apps 28 | resources: 29 | - deployments 30 | - daemonsets 31 | - replicasets 32 | - statefulsets 33 | verbs: 34 | - '*' 35 | - apiGroups: 36 | - monitoring.coreos.com 37 | resources: 38 | - servicemonitors 39 | verbs: 40 | - get 41 | - create 42 | - apiGroups: 43 | - apps 44 | resourceNames: 45 | - jmeter-operator 46 | resources: 47 | - deployments/finalizers 48 | verbs: 49 | - update 50 | - apiGroups: 51 | - loadtest.jmeter.com 52 | resources: 53 | - '*' 54 | verbs: 55 | - '*' 56 | -------------------------------------------------------------------------------- /deploy/role_binding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: jmeter-operator 5 | subjects: 6 | - kind: ServiceAccount 7 | name: jmeter-operator 8 | namespace: kube-system 9 | roleRef: 10 | kind: ClusterRole 11 | name: jmeter-operator 12 | apiGroup: rbac.authorization.k8s.io 13 | -------------------------------------------------------------------------------- /deploy/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: jmeter-operator 5 | namespace: kube-system 6 | -------------------------------------------------------------------------------- /img/grafana_datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubernauts/jmeter-operator/9de46870386bb5438e7183f41e99add44d0cc6ca/img/grafana_datasource.png -------------------------------------------------------------------------------- /img/test_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubernauts/jmeter-operator/9de46870386bb5438e7183f41e99add44d0cc6ca/img/test_progress.png -------------------------------------------------------------------------------- /initialize_cluster.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Create jmeter database automatically in Influxdb 4 | 5 | namespace="$1" 6 | 7 | [ -n "$namespace" ] || read -p 'Enter the Jmeter Namespace: ' namespace 8 | 9 | kubectl get namespace | grep $namespace >> /dev/null 10 | 11 | if [ $? != 0 ]; 12 | then 13 | echo "Namespace does not exist in the kubernetes cluster" 14 | echo "" 15 | echo "Below is the list of namespaces in the kubernetes cluster" 16 | 17 | kubectl get namespaces 18 | echo "" 19 | echo "Please check and try again" 20 | exit 21 | fi 22 | 23 | echo "Creating Influxdb jmeter Database" 24 | 25 | ##Wait until Influxdb Deployment is up and running 26 | ##influxdb_status=`kubectl get po -n $tenant | grep influxdb-jmeter | awk '{print $2}' | grep Running 27 | 28 | influxdb_pod=`kubectl -n $namespace get po | grep influxdb | awk '{print $1}'` 29 | 30 | kubectl -n $namespace exec -ti $influxdb_pod -- influx -execute 'CREATE DATABASE jmeter' 31 | 32 | ## Create the influxdb datasource in Grafana 33 | 34 | echo "Creating the Influxdb data source" 35 | 36 | grafana_pod=`kubectl -n $namespace get po | grep grafana | awk '{print $1}'` 37 | 38 | ## Make load test script in Jmeter master pod executable 39 | 40 | #Get Master pod details 41 | 42 | master_pod=`kubectl -n $namespace get po | grep master | awk '{print $1}'` 43 | 44 | # Workaround for the read only attribute of config map 45 | kubectl -n $namespace exec -ti $master_pod -- cp -rf /load_test /jmeter/load_test 46 | 47 | kubectl -n $namespace exec -ti $master_pod -- chmod 755 /jmeter/load_test 48 | 49 | #Get the name of the influxDB svc 50 | 51 | influxdb_svc=`kubectl -n $namespace get svc | grep influxdb | awk '{print $1}'` 52 | 53 | kubectl -n $namespace exec -ti $grafana_pod -- curl 'http://admin:admin@127.0.0.1:3000/api/datasources' -X POST -H 'Content-Type: application/json;charset=UTF-8' --data-binary '{"name":"jmeterdb","type":"influxdb","url":"http://'$influxdb_svc':8086","access":"proxy","isDefault":true,"database":"jmeter","user":"admin","password":"admin"}' -------------------------------------------------------------------------------- /jmeter-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: loadtest.jmeter.com/v1alpha1 2 | kind: Jmeter 3 | metadata: 4 | name: tqa-loadtest 5 | namespace: tqa 6 | spec: 7 | # Add fields here 8 | slave_size: 2 9 | jmeter_master_image: kubernautslabs/jmeter_master:5.0 10 | jmeter_slave_image: kubernautslabs/jmeter_slave:5.0 11 | grafana_server_root: / 12 | grafana_service_type: LoadBalancer 13 | grafana_image: grafana/grafana:5.2.0 14 | influxdb_image: influxdb 15 | grafana_install: "true" 16 | grafana_reporter_install: "false" 17 | grafana_reporter_image: kubernautslabs/jmeter-reporter:latest 18 | influxdb_install: "true" 19 | -------------------------------------------------------------------------------- /molecule/default/asserts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Verify 4 | hosts: localhost 5 | connection: local 6 | vars: 7 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 8 | tasks: 9 | - name: Get all pods in {{ namespace }} 10 | k8s_facts: 11 | api_version: v1 12 | kind: Pod 13 | namespace: '{{ namespace }}' 14 | register: pods 15 | 16 | - name: Output pods 17 | debug: var=pods 18 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | lint: 7 | name: yamllint 8 | enabled: False 9 | platforms: 10 | - name: kind-default 11 | groups: 12 | - k8s 13 | image: bsycorp/kind:latest-1.12 14 | privileged: True 15 | override_command: no 16 | exposed_ports: 17 | - 8443/tcp 18 | - 10080/tcp 19 | published_ports: 20 | - 0.0.0.0:${TEST_CLUSTER_PORT:-9443}:8443/tcp 21 | pre_build_image: yes 22 | provisioner: 23 | name: ansible 24 | log: True 25 | lint: 26 | name: ansible-lint 27 | enabled: False 28 | inventory: 29 | group_vars: 30 | all: 31 | namespace: ${TEST_NAMESPACE:-osdk-test} 32 | env: 33 | K8S_AUTH_KUBECONFIG: /tmp/molecule/kind-default/kubeconfig 34 | KUBECONFIG: /tmp/molecule/kind-default/kubeconfig 35 | ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles 36 | KIND_PORT: '${TEST_CLUSTER_PORT:-9443}' 37 | scenario: 38 | name: default 39 | verifier: 40 | name: testinfra 41 | lint: 42 | name: flake8 43 | -------------------------------------------------------------------------------- /molecule/default/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: localhost 4 | connection: local 5 | vars: 6 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 7 | roles: 8 | - jmeter 9 | 10 | - import_playbook: '{{ playbook_dir }}/asserts.yml' 11 | -------------------------------------------------------------------------------- /molecule/default/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare 3 | hosts: k8s 4 | gather_facts: no 5 | vars: 6 | kubeconfig: "{{ lookup('env', 'KUBECONFIG') }}" 7 | tasks: 8 | - name: delete the kubeconfig if present 9 | file: 10 | path: '{{ kubeconfig }}' 11 | state: absent 12 | delegate_to: localhost 13 | 14 | - name: Fetch the kubeconfig 15 | fetch: 16 | dest: '{{ kubeconfig }}' 17 | flat: yes 18 | src: /root/.kube/config 19 | 20 | - name: Change the kubeconfig port to the proper value 21 | replace: 22 | regexp: 8443 23 | replace: "{{ lookup('env', 'KIND_PORT') }}" 24 | path: '{{ kubeconfig }}' 25 | delegate_to: localhost 26 | 27 | - name: Wait for the Kubernetes API to become available (this could take a minute) 28 | uri: 29 | url: "https://localhost:8443/apis" 30 | status_code: 200 31 | validate_certs: no 32 | register: result 33 | until: (result.status|default(-1)) == 200 34 | retries: 60 35 | delay: 5 36 | -------------------------------------------------------------------------------- /molecule/test-cluster/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: delegated 6 | options: 7 | managed: False 8 | ansible_connection_options: {} 9 | lint: 10 | name: yamllint 11 | enabled: False 12 | platforms: 13 | - name: test-cluster 14 | groups: 15 | - k8s 16 | provisioner: 17 | name: ansible 18 | inventory: 19 | group_vars: 20 | all: 21 | namespace: ${TEST_NAMESPACE:-osdk-test} 22 | lint: 23 | name: ansible-lint 24 | enabled: False 25 | env: 26 | ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles 27 | scenario: 28 | name: test-cluster 29 | test_sequence: 30 | - lint 31 | - destroy 32 | - dependency 33 | - syntax 34 | - create 35 | - prepare 36 | - converge 37 | - side_effect 38 | - verify 39 | - destroy 40 | verifier: 41 | name: testinfra 42 | lint: 43 | name: flake8 44 | -------------------------------------------------------------------------------- /molecule/test-cluster/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Converge 4 | hosts: localhost 5 | connection: local 6 | vars: 7 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 8 | deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" 9 | image_name: loadtest.jmeter.com/jmeter-operator:testing 10 | custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/loadtest_v1alpha1_jmeter_cr.yaml'])) | from_yaml }}" 11 | tasks: 12 | - name: Create the loadtest.jmeter.com/v1alpha1.Jmeter 13 | k8s: 14 | namespace: '{{ namespace }}' 15 | definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/loadtest_v1alpha1_jmeter_cr.yaml'])) }}" 16 | 17 | - name: Get the newly created Custom Resource 18 | debug: 19 | msg: "{{ lookup('k8s', group='loadtest.jmeter.com', api_version='v1alpha1', kind='Jmeter', namespace=namespace, resource_name=custom_resource.metadata.name) }}" 20 | 21 | - name: Wait 40s for reconciliation to run 22 | k8s_facts: 23 | api_version: 'v1alpha1' 24 | kind: 'Jmeter' 25 | namespace: '{{ namespace }}' 26 | name: '{{ custom_resource.metadata.name }}' 27 | register: reconcile_cr 28 | until: 29 | - "'Successful' in (reconcile_cr | json_query('resources[].status.conditions[].reason'))" 30 | delay: 4 31 | retries: 10 32 | 33 | - import_playbook: "{{ playbook_dir }}/../default/asserts.yml" 34 | -------------------------------------------------------------------------------- /molecule/test-local/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | lint: 7 | name: yamllint 8 | enabled: False 9 | platforms: 10 | - name: kind-test-local 11 | groups: 12 | - k8s 13 | image: bsycorp/kind:latest-1.12 14 | privileged: True 15 | override_command: no 16 | exposed_ports: 17 | - 8443/tcp 18 | - 10080/tcp 19 | published_ports: 20 | - 0.0.0.0:${TEST_CLUSTER_PORT:-10443}:8443/tcp 21 | pre_build_image: yes 22 | volumes: 23 | - ${MOLECULE_PROJECT_DIRECTORY}:/build:Z 24 | provisioner: 25 | name: ansible 26 | log: True 27 | lint: 28 | name: ansible-lint 29 | enabled: False 30 | inventory: 31 | group_vars: 32 | all: 33 | namespace: ${TEST_NAMESPACE:-osdk-test} 34 | env: 35 | K8S_AUTH_KUBECONFIG: /tmp/molecule/kind-test-local/kubeconfig 36 | KUBECONFIG: /tmp/molecule/kind-test-local/kubeconfig 37 | ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles 38 | KIND_PORT: '${TEST_CLUSTER_PORT:-10443}' 39 | scenario: 40 | name: test-local 41 | test_sequence: 42 | - lint 43 | - destroy 44 | - dependency 45 | - syntax 46 | - create 47 | - prepare 48 | - converge 49 | - side_effect 50 | - verify 51 | - destroy 52 | verifier: 53 | name: testinfra 54 | lint: 55 | name: flake8 56 | -------------------------------------------------------------------------------- /molecule/test-local/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Build Operator in Kubernetes docker container 4 | hosts: k8s 5 | vars: 6 | image_name: loadtest.jmeter.com/jmeter-operator:testing 7 | tasks: 8 | # using command so we don't need to install any dependencies 9 | - name: Get existing image hash 10 | command: docker images -q {{image_name}} 11 | register: prev_hash 12 | changed_when: false 13 | 14 | - name: Build Operator Image 15 | command: docker build -f /build/build/Dockerfile -t {{ image_name }} /build 16 | register: build_cmd 17 | changed_when: not prev_hash.stdout or (prev_hash.stdout and prev_hash.stdout not in ''.join(build_cmd.stdout_lines[-2:])) 18 | 19 | - name: Converge 20 | hosts: localhost 21 | connection: local 22 | vars: 23 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 24 | deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" 25 | pull_policy: Never 26 | REPLACE_IMAGE: loadtest.jmeter.com/jmeter-operator:testing 27 | custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/loadtest_v1alpha1_jmeter_cr.yaml'])) | from_yaml }}" 28 | tasks: 29 | - block: 30 | - name: Delete the Operator Deployment 31 | k8s: 32 | state: absent 33 | namespace: '{{ namespace }}' 34 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}" 35 | register: delete_deployment 36 | when: hostvars[groups.k8s.0].build_cmd.changed 37 | 38 | - name: Wait 30s for Operator Deployment to terminate 39 | k8s_facts: 40 | api_version: '{{ definition.apiVersion }}' 41 | kind: '{{ definition.kind }}' 42 | namespace: '{{ namespace }}' 43 | name: '{{ definition.metadata.name }}' 44 | vars: 45 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}" 46 | register: deployment 47 | until: not deployment.resources 48 | delay: 3 49 | retries: 10 50 | when: delete_deployment.changed 51 | 52 | - name: Create the Operator Deployment 53 | k8s: 54 | namespace: '{{ namespace }}' 55 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}" 56 | 57 | - name: Create the loadtest.jmeter.com/v1alpha1.Jmeter 58 | k8s: 59 | state: present 60 | namespace: '{{ namespace }}' 61 | definition: "{{ custom_resource }}" 62 | 63 | - name: Wait 40s for reconciliation to run 64 | k8s_facts: 65 | api_version: '{{ custom_resource.apiVersion }}' 66 | kind: '{{ custom_resource.kind }}' 67 | namespace: '{{ namespace }}' 68 | name: '{{ custom_resource.metadata.name }}' 69 | register: cr 70 | until: 71 | - "'Successful' in (cr | json_query('resources[].status.conditions[].reason'))" 72 | delay: 4 73 | retries: 10 74 | rescue: 75 | - name: debug cr 76 | ignore_errors: yes 77 | failed_when: false 78 | debug: 79 | var: debug_cr 80 | vars: 81 | debug_cr: '{{ lookup("k8s", 82 | kind=custom_resource.kind, 83 | api_version=custom_resource.apiVersion, 84 | namespace=namespace, 85 | resource_name=custom_resource.metadata.name 86 | )}}' 87 | 88 | - name: debug memcached lookup 89 | ignore_errors: yes 90 | failed_when: false 91 | debug: 92 | var: deploy 93 | vars: 94 | deploy: '{{ lookup("k8s", 95 | kind="Deployment", 96 | api_version="apps/v1", 97 | namespace=namespace, 98 | label_selector="app=memcached" 99 | )}}' 100 | 101 | - name: get operator logs 102 | ignore_errors: yes 103 | failed_when: false 104 | command: kubectl logs deployment/{{ definition.metadata.name }} -n {{ namespace }} 105 | environment: 106 | KUBECONFIG: '{{ lookup("env", "KUBECONFIG") }}' 107 | vars: 108 | definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}" 109 | register: log 110 | 111 | - debug: var=log.stdout_lines 112 | 113 | - fail: 114 | msg: "Failed on action: converge" 115 | 116 | - import_playbook: '{{ playbook_dir }}/../default/asserts.yml' 117 | -------------------------------------------------------------------------------- /molecule/test-local/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_playbook: ../default/prepare.yml 3 | 4 | - name: Prepare operator resources 5 | hosts: localhost 6 | connection: local 7 | vars: 8 | ansible_python_interpreter: '{{ ansible_playbook_python }}' 9 | deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" 10 | tasks: 11 | - name: Create Custom Resource Definition 12 | k8s: 13 | definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/loadtest_v1alpha1_jmeter_crd.yaml'])) }}" 14 | 15 | - name: Ensure specified namespace is present 16 | k8s: 17 | api_version: v1 18 | kind: Namespace 19 | name: '{{ namespace }}' 20 | 21 | - name: Create RBAC resources 22 | k8s: 23 | definition: "{{ lookup('template', '/'.join([deploy_dir, item])) }}" 24 | namespace: '{{ namespace }}' 25 | with_items: 26 | - role.yaml 27 | - role_binding.yaml 28 | - service_account.yaml 29 | -------------------------------------------------------------------------------- /roles/jmeter/README.md: -------------------------------------------------------------------------------- 1 | Role Name 2 | ========= 3 | 4 | A brief description of the role goes here. 5 | 6 | Requirements 7 | ------------ 8 | 9 | Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. 10 | 11 | Role Variables 12 | -------------- 13 | 14 | A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. 15 | 16 | Dependencies 17 | ------------ 18 | 19 | A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. 20 | 21 | Example Playbook 22 | ---------------- 23 | 24 | Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: 25 | 26 | - hosts: servers 27 | roles: 28 | - { role: username.rolename, x: 42 } 29 | 30 | License 31 | ------- 32 | 33 | BSD 34 | 35 | Author Information 36 | ------------------ 37 | 38 | An optional section for the role authors to include contact information, or a website (HTML is not allowed). 39 | -------------------------------------------------------------------------------- /roles/jmeter/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for jmeter 3 | slave_size: 2 4 | jmeter_master_image: kubernautslabs/jmeter_master:latest 5 | jmeter_slave_image: kubernautslabs/jmeter_slave:latest 6 | grafana_server_root: / 7 | grafana_service_type: NodePort 8 | grafana_image: grafana/grafana:5.2.0 9 | grafana_install: "true" 10 | grafana_reporter_install: "false" 11 | grafana_reporter_image: kubernautslabs/jmeter-reporter:latest 12 | influxdb_install: "true" 13 | -------------------------------------------------------------------------------- /roles/jmeter/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for jmeter 3 | -------------------------------------------------------------------------------- /roles/jmeter/meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: your name 3 | description: your description 4 | company: your company (optional) 5 | 6 | # If the issue tracker for your role is not on github, uncomment the 7 | # next line and provide a value 8 | # issue_tracker_url: http://example.com/issue/tracker 9 | 10 | # Some suggested licenses: 11 | # - BSD (default) 12 | # - MIT 13 | # - GPLv2 14 | # - GPLv3 15 | # - Apache 16 | # - CC-BY 17 | license: license (GPLv2, CC-BY, etc) 18 | 19 | min_ansible_version: 2.4 20 | 21 | # If this a Container Enabled role, provide the minimum Ansible Container version. 22 | # min_ansible_container_version: 23 | 24 | # Optionally specify the branch Galaxy will use when accessing the GitHub 25 | # repo for this role. During role install, if no tags are available, 26 | # Galaxy will use this branch. During import Galaxy will access files on 27 | # this branch. If Travis integration is configured, only notifications for this 28 | # branch will be accepted. Otherwise, in all cases, the repo's default branch 29 | # (usually master) will be used. 30 | #github_branch: 31 | 32 | # 33 | # Provide a list of supported platforms, and for each platform a list of versions. 34 | # If you don't wish to enumerate all versions for a particular platform, use 'all'. 35 | # To view available platforms and versions (or releases), visit: 36 | # https://galaxy.ansible.com/api/v1/platforms/ 37 | # 38 | # platforms: 39 | # - name: Fedora 40 | # versions: 41 | # - all 42 | # - 25 43 | # - name: SomePlatform 44 | # versions: 45 | # - all 46 | # - 1.0 47 | # - 7 48 | # - 99.99 49 | 50 | galaxy_tags: [] 51 | # List tags for your role here, one per line. A tag is a keyword that describes 52 | # and categorizes the role. Users find roles by searching for tags. Be sure to 53 | # remove the '[]' above, if you add tags to this list. 54 | # 55 | # NOTE: A tag is limited to a single word comprised of alphanumeric characters. 56 | # Maximum 20 tags per role. 57 | 58 | dependencies: [] 59 | # List your role dependencies here, one per line. Be sure to remove the '[]' above, 60 | # if you add dependencies to this list. -------------------------------------------------------------------------------- /roles/jmeter/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for jmeter 3 | - name: Create Jmeter Master Configmap 4 | k8s: 5 | state: present 6 | definition: 7 | kind: ConfigMap 8 | metadata: 9 | name: '{{ meta.name }}-jmeter-master-config' 10 | namespace: '{{ meta.namespace }}' 11 | labels: 12 | jmeter_mode: '{{ meta.name }}-jmeter-master' 13 | data: 14 | load_test: | 15 | #!/bin/bash 16 | #Script created to invoke jmeter test script with the slave POD IP addresses 17 | #Script should be run like: ./load_test "path to the test script in jmx format" 18 | /jmeter/apache-jmeter-*/bin/jmeter -n -t $1 -Dserver.rmi.ssl.disable=true -R `getent ahostsv4 '{{ meta.name }}-jmeter-slaves-svc' | cut -d' ' -f1 | sort -u | awk -v ORS=, '{print $1}' | sed 's/,$//'` 19 | 20 | - name: Start Jmeter Master 21 | k8s: 22 | state: present 23 | definition: 24 | apiVersion: apps/v1 25 | kind: Deployment 26 | metadata: 27 | name: '{{ meta.name }}-jmeter-master' 28 | namespace: '{{ meta.namespace }}' 29 | labels: 30 | jmeter_mode: '{{ meta.name }}-jmeter-master' 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | jmeter_mode: '{{ meta.name }}-jmeter-master' 36 | template: 37 | metadata: 38 | labels: 39 | jmeter_mode: '{{ meta.name }}-jmeter-master' 40 | spec: 41 | containers: 42 | - name: jmetermaster 43 | image: "{{jmeter_master_image}}" 44 | imagePullPolicy: IfNotPresent 45 | command: [ "/bin/bash", "-c", "--" ] 46 | args: [ "while true; do sleep 30; done;" ] 47 | volumeMounts: 48 | - name: loadtest 49 | mountPath: /load_test 50 | subPath: "load_test" 51 | ports: 52 | - containerPort: 60000 53 | volumes: 54 | - name: loadtest 55 | configMap: 56 | name: '{{ meta.name }}-jmeter-master-config' 57 | 58 | - name: Start Jmeter Slaves 59 | k8s: 60 | state: present 61 | definition: 62 | apiVersion: apps/v1 63 | kind: Deployment 64 | metadata: 65 | name: '{{ meta.name }}-jmeter-slaves' 66 | namespace: '{{ meta.namespace }}' 67 | labels: 68 | jmeter_mode: '{{ meta.name }}-slave' 69 | spec: 70 | replicas: "{{slave_size}}" 71 | selector: 72 | matchLabels: 73 | jmeter_mode: '{{ meta.name }}-slave' 74 | template: 75 | metadata: 76 | labels: 77 | jmeter_mode: '{{ meta.name }}-slave' 78 | spec: 79 | containers: 80 | - name: jmslave 81 | image: "{{jmeter_slave_image}}" 82 | imagePullPolicy: IfNotPresent 83 | ports: 84 | - containerPort: 1099 85 | - containerPort: 50000 86 | 87 | - name: Create Jmeter Slave Service 88 | k8s: 89 | state: present 90 | definition: 91 | apiVersion: v1 92 | kind: Service 93 | metadata: 94 | name: '{{ meta.name }}-jmeter-slaves-svc' 95 | namespace: '{{ meta.namespace }}' 96 | labels: 97 | jmeter_mode: '{{ meta.name }}-slave' 98 | spec: 99 | clusterIP: None 100 | ports: 101 | - port: 1099 102 | name: first 103 | targetPort: 1099 104 | - port: 50000 105 | name: second 106 | targetPort: 50000 107 | selector: 108 | jmeter_mode: '{{ meta.name }}-slave' 109 | 110 | - name: Create InfluxDB Configmap 111 | when: influxdb_install == "true" 112 | k8s: 113 | state: present 114 | definition: 115 | kind: ConfigMap 116 | metadata: 117 | name: '{{ meta.name }}-influxdb-config' 118 | namespace: '{{ meta.namespace }}' 119 | labels: 120 | jmeter_mode: '{{ meta.name }}-influxdb' 121 | data: 122 | influxdb.conf: | 123 | [meta] 124 | dir = "/var/lib/influxdb/meta" 125 | [data] 126 | dir = "/var/lib/influxdb/data" 127 | engine = "tsm1" 128 | wal-dir = "/var/lib/influxdb/wal" 129 | # Configure the graphite api 130 | [[graphite]] 131 | enabled = true 132 | bind-address = ":2003" # If not set, is actually set to bind-address. 133 | database = "jmeter" # store graphite data in this database 134 | 135 | - name: InfluxDB Deployment 136 | when: influxdb_install == "true" 137 | k8s: 138 | state: present 139 | definition: 140 | apiVersion: apps/v1 141 | kind: Deployment 142 | metadata: 143 | name: '{{ meta.name }}-influxdb' 144 | namespace: '{{ meta.namespace }}' 145 | labels: 146 | app: '{{ meta.name }}-influxdb' 147 | spec: 148 | replicas: 1 149 | selector: 150 | matchLabels: 151 | app: '{{ meta.name }}-influxdb' 152 | template: 153 | metadata: 154 | labels: 155 | app: '{{ meta.name }}-influxdb' 156 | spec: 157 | containers: 158 | - image: "{{influxdb_image}}" 159 | imagePullPolicy: IfNotPresent 160 | name: influxdb 161 | volumeMounts: 162 | - name: config-volume 163 | mountPath: /etc/influxdb 164 | ports: 165 | - containerPort: 8083 166 | name: influx 167 | - containerPort: 8086 168 | name: api 169 | - containerPort: 2003 170 | name: graphite 171 | volumes: 172 | - name: config-volume 173 | configMap: 174 | name: '{{ meta.name }}-influxdb-config' 175 | 176 | - name: InfluxDB Service 177 | when: influxdb_install == "true" 178 | k8s: 179 | state: present 180 | definition: 181 | apiVersion: v1 182 | kind: Service 183 | metadata: 184 | name: '{{ meta.name }}-influxdb' 185 | namespace: '{{ meta.namespace }}' 186 | labels: 187 | app: '{{ meta.name }}-influxdb' 188 | spec: 189 | ports: 190 | - port: 8083 191 | name: http 192 | targetPort: 8083 193 | - port: 8086 194 | name: api 195 | targetPort: 8086 196 | - port: 2003 197 | name: graphite 198 | targetPort: 2003 199 | selector: 200 | app: '{{ meta.name }}-influxdb' 201 | - name: Grafana Deployment 202 | when: grafana_install == "true" 203 | k8s: 204 | state: present 205 | definition: 206 | apiVersion: apps/v1 207 | kind: Deployment 208 | metadata: 209 | name: '{{ meta.name }}-grafana' 210 | namespace: '{{ meta.namespace }}' 211 | labels: 212 | app: '{{ meta.name }}-grafana' 213 | spec: 214 | replicas: 1 215 | selector: 216 | matchLabels: 217 | app: '{{ meta.name }}-grafana' 218 | template: 219 | metadata: 220 | labels: 221 | app: '{{ meta.name }}-grafana' 222 | spec: 223 | containers: 224 | - name: grafana 225 | image: "{{grafana_image}}" 226 | imagePullPolicy: IfNotPresent 227 | ports: 228 | - containerPort: 3000 229 | protocol: TCP 230 | env: 231 | - name: GF_AUTH_BASIC_ENABLED 232 | value: "true" 233 | - name: GF_USERS_ALLOW_ORG_CREATE 234 | value: "true" 235 | - name: GF_AUTH_ANONYMOUS_ENABLED 236 | value: "true" 237 | - name: GF_AUTH_ANONYMOUS_ORG_ROLE 238 | value: Admin 239 | - name: GF_SERVER_ROOT_URL 240 | # If you're only using the API Server proxy, set this value instead: 241 | # value: /api/v1/namespaces/kube-system/services/monitoring-grafana/proxy 242 | value: "{{grafana_server_root}}" 243 | 244 | - name: Grafana Service 245 | when: grafana_install == "true" 246 | k8s: 247 | state: present 248 | definition: 249 | apiVersion: v1 250 | kind: Service 251 | metadata: 252 | name: '{{ meta.name }}-grafana' 253 | namespace: '{{ meta.namespace }}' 254 | labels: 255 | app: '{{ meta.name }}-grafana' 256 | spec: 257 | ports: 258 | - port: 3000 259 | targetPort: 3000 260 | selector: 261 | app: '{{ meta.name }}-grafana' 262 | type: "{{grafana_service_type}}" 263 | 264 | - name: Grafana-Reporter Deployment 265 | when: grafana_reporter_install == "true" 266 | k8s: 267 | state: present 268 | definition: 269 | apiVersion: apps/v1 270 | kind: Deployment 271 | metadata: 272 | name: '{{ meta.name }}-jmeter-reporter' 273 | namespace: '{{ meta.namespace }}' 274 | labels: 275 | jmeter_mode: '{{ meta.name }}-jmeter-reporter' 276 | spec: 277 | replicas: 1 278 | selector: 279 | matchLabels: 280 | jmeter_mode: '{{ meta.name }}-jmeter-reporter' 281 | template: 282 | metadata: 283 | labels: 284 | jmeter_mode: '{{ meta.name }}-jmeter-reporter' 285 | spec: 286 | containers: 287 | - name: jmreporter 288 | image: "{{grafana_reporter_image}}" 289 | imagePullPolicy: IfNotPresent 290 | ports: 291 | - containerPort: 8686 292 | 293 | - name: Grafana-Reporter Service 294 | when: grafana_reporter_install == "true" 295 | k8s: 296 | state: present 297 | definition: 298 | apiVersion: v1 299 | kind: Service 300 | metadata: 301 | name: '{{ meta.name }}-jmeter-reporter' 302 | namespace: '{{ meta.namespace }}' 303 | labels: 304 | jmeter_mode: '{{ meta.name }}-jmeter-reporter' 305 | spec: 306 | ports: 307 | - port: 8686 308 | targetPort: 8686 309 | selector: 310 | jmeter_mode: '{{ meta.name }}-jmeter-reporter' 311 | type: "{{grafana_service_type}}" 312 | 313 | - name: Delete InfluxDB Configmap if set to false 314 | when: influxdb_install == "false" 315 | k8s: 316 | state: absent 317 | api_version: v1 318 | kind: ConfigMap 319 | namespace: '{{ meta.namespace }}' 320 | name: '{{ meta.name }}-influxdb-config' 321 | 322 | - name: Delete InfluxDB Deployment if set to false 323 | when: influxdb_install == "false" 324 | k8s: 325 | state: absent 326 | api_version: apps/v1 327 | kind: Deployment 328 | namespace: '{{ meta.namespace }}' 329 | name: '{{ meta.name }}-influxdb' 330 | 331 | - name: Delete InfluxDB Service if set to false 332 | when: influxdb_install == "false" 333 | k8s: 334 | state: absent 335 | api_version: v1 336 | kind: Service 337 | namespace: '{{ meta.namespace }}' 338 | name: '{{ meta.name }}-influxdb' 339 | 340 | - name: Delete Grafana Deployment if set to false 341 | when: grafana_install == "false" 342 | k8s: 343 | state: absent 344 | api_version: apps/v1 345 | kind: Deployment 346 | namespace: '{{ meta.namespace }}' 347 | name: '{{ meta.name }}-grafana' 348 | 349 | - name: Delete Grafana Service if set to false 350 | when: grafana_install == "false" 351 | k8s: 352 | state: absent 353 | api_version: v1 354 | kind: Service 355 | namespace: '{{ meta.namespace }}' 356 | name: '{{ meta.name }}-grafana' 357 | 358 | - name: Delete Grafana Reporter Deployment if set to false 359 | when: grafana_reporter_install == "false" 360 | k8s: 361 | state: absent 362 | api_version: apps/v1 363 | kind: Deployment 364 | namespace: '{{ meta.namespace }}' 365 | name: '{{ meta.name }}-jmeter-reporter' 366 | 367 | - name: Delete Grafana Reporter Service if set to false 368 | when: grafana_reporter_install == "false" 369 | k8s: 370 | state: absent 371 | api_version: v1 372 | kind: Service 373 | namespace: '{{ meta.namespace }}' 374 | name: '{{ meta.name }}-jmeter-reporter' -------------------------------------------------------------------------------- /roles/jmeter/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for jmeter -------------------------------------------------------------------------------- /start_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #Script created to launch Jmeter tests directly from the current terminal without accessing the jmeter master pod. 3 | #It requires that you supply the path to the jmx file 4 | #After execution, test script jmx file may be deleted from the pod itself but not locally. 5 | 6 | namespace="$1" 7 | 8 | [ -n "$namespace" ] || read -p 'Enter the Jmeter Namespace: ' namespace 9 | 10 | kubectl get namespace | grep $namespace >> /dev/null 11 | 12 | if [ $? != 0 ]; 13 | then 14 | echo "Namespace does not exist in the kubernetes cluster" 15 | echo "" 16 | echo "Below is the list of namespaces in the kubernetes cluster" 17 | 18 | kubectl get namespaces 19 | echo "" 20 | echo "Please check and try again" 21 | exit 22 | fi 23 | 24 | jmx="$1" 25 | [ -n "$jmx" ] || read -p 'Enter path to the jmx file ' jmx 26 | 27 | if [ ! -f "$jmx" ]; 28 | then 29 | echo "Test script file was not found in PATH" 30 | echo "Kindly check and input the correct file path" 31 | exit 32 | fi 33 | 34 | test_name="$(basename "$jmx")" 35 | 36 | #Get Master pod details 37 | 38 | master_pod=`kubectl -n $namespace get po | grep jmeter-master | awk '{print $1}'` 39 | 40 | kubectl -n $namespace cp "$jmx" "$master_pod:/$test_name" 41 | 42 | ## Echo Starting Jmeter load test 43 | 44 | kubectl -n $namespace exec -ti $master_pod -- /bin/bash /load_test "$test_name" -------------------------------------------------------------------------------- /watches.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - version: v1alpha1 3 | group: loadtest.jmeter.com 4 | kind: Jmeter 5 | role: /opt/ansible/roles/jmeter 6 | --------------------------------------------------------------------------------