├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-docs.yml │ ├── commit-message-check.yml │ ├── osv-scanner.yml │ └── pr-check.yml ├── .gitignore ├── .licensure.yml ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── api-testing-example.dsl ├── bulk-testing-example.dsl ├── config └── generated.go ├── dict ├── ip.txt ├── type.txt └── user.txt ├── docs ├── .gitignore ├── Makefile ├── README.md ├── config.yaml ├── content.en │ ├── _index.md │ ├── docs │ │ ├── getting-started │ │ │ ├── _index.md │ │ │ ├── benchmark.md │ │ │ └── install.md │ │ └── release-notes │ │ │ └── _index.md │ └── menu │ │ └── index.md ├── content.zh │ ├── _index.md │ ├── docs │ │ ├── getting-started │ │ │ ├── _index.md │ │ │ ├── benchmark.md │ │ │ └── install.md │ │ ├── release-notes │ │ │ └── _index.md │ │ └── resources │ │ │ └── _index.md │ └── menu │ │ └── index.md └── static │ └── img │ ├── logo-en.svg │ └── logo-zh.svg ├── domain.go ├── domain_test.go ├── loader.go ├── loadgen.dsl ├── loadgen.yml ├── main.go ├── plugins └── loadgen_dsl.wasm └── runner.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do 2 | 3 | ## Rationale for this change 4 | 5 | ## Standards checklist 6 | 7 | - [ ] The PR title is descriptive 8 | - [ ] The commit messages are [semantic](https://www.conventionalcommits.org/) 9 | - [ ] Necessary tests are added 10 | - [ ] Updated the release notes 11 | - [ ] Necessary documents have been added if this is a new feature 12 | - [ ] Performance tests checked, no obvious performance degradation -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'v*' 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | build-deploy-docs: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout Product Repo 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set Variables Based on Ref 22 | id: vars 23 | run: | 24 | PRODUCT_NAME=$(basename $(pwd)) # Get the directory name as the product name 25 | echo "PRODUCT_NAME=$PRODUCT_NAME" >> $GITHUB_ENV 26 | CURRENT_REF=${GITHUB_REF##*/} 27 | IS_SEMVER=false 28 | SEMVER_REGEX="^v([0-9]+)\.([0-9]+)\.([0-9]+)$" 29 | 30 | if [[ "${GITHUB_REF_TYPE}" == "branch" ]]; then 31 | if [[ "$CURRENT_REF" == "main" ]]; then 32 | echo "VERSION=main" >> $GITHUB_ENV 33 | echo "BRANCH=main" >> $GITHUB_ENV 34 | elif [[ "$CURRENT_REF" =~ $SEMVER_REGEX ]]; then 35 | IS_SEMVER=true 36 | echo "VERSION=$CURRENT_REF" >> $GITHUB_ENV 37 | echo "BRANCH=$CURRENT_REF" >> $GITHUB_ENV 38 | else 39 | echo "Branch '$CURRENT_REF' is not a valid semantic version. Skipping build." 40 | exit 0 41 | fi 42 | elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then 43 | if [[ "$CURRENT_REF" =~ $SEMVER_REGEX ]]; then 44 | IS_SEMVER=true 45 | echo "VERSION=$CURRENT_REF" >> $GITHUB_ENV 46 | echo "BRANCH=main" >> $GITHUB_ENV # Set BRANCH to 'main' for tags 47 | else 48 | echo "Tag '$CURRENT_REF' is not a valid semantic version. Skipping build." 49 | exit 0 50 | fi 51 | fi 52 | 53 | # Gather branches and tags, filter for semantic versions, sort, remove duplicates 54 | VERSIONS=$(git for-each-ref refs/remotes/origin refs/tags --format="%(refname:short)" | \ 55 | grep -E "v[0-9]+\.[0-9]+\.[0-9]+$" | awk -F'[v]' '{print "v"$2}' | sort -Vr | uniq | tr '\n' ',' | sed 's/,$//') 56 | echo "VERSIONS=main,$VERSIONS" >> $GITHUB_ENV 57 | 58 | - name: Install Hugo 59 | run: | 60 | wget https://github.com/gohugoio/hugo/releases/download/v0.79.1/hugo_extended_0.79.1_Linux-64bit.tar.gz 61 | tar -xzvf hugo_extended_0.79.1_Linux-64bit.tar.gz 62 | sudo mv hugo /usr/local/bin/ 63 | 64 | - name: Checkout Docs Repo 65 | uses: actions/checkout@v2 66 | with: 67 | repository: infinilabs/docs 68 | path: docs-output 69 | token: ${{ secrets.DOCS_DEPLOYMENT_TOKEN }} 70 | 71 | - name: Build Documentation 72 | run: | 73 | (cd docs && OUTPUT=$(pwd)/../docs-output make docs-build docs-place-redirect) 74 | 75 | - name: Commit and Push Changes to Docs Repo 76 | working-directory: docs-output 77 | run: | 78 | git config user.name "GitHub Actions" 79 | git config user.email "actions@github.com" 80 | 81 | if [[ -n $(git status --porcelain) ]]; then 82 | git add . 83 | git commit -m "Rebuild $PRODUCT_NAME docs for version $VERSION" 84 | git push origin main 85 | else 86 | echo "No changes to commit." 87 | fi 88 | 89 | - name: Rebuild Docs for Latest Version (main), if not already on main 90 | run: | 91 | # Only rebuild the main branch docs if the current ref is not "main" 92 | if [[ "$CURRENT_REF" != "main" ]]; then 93 | echo "Switching to main branch and rebuilding docs for 'latest'" 94 | 95 | # Checkout the main branch of the product repo to rebuild docs for "latest" 96 | git checkout main 97 | 98 | # Ensure the latest changes are pulled 99 | git pull origin main 100 | 101 | # Build Docs for Main Branch (latest) 102 | (cd docs && OUTPUT=$(pwd)/../docs-output VERSION="main" BRANCH="main" make docs-build docs-place-redirect) 103 | 104 | # Commit and Push Latest Docs to Main 105 | cd docs-output 106 | git config user.name "GitHub Actions" 107 | git config user.email "actions@github.com" 108 | 109 | if [[ -n $(git status --porcelain) ]]; then 110 | git add . 111 | git commit -m "Rebuild $PRODUCT_NAME docs for main branch with latest version" 112 | git push origin main 113 | else 114 | echo "No changes to commit for main." 115 | fi 116 | else 117 | echo "Current ref is 'main', skipping rebuild for 'latest'." 118 | fi 119 | working-directory: ./ # Working in the product repo 120 | -------------------------------------------------------------------------------- /.github/workflows/commit-message-check.yml: -------------------------------------------------------------------------------- 1 | name: 'commit-message-check' 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | check-commit-message: 7 | name: check-subject 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: check-subject-type 11 | uses: gsactions/commit-message-checker@v2 12 | with: 13 | checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request 14 | excludeDescription: 'true' # optional: this excludes the description body of a pull request 15 | accessToken: ${{ secrets.GITHUB_TOKEN }} 16 | pattern: '^(change:|feat:|improve:|perf:|dep:|docs:|test:|ci:|style:|refactor:|fix:|fixdoc:|fixup:|merge|Merge|bumpver:|chore:|build:) .+$' 17 | flags: 'gm' 18 | error: | 19 | Subject line has to contain a commit type, e.g.: "chore: blabla" or a merge commit e.g.: "merge xxx". 20 | Valid types are: 21 | change - API breaking change 22 | feat - API compatible new feature 23 | improve - Become better without functional changes 24 | perf - Performance improvement 25 | dep - dependency update 26 | docs - docs update 27 | test - test udpate 28 | ci - CI workflow update 29 | refactor - refactor without function change. 30 | fix - fix bug 31 | fixdoc - fix doc 32 | fixup - minor change: e.g., fix sth mentioned in a review. 33 | bumpver - Bump to a new version. 34 | chore - Nothing important. 35 | build - bot: dependabot. -------------------------------------------------------------------------------- /.github/workflows/osv-scanner.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities, 7 | # in addition to a PR check which fails if new vulnerabilities are introduced. 8 | # 9 | # For more examples and options, including how to ignore specific vulnerabilities, 10 | # see https://google.github.io/osv-scanner/github-action/ 11 | 12 | name: OSV-Scanner 13 | 14 | on: 15 | pull_request: 16 | branches: [ "main" ] 17 | 18 | permissions: 19 | # Required to upload SARIF file to CodeQL. See: https://github.com/github/codeql-action/issues/2117 20 | actions: read 21 | # Require writing security events to upload SARIF file to security tab 22 | security-events: write 23 | # Only need to read contents 24 | contents: read 25 | 26 | jobs: 27 | scan-pr: 28 | uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.9.1" 29 | with: 30 | # Example of specifying custom arguments 31 | scan-args: |- 32 | -r 33 | --skip-git 34 | ./ 35 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR-Check 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | env: 12 | GO_VERSION: 1.23.4 13 | PNAME: loadgen 14 | 15 | jobs: 16 | format_check: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout current repository 20 | uses: actions/checkout@v4 21 | with: 22 | path: ${{ env.PNAME }} 23 | 24 | - name: Checkout framework repository 25 | uses: actions/checkout@v4 26 | with: 27 | repository: infinilabs/framework 28 | path: framework 29 | 30 | - name: Checkout framework-vendor 31 | uses: actions/checkout@v4 32 | with: 33 | ref: main 34 | repository: infinilabs/framework-vendor 35 | path: vendor 36 | 37 | - name: Set up go toolchain 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version: ${{ env.GO_VERSION }} 41 | check-latest: false 42 | cache: true 43 | 44 | - name: Check go toolchain 45 | run: go version 46 | 47 | - name: Run make format 48 | shell: bash 49 | run: | 50 | echo Home path is $HOME 51 | export WORKBASE=$HOME/go/src/infini.sh 52 | export WORK=$WORKBASE/$PNAME 53 | 54 | # for test workspace 55 | mkdir -p $HOME/go/src/ 56 | ln -s $GITHUB_WORKSPACE $WORKBASE 57 | 58 | # check work folder 59 | ls -lrt $WORKBASE/ 60 | ls -alrt $WORK 61 | 62 | # for formatting code 63 | cd $WORK 64 | echo Formating code at $PWD ... 65 | make format 66 | 67 | - name: Check for changes after format 68 | id: check-changes 69 | shell: bash 70 | run: | 71 | export WORKBASE=$HOME/go/src/infini.sh 72 | export WORK=$WORKBASE/$PNAME 73 | 74 | # for foramt check 75 | cd $WORK 76 | if [[ $(git status --porcelain | grep -c " M .*\.go$") -gt 0 ]]; then 77 | echo "go format detected formatting changes" 78 | echo "changes=true" >> $GITHUB_OUTPUT 79 | else 80 | echo "go format no changes found" 81 | echo "changes=false" >> $GITHUB_OUTPUT 82 | fi 83 | 84 | - name: Fail workflow if changes after format 85 | if: steps.check-changes.outputs.changes == 'true' 86 | run: | 87 | export WORKBASE=$HOME/go/src/infini.sh 88 | export WORK=$WORKBASE/$PNAME 89 | 90 | # for foramt check 91 | cd $WORK && echo 92 | git status --porcelain | grep " M .*\.go$" 93 | echo "----------------------------------------------------------------------------------" 94 | echo "IMPORTANT: Above files are not formatted, please run 'make format' to format them." 95 | echo "----------------------------------------------------------------------------------" 96 | exit 1 97 | 98 | unit_test: 99 | runs-on: ubuntu-latest 100 | steps: 101 | - name: Checkout current repository 102 | uses: actions/checkout@v4 103 | with: 104 | path: ${{ env.PNAME }} 105 | 106 | - name: Checkout framework repository 107 | uses: actions/checkout@v4 108 | with: 109 | repository: infinilabs/framework 110 | path: framework 111 | 112 | - name: Checkout framework-vendor 113 | uses: actions/checkout@v4 114 | with: 115 | ref: main 116 | repository: infinilabs/framework-vendor 117 | path: vendor 118 | 119 | - name: Set up go toolchain 120 | uses: actions/setup-go@v5 121 | with: 122 | go-version: ${{ env.GO_VERSION }} 123 | check-latest: false 124 | cache: true 125 | 126 | - name: Check go toolchain 127 | run: go version 128 | 129 | - name: Unit test 130 | env: 131 | GOFLAGS: -tags=ci 132 | run: | 133 | echo Home path is $HOME 134 | export WORKBASE=$HOME/go/src/infini.sh 135 | export WORK=$WORKBASE/$PNAME 136 | 137 | # for test workspace 138 | mkdir -p $HOME/go/src/ 139 | ln -s $GITHUB_WORKSPACE $WORKBASE 140 | 141 | # check work folder 142 | ls -lrt $WORKBASE/ 143 | ls -alrt $WORK 144 | 145 | # for unit test 146 | cd $WORK 147 | echo Testing code at $PWD ... 148 | make test 149 | 150 | code_lint: 151 | runs-on: ubuntu-latest 152 | steps: 153 | - name: Checkout current repository 154 | uses: actions/checkout@v4 155 | with: 156 | path: ${{ env.PNAME }} 157 | 158 | - name: Checkout framework repository 159 | uses: actions/checkout@v4 160 | with: 161 | repository: infinilabs/framework 162 | path: framework 163 | 164 | - name: Checkout framework-vendor 165 | uses: actions/checkout@v4 166 | with: 167 | ref: main 168 | repository: infinilabs/framework-vendor 169 | path: vendor 170 | 171 | - name: Set up go toolchain 172 | uses: actions/setup-go@v5 173 | with: 174 | go-version: ${{ env.GO_VERSION }} 175 | check-latest: false 176 | cache: true 177 | 178 | - name: Check go toolchain 179 | run: go version 180 | 181 | - name: Code lint 182 | env: 183 | GOFLAGS: -tags=ci 184 | run: | 185 | echo Home path is $HOME 186 | export WORKBASE=$HOME/go/src/infini.sh 187 | export WORK=$WORKBASE/$PNAME 188 | 189 | # for test workspace 190 | mkdir -p $HOME/go/src/ 191 | ln -s $GITHUB_WORKSPACE $WORKBASE 192 | 193 | # check work folder 194 | ls -lrt $WORKBASE/ 195 | ls -alrt $WORK 196 | 197 | # for code lint 198 | cd $WORK 199 | echo Linting code at $PWD ... 200 | make lint -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.idea 3 | .idea 4 | out/* 5 | build/* 6 | gen/* 7 | */bin/* 8 | *.class 9 | *.iws 10 | *.ipr 11 | */out/** 12 | */build/** 13 | */*.iml 14 | */gen/* 15 | */bin/** 16 | */*.class 17 | *.iws 18 | *.ipr 19 | */R.java 20 | gen/ 21 | classes/ 22 | bin/ 23 | *.log 24 | */*/proguard_logs/** 25 | /private_test 26 | *.exe 27 | /log 28 | /out 29 | /data 30 | /bloomfilter.bin 31 | /bin 32 | /src/github.com 33 | /pkg 34 | /pkg/* 35 | /cluster 36 | .DS_Store 37 | /bin-run/ 38 | /leveldb 39 | /dist 40 | vendor 41 | .git 42 | trash 43 | *.so 44 | .public 45 | generated_*.go 46 | config/generated.go 47 | config/*.tpl 48 | config/*.yml 49 | -------------------------------------------------------------------------------- /.licensure.yml: -------------------------------------------------------------------------------- 1 | change_in_place: true 2 | 3 | # Regexes which if matched by a file path will always be excluded from 4 | # getting a license header 5 | excludes: 6 | - NOTICE 7 | - .gitmodules 8 | - Makefile 9 | - justfile 10 | - \.gitignore 11 | - .*lock 12 | - \.git/.* 13 | - \.licensure\.yml 14 | - README.* 15 | - LICENSE.* 16 | - .*\.(nix|toml|yml|md|rst|txt) 17 | - config/.* 18 | - lib/.* 19 | - script/.* 20 | - tests/.* 21 | - website/.* 22 | - data/.* 23 | - docs/.* 24 | - src/gen/.* 25 | - gen/.* 26 | - distribution/.* 27 | - contrib/.* 28 | - bin/.* 29 | - benches/.* 30 | - assets/.* 31 | - /config/.* 32 | - \..*/.* 33 | # Definition of the licenses used on this project and to what files 34 | # they should apply. 35 | # 36 | # No default license configuration is provided. This section must be 37 | # configured by the user. 38 | # 39 | # Make sure to delete the [] below when you add your configs. 40 | licenses: 41 | - files: any 42 | ident: AGPL-3.0-or-later 43 | authors: 44 | - name: INFINI Labs Team 45 | email: hello@infini.ltd 46 | auto_template: false 47 | template: | 48 | Copyright (C) INFINI Labs & INFINI LIMITED. 49 | 50 | The INFINI Loadgen is offered under the GNU Affero General Public License v3.0 51 | and as commercial software. 52 | 53 | For commercial licensing, contact us at: 54 | - Website: infinilabs.com 55 | - Email: hello@infini.ltd 56 | 57 | Open Source licensed under AGPL V3: 58 | This program is free software: you can redistribute it and/or modify 59 | it under the terms of the GNU Affero General Public License as published by 60 | the Free Software Foundation, either version 3 of the License, or 61 | (at your option) any later version. 62 | 63 | This program is distributed in the hope that it will be useful, 64 | but WITHOUT ANY WARRANTY; without even the implied warranty of 65 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 66 | GNU Affero General Public License for more details. 67 | 68 | You should have received a copy of the GNU Affero General Public License 69 | along with this program. If not, see . 70 | 71 | 72 | unwrap_text: false 73 | commenter: 74 | type: line 75 | comment_char: "//" 76 | trailing_lines: 1 77 | # Define type of comment characters to apply based on file extensions. 78 | comments: 79 | # The extensions (or singular extension) field defines which file 80 | # extensions to apply the commenter to. 81 | - extensions: 82 | - js 83 | - rs 84 | - go 85 | # The commenter field defines the kind of commenter to 86 | # generate. There are two types of commenters: line and block. 87 | # 88 | # This demonstrates a line commenter configuration. A line 89 | # commenter type will apply the comment_char to the beginning of 90 | # each line in the license header. It will then apply a number of 91 | # empty newlines to the end of the header equal to trailing_lines. 92 | # 93 | # If trailing_lines is omitted it is assumed to be 0. 94 | commenter: 95 | type: line 96 | comment_char: "//" 97 | trailing_lines: 1 98 | - extensions: 99 | - css 100 | - cpp 101 | - c 102 | # This demonstrates a block commenter configuration. A block 103 | # commenter type will add start_block_char as the first character 104 | # in the license header and add end_block_char as the last character 105 | # in the license header. If per_line_char is provided each line of 106 | # the header between the block start and end characters will be 107 | # line commented with the per_line_char 108 | # 109 | # trailing_lines works the same for both block and line commenter 110 | # types 111 | commenter: 112 | type: block 113 | start_block_char: "/*\n" 114 | end_block_char: "*/" 115 | per_line_char: "*" 116 | trailing_lines: 0 117 | # In this case extension is singular and a single string extension is provided. 118 | - extension: html 119 | commenter: 120 | type: block 121 | start_block_char: "" 123 | - extensions: 124 | - el 125 | - lisp 126 | commenter: 127 | type: line 128 | comment_char: ";;;" 129 | trailing_lines: 0 130 | # The extension string "any" is special and so will match any file 131 | # extensions. Commenter configurations are always checked in the 132 | # order they are defined, so if any is used it should be the last 133 | # commenter configuration or else it will override all others. 134 | # 135 | # In this configuration if we can't match the file extension we fall 136 | # back to the popular '#' line comment used in most scripting 137 | # languages. 138 | - extension: any 139 | commenter: 140 | type: line 141 | comment_char: '#' 142 | trailing_lines: 0 143 | 144 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | # APP info 4 | APP_NAME := loadgen 5 | APP_VERSION := 1.0.0_SNAPSHOT 6 | APP_CONFIG := $(APP_NAME).yml $(APP_NAME).dsl 7 | APP_EOLDate ?= "2025-12-31T10:10:10Z" 8 | APP_STATIC_FOLDER := .public 9 | APP_STATIC_PACKAGE := public 10 | APP_UI_FOLDER := ui 11 | APP_PLUGIN_FOLDER := proxy 12 | GOMODULE := false 13 | 14 | include ../framework/Makefile 15 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | ## Acknowledgements 3 | Loadgen original start with rewrite based on this project: 4 | [go-wrk - an HTTP benchmarking tool](https://github.com/tsliwowicz/go-wrk) Apache-2.0 license 5 | 6 | Apache License 7 | Version 2.0, January 2004 8 | http://www.apache.org/licenses/ 9 | 10 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 11 | 12 | 1. Definitions. 13 | 14 | "License" shall mean the terms and conditions for use, reproduction, 15 | and distribution as defined by Sections 1 through 9 of this document. 16 | 17 | "Licensor" shall mean the copyright owner or entity authorized by 18 | the copyright owner that is granting the License. 19 | 20 | "Legal Entity" shall mean the union of the acting entity and all 21 | other entities that control, are controlled by, or are under common 22 | control with that entity. For the purposes of this definition, 23 | "control" means (i) the power, direct or indirect, to cause the 24 | direction or management of such entity, whether by contract or 25 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 26 | outstanding shares, or (iii) beneficial ownership of such entity. 27 | 28 | "You" (or "Your") shall mean an individual or Legal Entity 29 | exercising permissions granted by this License. 30 | 31 | "Source" form shall mean the preferred form for making modifications, 32 | including but not limited to software source code, documentation 33 | source, and configuration files. 34 | 35 | "Object" form shall mean any form resulting from mechanical 36 | transformation or translation of a Source form, including but 37 | not limited to compiled object code, generated documentation, 38 | and conversions to other media types. 39 | 40 | "Work" shall mean the work of authorship, whether in Source or 41 | Object form, made available under the License, as indicated by a 42 | copyright notice that is included in or attached to the work 43 | (an example is provided in the Appendix below). 44 | 45 | "Derivative Works" shall mean any work, whether in Source or Object 46 | form, that is based on (or derived from) the Work and for which the 47 | editorial revisions, annotations, elaborations, or other modifications 48 | represent, as a whole, an original work of authorship. For the purposes 49 | of this License, Derivative Works shall not include works that remain 50 | separable from, or merely link (or bind by name) to the interfaces of, 51 | the Work and Derivative Works thereof. 52 | 53 | "Contribution" shall mean any work of authorship, including 54 | the original version of the Work and any modifications or additions 55 | to that Work or Derivative Works thereof, that is intentionally 56 | submitted to Licensor for inclusion in the Work by the copyright owner 57 | or by an individual or Legal Entity authorized to submit on behalf of 58 | the copyright owner. For the purposes of this definition, "submitted" 59 | means any form of electronic, verbal, or written communication sent 60 | to the Licensor or its representatives, including but not limited to 61 | communication on electronic mailing lists, source code control systems, 62 | and issue tracking systems that are managed by, or on behalf of, the 63 | Licensor for the purpose of discussing and improving the Work, but 64 | excluding communication that is conspicuously marked or otherwise 65 | designated in writing by the copyright owner as "Not a Contribution." 66 | 67 | "Contributor" shall mean Licensor and any individual or Legal Entity 68 | on behalf of whom a Contribution has been received by Licensor and 69 | subsequently incorporated within the Work. 70 | 71 | 2. Grant of Copyright License. Subject to the terms and conditions of 72 | this License, each Contributor hereby grants to You a perpetual, 73 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 74 | copyright license to reproduce, prepare Derivative Works of, 75 | publicly display, publicly perform, sublicense, and distribute the 76 | Work and such Derivative Works in Source or Object form. 77 | 78 | 3. Grant of Patent License. Subject to the terms and conditions of 79 | this License, each Contributor hereby grants to You a perpetual, 80 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 81 | (except as stated in this section) patent license to make, have made, 82 | use, offer to sell, sell, import, and otherwise transfer the Work, 83 | where such license applies only to those patent claims licensable 84 | by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) 86 | with the Work to which such Contribution(s) was submitted. If You 87 | institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work 89 | or a Contribution incorporated within the Work constitutes direct 90 | or contributory patent infringement, then any patent licenses 91 | granted to You under this License for that Work shall terminate 92 | as of the date such litigation is filed. 93 | 94 | 4. Redistribution. You may reproduce and distribute copies of the 95 | Work or Derivative Works thereof in any medium, with or without 96 | modifications, and in Source or Object form, provided that You 97 | meet the following conditions: 98 | 99 | (a) You must give any other recipients of the Work or 100 | Derivative Works a copy of this License; and 101 | 102 | (b) You must cause any modified files to carry prominent notices 103 | stating that You changed the files; and 104 | 105 | (c) You must retain, in the Source form of any Derivative Works 106 | that You distribute, all copyright, patent, trademark, and 107 | attribution notices from the Source form of the Work, 108 | excluding those notices that do not pertain to any part of 109 | the Derivative Works; and 110 | 111 | (d) If the Work includes a "NOTICE" text file as part of its 112 | distribution, then any Derivative Works that You distribute must 113 | include a readable copy of the attribution notices contained 114 | within such NOTICE file, excluding those notices that do not 115 | pertain to any part of the Derivative Works, in at least one 116 | of the following places: within a NOTICE text file distributed 117 | as part of the Derivative Works; within the Source form or 118 | documentation, if provided along with the Derivative Works; or, 119 | within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents 121 | of the NOTICE file are for informational purposes only and 122 | do not modify the License. You may add Your own attribution 123 | notices within Derivative Works that You distribute, alongside 124 | or as an addendum to the NOTICE text from the Work, provided 125 | that such additional attribution notices cannot be construed 126 | as modifying the License. 127 | 128 | You may add Your own copyright statement to Your modifications and 129 | may provide additional or different license terms and conditions 130 | for use, reproduction, or distribution of Your modifications, or 131 | for any such Derivative Works as a whole, provided Your use, 132 | reproduction, and distribution of the Work otherwise complies with 133 | the conditions stated in this License. 134 | 135 | 5. Submission of Contributions. Unless You explicitly state otherwise, 136 | any Contribution intentionally submitted for inclusion in the Work 137 | by You to the Licensor shall be under the terms and conditions of 138 | this License, without any additional terms or conditions. 139 | Notwithstanding the above, nothing herein shall supersede or modify 140 | the terms of any separate license agreement you may have executed 141 | with Licensor regarding such Contributions. 142 | 143 | 6. Trademarks. This License does not grant permission to use the trade 144 | names, trademarks, service marks, or product names of the Licensor, 145 | except as required for reasonable and customary use in describing the 146 | origin of the Work and reproducing the content of the NOTICE file. 147 | 148 | 7. Disclaimer of Warranty. Unless required by applicable law or 149 | agreed to in writing, Licensor provides the Work (and each 150 | Contributor provides its Contributions) on an "AS IS" BASIS, 151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 152 | implied, including, without limitation, any warranties or conditions 153 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 154 | PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any 156 | risks associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. In no event and under no legal theory, 159 | whether in tort (including negligence), contract, or otherwise, 160 | unless required by applicable law (such as deliberate and grossly 161 | negligent acts) or agreed to in writing, shall any Contributor be 162 | liable to You for damages, including any direct, indirect, special, 163 | incidental, or consequential damages of any character arising as a 164 | result of this License or out of the use or inability to use the 165 | Work (including but not limited to damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, or any and all 167 | other commercial damages or losses), even if such Contributor 168 | has been advised of the possibility of such damages. 169 | 170 | 9. Accepting Warranty or Additional Liability. While redistributing 171 | the Work or Derivative Works thereof, You may choose to offer, 172 | and charge a fee for, acceptance of support, warranty, indemnity, 173 | or other liability obligations and/or rights consistent with this 174 | License. However, in accepting such obligations, You may act only 175 | on Your own behalf and on Your sole responsibility, not on behalf 176 | of any other Contributor, and only if You agree to indemnify, 177 | defend, and hold each Contributor harmless for any liability 178 | incurred by, or claims asserted against, such Contributor by reason 179 | of your accepting any such warranty or additional liability. 180 | 181 | END OF TERMS AND CONDITIONS 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # INFINI Loadgen 3 | 4 | 5 | Highlights of Loadgen: 6 | 7 | - Robust performance 8 | - Lightweight and dependency-free 9 | - Random selection of template-based parameters 10 | - High concurrency 11 | - Balanced traffic control at the benchmark end 12 | - Validate server responses. 13 | 14 | Install with script: 15 | 16 | ``` 17 | curl -sSL http://get.infini.cloud | bash -s -- -p loadgen 18 | ``` 19 | 20 | > Or download from here: [http://release.infinilabs.com/loadgen/](http://release.infinilabs.com/loadgen/) 21 | 22 | ``` 23 | ➜ /tmp mkdir loadgen 24 | ➜ /tmp curl -sSL http://get.infini.cloud | bash -s -- -p loadgen -d /tmp/loadgen 25 | 26 | @@@@@@@@@@@ 27 | @@@@@@@@@@@@ 28 | @@@@@@@@@@@@ 29 | @@@@@@@@@&@@@ 30 | #@@@@@@@@@@@@@ 31 | @@@ @@@@@@@@@@@@@ 32 | &@@@@@@@ &@@@@@@@@@@@@@ 33 | @&@@@@@@@&@ @@@&@@@@@@@&@ 34 | @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ 35 | @@@@@@@@@@@@@@@@@@& @@@@@@@@@@@@@ 36 | %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 37 | @@@@@@@@@@@@&@@@@@@@@@@@@@@@ 38 | @@ ,@@@@@@@@@@@@@@@@@@@@@@@& 39 | @@@@@. @@@@@&@@@@@@@@@@@@@@ 40 | @@@@@@@@@@ @@@@@@@@@@@@@@@# 41 | @&@@@&@@@&@@@ &@&@@@&@@@&@ 42 | @@@@@@@@@@@@@. @@@@@@@* 43 | @@@@@@@@@@@@@ %@@@ 44 | @@@@@@@@@@@@@ 45 | /@@@@@@@&@@@@@ 46 | @@@@@@@@@@@@@ 47 | @@@@@@@@@@@@@ 48 | @@@@@@@@@@@@ Welcome to INFINI Labs! 49 | 50 | 51 | Now attempting the installation... 52 | 53 | Name: [loadgen], Version: [1.26.1-598], Path: [/tmp/loadgen] 54 | File: [https://release.infinilabs.com/loadgen/stable/loadgen-1.26.1-598-mac-arm64.zip] 55 | ##=O#- # 56 | 57 | Installation complete. [loadgen] is ready to use! 58 | 59 | 60 | ---------------------------------------------------------------- 61 | cd /tmp/loadgen && ./loadgen-mac-arm64 62 | ---------------------------------------------------------------- 63 | 64 | 65 | __ _ __ ____ __ _ __ __ 66 | / // |/ // __// // |/ // / 67 | / // || // _/ / // || // / 68 | /_//_/|_//_/ /_//_/|_//_/ 69 | 70 | ©INFINI.LTD, All Rights Reserved. 71 | ``` 72 | 73 | ## Loadgen 74 | 75 | Loadgen is easy to use. After the tool is downloaded and decompressed, two files are obtained: one executable program and one configuration file `loadgen.yml`. An example of the configuration file is as follows: 76 | 77 | ``` 78 | env: 79 | ES_USERNAME: elastic 80 | ES_PASSWORD: elastic 81 | runner: 82 | # total_rounds: 1 83 | no_warm: false 84 | log_requests: false 85 | assert_invalid: false 86 | assert_error: false 87 | variables: 88 | - name: ip 89 | type: file 90 | path: test/ip.txt 91 | - name: user 92 | type: file 93 | path: test/user.txt 94 | - name: id 95 | type: sequence 96 | - name: uuid 97 | type: uuid 98 | - name: now_local 99 | type: now_local 100 | - name: now_utc 101 | type: now_utc 102 | - name: now_unix 103 | type: now_unix 104 | requests: 105 | - request: 106 | method: GET 107 | basic_auth: 108 | username: $[[env.ES_USERNAME]] 109 | password: $[[env.ES_PASSWORD]] 110 | url: http://localhost:8000/medcl/_search 111 | body: '{ "query": {"match": { "name": "$[[user]]" }}}' 112 | ``` 113 | 114 | ### Runner Configurations 115 | 116 | By default, `loadgen` will run under the benchmarking mode, repeating through all the `requests` during the specified duration (`-d`). If you only need to test the responses, setting `runner.total_rounds: 1` will let `loadgen` run for only once. 117 | 118 | ### HTTP Headers Canonization 119 | 120 | By default, `loadgen` will canonilize the HTTP response header keys received from the server side (`user-agent: xxx` -> `User-Agent: xxx`). If you need to assert the header keys exactly, you can set `runner.disable_header_names_normalizing: true` to disable this behavior. 121 | 122 | ## Usage of Variables 123 | 124 | In the above configuration, `variables` is used to define variable parameters and variables are identified by `name`. In a constructed request, `$[[Variable name]]` can be used to access the value of the variable. Supported variable types are as follows: 125 | 126 | | Type | Description | Parameters | 127 | | ----------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 128 | | `file` | Load variables from file | `path`: the path of the data files
`data`: a list of values, will get appended to the end of the data specified by `path` file | 129 | | `list` | Defined variables inline | use `data` to define a string array | 130 | | `sequence` | 32-bit Variable of the auto incremental numeric type | `from`: the minimum of the values
`to`: the maximum of the values | 131 | | `sequence64` | 64-bit Variable of the auto incremental numeric type | `from`: the minimum of the values
`to`: the maximum of the values | 132 | | `range` | Variable of the range numbers, support parameters `from` and `to` to define the range | `from`: the minimum of the values
`to`: the maximum of the values | 133 | | `random_array` | Generate a random array from the variable specified by `variable_key` | `variable_key`: the variable name for the source of array values
`size`: the size of array
`square_bracket`: `true/false`, whether to add `[]` for the outputed array
`string_bracket`: the string to surround the outputed elements. | 134 | | `uuid` | Variable of the UUID character type | | 135 | | `now_local` | Current time and local time zone | | 136 | | `now_utc` | Current time and UTC time zone | | 137 | | `now_unix` | Current time and Unix timestamp | | 138 | | `now_with_format` | Current time,support parameter `format` to customize the output format, eg: `2006-01-02T15:04:05-0700` | `format`: the format of the time output ([Example](https://www.geeksforgeeks.org/time-formatting-in-golang/)) | 139 | 140 | ### Examples 141 | 142 | Variable parameters of the `file` type are loaded from an external text file. One variable parameter occupies one line. When one variable of the file type is accessed, one variable value is taken randomly. An example of the variable format is as follows: 143 | 144 | ``` 145 | ➜ loadgen git:(master) ✗ cat test/user.txt 146 | medcl 147 | elastic 148 | ``` 149 | 150 | Tips about how to generate a random string of fixed length, such as 1024 per line: 151 | 152 | ``` 153 | LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\&\*\(\)-+= < /dev/random | head -c 1024 >> 1k.txt 154 | ``` 155 | 156 | ### Environment Variables 157 | 158 | `loadgen` supporting loading and using environment variables in `loadgen.yml`, you can specify the default values in `env` configuration. `loadgen` will overwrite the variables at runtime if they're also specified by the command-line environment. 159 | 160 | The environment variables can be access by `$[[env.ENV_KEY]]`: 161 | 162 | ``` 163 | # Default values for the environment variables. 164 | env: 165 | ES_USERNAME: elastic 166 | ES_PASSWORD: elastic 167 | ES_ENDPOINT: http://localhost:8000 168 | requests: 169 | - request: 170 | method: GET 171 | basic_auth: 172 | username: $[[env.ES_USERNAME]] # Use environment variables 173 | password: $[[env.ES_PASSWORD]] # Use environment variables 174 | url: $[[env.ES_ENDPOINT]]/medcl/_search # Use environment variables 175 | body: '{ "query": {"match": { "name": "$[[user]]" }}}' 176 | ``` 177 | 178 | ## Request Definition 179 | 180 | The `requests` node is used to set requests to be executed by Loadgen in sequence. Loadgen supports fixed-parameter requests and requests constructed using template-based variable parameters. The following is an example of a common query request. 181 | 182 | ``` 183 | requests: 184 | - request: 185 | method: GET 186 | basic_auth: 187 | username: elastic 188 | password: pass 189 | url: http://localhost:8000/medcl/_search?q=name:$[[user]] 190 | ``` 191 | 192 | In the above query, Loadgen conducts queries based on the `medcl` index and executes one query based on the `name` field. The value of each request is from the random variable `user`. 193 | 194 | ### Simulating Bulk Ingestion 195 | 196 | It is very easy to use Loadgen to simulate bulk ingestion. Configure one index operation in the request body and then use the `body_repeat_times` parameter to randomly replicate several parameterized requests to complete the preparation of a batch of requests. See the following example. 197 | 198 | ``` 199 | - request: 200 | method: POST 201 | basic_auth: 202 | username: test 203 | password: testtest 204 | url: http://localhost:8000/_bulk 205 | body_repeat_times: 1000 206 | body: | 207 | { "index" : { "_index" : "medcl-y4","_type":"doc", "_id" : "$[[uuid]]" } } 208 | { "id" : "$[[id]]","field1" : "$[[user]]","ip" : "$[[ip]]","now_local" : "$[[now_local]]","now_unix" : "$[[now_unix]]" } 209 | ``` 210 | 211 | ### Response Assertions 212 | 213 | You can use the `assert` configuration to check the response values. `assert` now supports most of all the [condition checkers](https://docs.infinilabs.com/gateway/main/docs/references/flow/#condition-type) of INFINI Gateway. 214 | 215 | ``` 216 | requests: 217 | - request: 218 | method: GET 219 | basic_auth: 220 | username: elastic 221 | password: pass 222 | url: http://localhost:8000/medcl/_search?q=name:$[[user]] 223 | assert: 224 | equals: 225 | _ctx.response.status: 201 226 | ``` 227 | 228 | The response value can be accessed from the `_ctx` value, currently it contains these values: 229 | 230 | | Parameter | Description | 231 | | ------------------------- | ----------------------------------------------------------------------------------------------- | 232 | | `_ctx.response.status` | HTTP response status code | 233 | | `_ctx.response.header` | HTTP response headers | 234 | | `_ctx.response.body` | HTTP response body text | 235 | | `_ctx.response.body_json` | If the HTTP response body is a valid JSON string, you can access the JSON fields by `body_json` | 236 | | `_ctx.elapsed` | The time elapsed since request sent to the server (milliseconds) | 237 | 238 | If the request failed (e.g. the host is not reachable), `loadgen` will record it under `Number of Errors` as part of the testing output. If you configured `runner.assert_error: true`, `loadgen` will exit as `exit(2)` when there're any requests failed. 239 | 240 | If the assertion failed, `loadgen` will record it under `Number of Invalid` as part of the testing output and skip the subsequent requests in this round. If you configured `runner.assert_invalid: true`, `loadgen` will exit as `exit(1)` when there're any assertions failed. 241 | 242 | ### Dynamic Variable Registration 243 | 244 | Each request can use `register` to dynamically set the variables based on the response value, a common usage is to update the parameters of the later requests based on the previous responses. 245 | 246 | In the below example, we're registering the response value `_ctx.response.body_json.test.settings.index.uuid` of the `$[[env.ES_ENDPOINT]]/test` to the `index_id` variable, then we can access it by `$[[index_id]]`. 247 | 248 | ``` 249 | requests: 250 | - request: 251 | method: GET 252 | url: $[[env.ES_ENDPOINT]]/test 253 | assert: 254 | equals: 255 | _ctx.response.status: 200 256 | register: 257 | - index_id: _ctx.response.body_json.test.settings.index.uuid 258 | ``` 259 | 260 | ### Benchmark Test 261 | 262 | Run Loadgen to perform the benchmark test as follows: 263 | 264 | ``` 265 | ➜ loadgen git:(master) ✗ ./bin/loadgen -d 30 -c 100 -compress 266 | __ ___ _ ___ ___ __ __ 267 | / / /___\/_\ / \/ _ \ /__\/\ \ \ 268 | / / // ///_\\ / /\ / /_\//_\ / \/ / 269 | / /__/ \_// _ \/ /_// /_\\//__/ /\ / 270 | \____|___/\_/ \_/___,'\____/\__/\_\ \/ 271 | 272 | [LOADGEN] A http load generator and testing suit. 273 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files 274 | [07-19 16:15:00] [INF] [instance.go:24] workspace: data/loadgen/nodes/0 275 | [07-19 16:15:00] [INF] [loader.go:312] warmup started 276 | [07-19 16:15:00] [INF] [app.go:306] loadgen now started. 277 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search 278 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}} 279 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search?q=name:medcl 280 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}} 281 | [07-19 16:15:01] [INF] [loader.go:316] [POST] http://localhost:8000/_bulk 282 | [07-19 16:15:01] [INF] [loader.go:317] status: 200,,{"took":120,"errors":false,"items":[{"index":{"_index":"medcl-y4","_type":"doc","_id":"c3qj9123r0okahraiej0","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":5735852,"_primary_term":3,"status":201}}]} 283 | [07-19 16:15:01] [INF] [loader.go:325] warmup finished 284 | 285 | 209 requests finished in 10.031365126s, 0.00bytes sent, 32.86KB received 286 | 287 | [Loadgen Client Metrics] 288 | Requests/sec: 20.82 289 | Request Traffic/sec: 0.00bytes 290 | Total Transfer/sec: 3.27KB 291 | Fastest Request: 1ms 292 | Slowest Request: 182.437792ms 293 | Status 302: 209 294 | 295 | [Latency Metrics] 296 | 209 samples of 209 events 297 | Cumulative: 10.031365126s 298 | HMean: 46.31664ms 299 | Avg.: 47.996962ms 300 | p50: 45.712292ms 301 | p75: 51.6065ms 302 | p95: 53.05475ms 303 | p99: 118.162416ms 304 | p999: 182.437792ms 305 | Long 5%: 87.678145ms 306 | Short 5%: 39.11217ms 307 | Max: 182.437792ms 308 | Min: 38.257791ms 309 | Range: 144.180001ms 310 | StdDev: 14.407579ms 311 | Rate/sec.: 20.82 312 | 313 | [Latency Distribution] 314 | 38.257ms - 52.675ms ------------------------------ 315 | 52.675ms - 67.093ms -- 316 | 67.093ms - 81.511ms - 317 | 81.511ms - 95.929ms - 318 | 95.929ms - 110.347ms - 319 | 110.347ms - 124.765ms - 320 | 321 | 322 | [Estimated Server Metrics] 323 | Requests/sec: 20.83 324 | Avg Req Time: 47.996962ms 325 | Transfer/sec: 3.28KB 326 | ``` 327 | 328 | Loadgen executes all requests once to warm up before the formal benchmark test. If an error occurs, a prompt is displayed, asking you whether to continue. 329 | The warm-up request results are also output to the terminal. After execution, an execution summary is output. 330 | You can set `runner.no_warm: true` to skip the warm-up stage. 331 | 332 | > The final results of Loadgen are the cumulative statistics after all requests are executed, and they may be inaccurate. You are advised to start the Kibana dashboard to check all operating indicators of Elasticsearch in real time. 333 | 334 | ### CLI Parameters 335 | 336 | Loadgen cyclically executes requests defined in the configuration file. By default, Loadgen runs for `5s` and then automatically exits. If you want to prolong the running time or increase the concurrency, you can set the tool's startup parameters. The help commands are as follows: 337 | 338 | ``` 339 | ➜ loadgen git:(master) ✗ ./bin/loadgen --help 340 | Usage of ./bin/loadgen: 341 | -c int 342 | Number of concurrent threads (default 1) 343 | -compress 344 | Compress requests with gzip 345 | -config string 346 | the location of config file, default: loadgen.yml (default "loadgen.yml") 347 | -d int 348 | Duration of tests in seconds (default 5) 349 | -debug 350 | run in debug mode, loadgen will quit with panic error 351 | -l int 352 | Limit total requests (default -1) 353 | -log string 354 | the log level,options:trace,debug,info,warn,error (default "info") 355 | -r int 356 | Max requests per second (fixed QPS) (default -1) 357 | -v version 358 | ``` 359 | 360 | ### Limiting the Client Workload 361 | 362 | You can use Loadgen and set the CLI parameter `-r` to restrict the number of requests that can be sent by the client per second, so as to evaluate the response time and load of Elasticsearch under fixed pressure. See the following example. 363 | 364 | ``` 365 | ➜ loadgen git:(master) ✗ ./bin/loadgen -d 30 -c 100 -r 100 366 | ``` 367 | 368 | > Note: The client throughput limit may not be accurate enough in the case of massive concurrencies. 369 | 370 | ### Limiting the Total Number of Requests 371 | 372 | You can set the `-l` parameter to control the total number of requests that can be sent by the client, so as to generate a fixed number of documents. Modify the configuration as follows: 373 | 374 | ``` 375 | requests: 376 | - request: 377 | method: POST 378 | basic_auth: 379 | username: test 380 | password: testtest 381 | url: http://localhost:8000/medcl-test/doc2/_bulk 382 | body_repeat_times: 1 383 | body: | 384 | { "index" : { "_index" : "medcl-test", "_id" : "$[[uuid]]" } } 385 | { "id" : "$[[id]]","field1" : "$[[user]]","ip" : "$[[ip]]" } 386 | ``` 387 | 388 | Configured parameters use the content of only one document for each request. Then, the system executes Loadgen. 389 | 390 | ``` 391 | ./bin/loadgen -config loadgen-gw.yml -d 600 -c 100 -l 50000 392 | ``` 393 | 394 | After execution, `50000` records are added for the Elasticsearch index `medcl-test`. 395 | 396 | ### Using Auto Incremental IDs to Ensure the Document Sequence 397 | 398 | If the IDs of generated documents need to increase regularly to facilitate comparison, you can use the auto incremental IDs of the `sequence` type as the primary key and avoid using random numbers in the content. See the following example. 399 | 400 | ``` 401 | requests: 402 | - request: 403 | method: POST 404 | basic_auth: 405 | username: test 406 | password: testtest 407 | url: http://localhost:8000/medcl-test/doc2/_bulk 408 | body_repeat_times: 1 409 | body: | 410 | { "index" : { "_index" : "medcl-test", "_id" : "$[[id]]" } } 411 | { "id" : "$[[id]]" } 412 | ``` 413 | 414 | ### Reuse variables in Request Context 415 | 416 | In a request, we might want use the same variable value, such as the `routing` parameter to control the shard destination, also store the field in the JSON document. 417 | You can use `runtime_variables` to set request-level variables, or `runtime_body_line_variables` to define request-body-level variables. 418 | If the request body set `body_repeat_times`, each line will be different, as shown in the following example: 419 | 420 | ``` 421 | variables: 422 | - name: id 423 | type: sequence 424 | - name: uuid 425 | type: uuid 426 | - name: now_local 427 | type: now_local 428 | - name: now_utc 429 | type: now_utc 430 | - name: now_unix 431 | type: now_unix 432 | - name: suffix 433 | type: range 434 | from: 10 435 | to: 15 436 | requests: 437 | - request: 438 | method: POST 439 | runtime_variables: 440 | batch_no: id 441 | runtime_body_line_variables: 442 | routing_no: uuid 443 | basic_auth: 444 | username: ingest 445 | password: password 446 | #url: http://localhost:8000/_search?q=$[[id]] 447 | url: http://192.168.3.188:9206/_bulk 448 | body_repeat_times: 10 449 | body: | 450 | { "create" : { "_index" : "test-$[[suffix]]","_type":"doc", "_id" : "$[[uuid]]" , "routing" : "$[[routing_no]]" } } 451 | { "id" : "$[[uuid]]","routing_no" : "$[[routing_no]]","batch_number" : "$[[batch_no]]", "random_no" : "$[[suffix]]","ip" : "$[[ip]]","now_local" : "$[[now_local]]","now_unix" : "$[[now_unix]]" } 452 | ``` 453 | 454 | We defined the `batch_no` variable to represent the same batch number in a batch of documents, and the `routing_no` variable to represent the routing value at each document level. 455 | 456 | ### Customize Header 457 | 458 | ``` 459 | requests: 460 | - request: 461 | method: GET 462 | url: http://localhost:8000/test/_search 463 | headers: 464 | - Agent: "Loadgen-1" 465 | disable_header_names_normalizing: false 466 | ``` 467 | 468 | By default, `loadgen` will canonilize the HTTP header keys before sending the request (`user-agent: xxx` -> `User-Agent: xxx`), if you need to set the header keys exactly as is, set `disable_header_names_normalizing: true`. 469 | 470 | ### Work with DSL 471 | 472 | Loadgen also support simply the requests called DSL,for example, prepare a dsl file for loadgen, save as `bulk.dsl`: 473 | 474 | ``` 475 | POST /_bulk 476 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}} 477 | {"id": "$[[id]]", "routing": "$[[routing_no]]", "batch": "$[[batch_no]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 478 | ``` 479 | And specify the dsl file with parameter `run`: 480 | 481 | ``` 482 | $ INDEX_NAME=medcl123 ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run bulk.dsl 483 | ``` 484 | 485 | Now you should ready to rock~ -------------------------------------------------------------------------------- /api-testing-example.dsl: -------------------------------------------------------------------------------- 1 | # // How to use this example? 2 | # // $ INDEX_NAME=medcl123 ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run api-testing-example.dsl 3 | 4 | # runner: { 5 | # total_rounds: 1, 6 | # no_warm: true, 7 | # assert_invalid: true, 8 | # continue_on_assert_invalid: true, 9 | # } 10 | 11 | DELETE /$[[env.INDEX_NAME]] 12 | 13 | PUT /$[[env.INDEX_NAME]] 14 | # 200 15 | # {"acknowledged":true,"shards_acknowledged":true,"index":"medcl123"} 16 | 17 | POST /_bulk 18 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}} 19 | {"id": "$[[id]]", "field1": "$[[list]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 20 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}} 21 | {"id": "$[[id]]", "field1": "$[[list]]", "some_other_fields": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 22 | # 200 23 | # {"errors":false,} 24 | 25 | GET /$[[env.INDEX_NAME]]/_refresh 26 | # 200 27 | # {"_shards":{"total":2,"successful":1,"failed":0}} 28 | 29 | GET /$[[env.INDEX_NAME]]/_count 30 | # 200 31 | # {"count":2} 32 | 33 | GET /$[[env.INDEX_NAME]]/_search 34 | # 200 35 | -------------------------------------------------------------------------------- /bulk-testing-example.dsl: -------------------------------------------------------------------------------- 1 | # // How to use this example? 2 | # // $ INDEX_NAME=medcl123 ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run bulk.dsl 3 | 4 | # runner: { 5 | # total_rounds: 100000, 6 | # no_warm: true, 7 | # assert_invalid: true, 8 | # continue_on_assert_invalid: true, 9 | # } 10 | 11 | 12 | POST /_bulk 13 | {"index": {"_index": "$[[env.INDEX_NAME]]", "_type": "_doc", "_id": "$[[uuid]]"}} 14 | {"id": "$[[id]]", "routing": "$[[routing_no]]", "batch": "$[[batch_no]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 15 | # request: { 16 | # runtime_variables: {batch_no: "uuid"}, 17 | # runtime_body_line_variables: {routing_no: "uuid"}, 18 | # body_repeat_times: 1000, 19 | # basic_auth: { 20 | # username: "$[[env.ES_USERNAME]]", 21 | # password: "$[[env.ES_PASSWORD]]", 22 | # }, 23 | # } -------------------------------------------------------------------------------- /config/generated.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const LastCommitLog = "N/A" 4 | 5 | const BuildDate = "N/A" 6 | 7 | const EOLDate = "N/A" 8 | 9 | const Version = "0.0.1-SNAPSHOT" 10 | 11 | const BuildNumber = "001" 12 | -------------------------------------------------------------------------------- /dict/ip.txt: -------------------------------------------------------------------------------- 1 | 192.168.0.1 2 | 192.168.0.2 3 | 192.168.0.3 4 | 192.168.0.4 5 | -------------------------------------------------------------------------------- /dict/type.txt: -------------------------------------------------------------------------------- 1 | doc 2 | _doc 3 | -------------------------------------------------------------------------------- /dict/user.txt: -------------------------------------------------------------------------------- 1 | medcl 2 | elastic 3 | "google" abc 4 | \"abcd" 5 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /public/ 2 | /resources/ 3 | /themes/ 4 | /config.bak -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | # Basic info 4 | PRODUCT?= $(shell basename "$(shell cd .. && pwd)") 5 | BRANCH?= main 6 | VERSION?= $(shell [[ "$(BRANCH)" == "main" ]] && echo "main" || echo "$(BRANCH)") 7 | CURRENT_VERSION?= $(VERSION) 8 | VERSIONS?= "main" 9 | OUTPUT?= "/tmp/docs" 10 | THEME_FOLDER?= "themes/book" 11 | THEME_REPO?= "https://github.com/infinilabs/docs-theme.git" 12 | THEME_BRANCH?= "main" 13 | 14 | .PHONY: docs-build 15 | 16 | default: docs-build 17 | 18 | docs-init: 19 | @if [ ! -d $(THEME_FOLDER) ]; then echo "theme does not exist";(git clone -b $(THEME_BRANCH) $(THEME_REPO) $(THEME_FOLDER) ) fi 20 | 21 | docs-env: 22 | @echo "Debugging Variables:" 23 | @echo "PRODUCT: $(PRODUCT)" 24 | @echo "BRANCH: $(BRANCH)" 25 | @echo "VERSION: $(VERSION)" 26 | @echo "CURRENT_VERSION: $(CURRENT_VERSION)" 27 | @echo "VERSIONS: $(VERSIONS)" 28 | @echo "OUTPUT: $(OUTPUT)" 29 | 30 | docs-config: docs-init 31 | cp config.yaml config.bak 32 | # Detect OS and apply the appropriate sed command 33 | @if [ "$$(uname)" = "Darwin" ]; then \ 34 | echo "Running on macOS"; \ 35 | sed -i '' "s/BRANCH/$(VERSION)/g" config.yaml; \ 36 | else \ 37 | echo "Running on Linux"; \ 38 | sed -i 's/BRANCH/$(VERSION)/g' config.yaml; \ 39 | fi 40 | 41 | docs-build: docs-config 42 | hugo --minify --theme book --destination="$(OUTPUT)/$(PRODUCT)/$(VERSION)" \ 43 | --baseURL="/$(PRODUCT)/$(VERSION)" 44 | @$(MAKE) docs-restore-generated-file 45 | 46 | docs-serve: docs-config 47 | hugo serve 48 | @$(MAKE) docs-restore-generated-file 49 | 50 | docs-place-redirect: 51 | echo "

REDIRECT TO THE LATEST_VERSION.

" > $(OUTPUT)/$(PRODUCT)/index.html 52 | 53 | docs-restore-generated-file: 54 | mv config.bak config.yaml -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/config.yaml: -------------------------------------------------------------------------------- 1 | # VERSIONS=latest,v1.0 hugo --minify --baseURL="/product/v1.0/" -d public/product/v1.0 2 | 3 | title: INFINI Loadgen 4 | theme: book 5 | 6 | # Book configuration 7 | disablePathToLower: true 8 | enableGitInfo: false 9 | 10 | outputs: 11 | home: 12 | - HTML 13 | - RSS 14 | - JSON 15 | 16 | # Needed for mermaid/katex shortcodes 17 | markup: 18 | goldmark: 19 | renderer: 20 | unsafe: true 21 | tableOfContents: 22 | startLevel: 1 23 | 24 | # Multi-lingual mode config 25 | # There are different options to translate files 26 | # See https://gohugo.io/content-management/multilingual/#translation-by-filename 27 | # And https://gohugo.io/content-management/multilingual/#translation-by-content-directory 28 | defaultContentLanguage: en 29 | languages: 30 | en: 31 | languageName: English 32 | contentDir: content.en 33 | weight: 3 34 | zh: 35 | languageName: 简体中文 36 | contentDir: content.zh 37 | weight: 4 38 | 39 | 40 | menu: 41 | before: [] 42 | after: 43 | - name: "Github" 44 | url: "https://github.com/infinilabs/loadgen" 45 | weight: 10 46 | 47 | params: 48 | # (Optional, default light) Sets color theme: light, dark or auto. 49 | # Theme 'auto' switches between dark and light modes based on browser/os preferences 50 | BookTheme: "auto" 51 | 52 | # (Optional, default true) Controls table of contents visibility on right side of pages. 53 | # Start and end levels can be controlled with markup.tableOfContents setting. 54 | # You can also specify this parameter per page in front matter. 55 | BookToC: true 56 | 57 | # (Optional, default none) Set the path to a logo for the book. If the logo is 58 | # /static/logo.png then the path would be logo.png 59 | BookLogo: img/logo 60 | 61 | # (Optional, default none) Set leaf bundle to render as side menu 62 | # When not specified file structure and weights will be used 63 | # BookMenuBundle: /menu 64 | 65 | # (Optional, default docs) Specify root page to render child pages as menu. 66 | # Page is resoled by .GetPage function: https://gohugo.io/functions/getpage/ 67 | # For backward compatibility you can set '*' to render all sections to menu. Acts same as '/' 68 | BookSection: docs 69 | 70 | # Set source repository location. 71 | # Used for 'Last Modified' and 'Edit this page' links. 72 | BookRepo: https://github.com/infinilabs/loadgen 73 | 74 | # Enable "Edit this page" links for 'doc' page type. 75 | # Disabled by default. Uncomment to enable. Requires 'BookRepo' param. 76 | # Edit path must point to root directory of repo. 77 | BookEditPath: edit/BRANCH/docs 78 | 79 | # Configure the date format used on the pages 80 | # - In git information 81 | # - In blog posts 82 | BookDateFormat: "January 2, 2006" 83 | 84 | # (Optional, default true) Enables search function with flexsearch, 85 | # Index is built on fly, therefore it might slowdown your website. 86 | # Configuration for indexing can be adjusted in i18n folder per language. 87 | BookSearch: false 88 | 89 | # (Optional, default true) Enables comments template on pages 90 | # By default partals/docs/comments.html includes Disqus template 91 | # See https://gohugo.io/content-management/comments/#configure-disqus 92 | # Can be overwritten by same param in page frontmatter 93 | BookComments: false 94 | 95 | # /!\ This is an experimental feature, might be removed or changed at any time 96 | # (Optional, experimental, default false) Enables portable links and link checks in markdown pages. 97 | # Portable links meant to work with text editors and let you write markdown without {{< relref >}} shortcode 98 | # Theme will print warning if page referenced in markdown does not exists. 99 | BookPortableLinks: true 100 | 101 | # /!\ This is an experimental feature, might be removed or changed at any time 102 | # (Optional, experimental, default false) Enables service worker that caches visited pages and resources for offline use. 103 | BookServiceWorker: false 104 | -------------------------------------------------------------------------------- /docs/content.en/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: INFINI Loadgen 3 | type: docs 4 | bookCollapseSection: true 5 | weight: 3 6 | --- 7 | 8 | # INFINI Loadgen 9 | 10 | ## Introduction 11 | 12 | INFINI Loadgen is a lightweight performance testing tool specifically designed for Easysearch, Elasticsearch, and OpenSearch. 13 | 14 | ## Features 15 | 16 | - Robust performance 17 | - Lightweight and dependency-free 18 | - Random selection of template-based parameters 19 | - High concurrency 20 | - Balanced traffic control at the benchmark end 21 | - Validate server responses. 22 | 23 | {{< button relref="../docs/getting-started/install/" >}}Getting Started Now{{< /button >}} 24 | 25 | ## Community 26 | 27 | Fell free to join the Discord server to discuss anything around this project: 28 | 29 | [Discord Server](https://discord.gg/4tKTMkkvVX) 30 | 31 | ## Who Is Using? 32 | 33 | If you are using INFINI Loadgen and feel it pretty good, please [let us know](https://discord.gg/4tKTMkkvVX). Thank you for your support. 34 | -------------------------------------------------------------------------------- /docs/content.en/docs/getting-started/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 10 3 | title: Getting Started 4 | bookCollapseSection: true 5 | --- 6 | -------------------------------------------------------------------------------- /docs/content.en/docs/getting-started/benchmark.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 50 3 | title: "Benchmark Testing" 4 | --- 5 | 6 | # Benchmark Testing 7 | 8 | INFINI Loadgen is a lightweight performance testing tool specifically designed for Easysearch, Elasticsearch, and OpenSearch. 9 | 10 | Features of Loadgen: 11 | 12 | - Robust performance 13 | - Lightweight and dependency-free 14 | - Supports template-based parameter randomization 15 | - Supports high concurrency 16 | - Supports balanced traffic control at the benchmark end 17 | - Supports server response validation 18 | 19 | > Download link: 20 | 21 | ## Loadgen 22 | 23 | Loadgen is easy to use. After the tool is downloaded and decompressed, you will get three files: an executable program, a configuration file `loadgen.yml`, and a test file `loadgen.dsl`. The configuration file example is as follows: 24 | 25 | ```yaml 26 | env: 27 | ES_USERNAME: elastic 28 | ES_PASSWORD: elastic 29 | ES_ENDPOINT: http://localhost:8000 30 | ``` 31 | 32 | The test file example is as follows: 33 | 34 | ```text 35 | # runner: { 36 | # // total_rounds: 1 37 | # no_warm: false, 38 | # // Whether to log all requests 39 | # log_requests: false, 40 | # // Whether to log all requests with the specified response status 41 | # log_status_codes: [0, 500], 42 | # assert_invalid: false, 43 | # assert_error: false, 44 | # }, 45 | # variables: [ 46 | # { 47 | # name: "ip", 48 | # type: "file", 49 | # path: "dict/ip.txt", 50 | # // Replace special characters in the value 51 | # replace: { 52 | # '"': '\\"', 53 | # '\\': '\\\\', 54 | # }, 55 | # }, 56 | # { 57 | # name: "id", 58 | # type: "sequence", 59 | # }, 60 | # { 61 | # name: "id64", 62 | # type: "sequence64", 63 | # }, 64 | # { 65 | # name: "uuid", 66 | # type: "uuid", 67 | # }, 68 | # { 69 | # name: "now_local", 70 | # type: "now_local", 71 | # }, 72 | # { 73 | # name: "now_utc", 74 | # type: "now_utc", 75 | # }, 76 | # { 77 | # name: "now_utc_lite", 78 | # type: "now_utc_lite", 79 | # }, 80 | # { 81 | # name: "now_unix", 82 | # type: "now_unix", 83 | # }, 84 | # { 85 | # name: "now_with_format", 86 | # type: "now_with_format", 87 | # // https://programming.guide/go/format-parse-string-time-date-example.html 88 | # format: "2006-01-02T15:04:05-0700", 89 | # }, 90 | # { 91 | # name: "suffix", 92 | # type: "range", 93 | # from: 10, 94 | # to: 1000, 95 | # }, 96 | # { 97 | # name: "bool", 98 | # type: "range", 99 | # from: 0, 100 | # to: 1, 101 | # }, 102 | # { 103 | # name: "list", 104 | # type: "list", 105 | # data: ["medcl", "abc", "efg", "xyz"], 106 | # }, 107 | # { 108 | # name: "id_list", 109 | # type: "random_array", 110 | # variable_type: "number", // number/string 111 | # variable_key: "suffix", // variable key to get array items 112 | # square_bracket: false, 113 | # size: 10, // how many items for array 114 | # }, 115 | # { 116 | # name: "str_list", 117 | # type: "random_array", 118 | # variable_type: "number", // number/string 119 | # variable_key: "suffix", // variable key to get array items 120 | # square_bracket: true, 121 | # size: 10, // how many items for array 122 | # replace: { 123 | # // Use ' instead of " for string quotes 124 | # '"': "'", 125 | # // Use {} instead of [] as array brackets 126 | # "[": "{", 127 | # "]": "}", 128 | # }, 129 | # }, 130 | # ], 131 | 132 | POST $[[env.ES_ENDPOINT]]/medcl/_search 133 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } } 134 | # request: { 135 | # runtime_variables: {batch_no: "uuid"}, 136 | # runtime_body_line_variables: {routing_no: "uuid"}, 137 | # basic_auth: { 138 | # username: "$[[env.ES_USERNAME]]", 139 | # password: "$[[env.ES_PASSWORD]]", 140 | # }, 141 | # }, 142 | ``` 143 | 144 | ### Running Mode Settings 145 | 146 | By default, Loadgen runs in performance testing mode, repeating all requests in `requests` for the specified duration (`-d`). If you only need to check the test results once, you can set the number of executions of `requests` by `runner.total_rounds`. 147 | 148 | ### HTTP Header Handling 149 | 150 | By default, Loadgen will automatically format the HTTP response headers (`user-agent: xxx` -> `User-Agent: xxx`). If you need to precisely determine the response headers returned by the server, you can disable this behavior by setting `runner.disable_header_names_normalizing`. 151 | 152 | ## Usage of Variables 153 | 154 | In the above configuration, `variables` is used to define variable parameters, identified by `name`. In a constructed request, `$[[Variable name]]` can be used to access the value of the variable. The currently supported variable types are: 155 | 156 | | Type | Description | Parameters | 157 | | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 158 | | `file` | Load variables from file | `path`: the path of the data files
`data`: a list of values, will get appended to the end of the data specified by `path` file | 159 | | `list` | Defined variables inline | use `data` to define a string array | 160 | | `sequence` | 32-bit Variable of the auto incremental numeric type | `from`: the minimum of the values
`to`: the maximum of the values | 161 | | `sequence64` | 64-bit Variable of the auto incremental numeric type | `from`: the minimum of the values
`to`: the maximum of the values | 162 | | `range` | Variable of the range numbers, support parameters `from` and `to` to define the range | `from`: the minimum of the values
`to`: the maximum of the values | 163 | | `random_array` | Generate a random array, data elements come from the variable specified by `variable_key` | `variable_key`: data source variable
`size`: length of the output array
`square_bracket`: `true/false`, whether the output value needs `[` and `]`
`string_bracket`: string, the specified string will be attached before and after the output element | 164 | | `uuid` | UUID string type variable | | 165 | | `now_local` | Current time, local time zone | | 166 | | `now_utc` | Current time, UTC time zone. Output format: `2006-01-02 15:04:05.999999999 -0700 MST` | | 167 | | `now_utc_lite` | Current time, UTC time zone. Output format: `2006-01-02T15:04:05.000` | | 168 | | `now_unix` | Current time, Unix timestamp | | 169 | | `now_with_format` | Current time, supports custom `format` parameter to format the time string, such as: `2006-01-02T15:04:05-0700` | `format`: output time format ([example](https://www.geeksforgeeks.org/time-formatting-in-golang/)) | 170 | 171 | ### Variable Usage Example 172 | 173 | Variable parameters of the `file` type are loaded from an external text file. One variable parameter occupies one line. When one variable of the file type is accessed, one variable value is taken randomly. An example of the variable format is as follows: 174 | 175 | ```text 176 | # test/user.txt 177 | medcl 178 | elastic 179 | ``` 180 | 181 | Tips about how to generate a random string of fixed length, such as 1024 per line: 182 | 183 | ```bash 184 | LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\&\*\(\)-+= < /dev/random | head -c 1024 >> 1k.txt 185 | ``` 186 | 187 | ### Environment Variables 188 | 189 | Loadgen supports loading and using environment variables. You can specify the default values in the `loadgen.dsl` configuration. Loadgen will overwrite the variables at runtime if they are also specified by the command-line environment. 190 | 191 | The environment variables can be accessed by `$[[env.ENV_KEY]]`: 192 | 193 | ```text 194 | #// Configure default values for environment variables 195 | # env: { 196 | # ES_USERNAME: "elastic", 197 | # ES_PASSWORD: "elastic", 198 | # ES_ENDPOINT: "http://localhost:8000", 199 | # }, 200 | 201 | #// Use runtime variables 202 | GET $[[env.ES_ENDPOINT]]/medcl/_search 203 | {"query": {"match": {"name": "$[[user]]"}}} 204 | # request: { 205 | # // Use runtime variables 206 | # basic_auth: { 207 | # username: "$[[env.ES_USERNAME]]", 208 | # password: "$[[env.ES_PASSWORD]]", 209 | # }, 210 | # }, 211 | ``` 212 | 213 | ## Request Definition 214 | 215 | The `requests` node is used to set requests to be executed by Loadgen in sequence. Loadgen supports fixed-parameter requests and requests constructed using template-based variable parameters. The following is an example of a common query request: 216 | 217 | ```text 218 | GET http://localhost:8000/medcl/_search?q=name:$[[user]] 219 | # request: { 220 | # username: elastic, 221 | # password: pass, 222 | # }, 223 | ``` 224 | 225 | In the above query, Loadgen conducts queries based on the `medcl` index and executes one query based on the `name` field. The value of each request is from the random variable `user`. 226 | 227 | ### Simulating Bulk Ingestion 228 | 229 | It is very easy to use Loadgen to simulate bulk ingestion. Configure one index operation in the request body and then use the `body_repeat_times` parameter to randomly replicate several parameterized requests to complete the preparation of a batch of requests. See the following example. 230 | 231 | ```text 232 | POST http://localhost:8000/_bulk 233 | {"index": {"_index": "medcl-y4", "_type": "doc", "_id": "$[[uuid]]"}} 234 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 235 | # request: { 236 | # basic_auth: { 237 | # username: "test", 238 | # password: "testtest", 239 | # }, 240 | # body_repeat_times: 1000, 241 | # }, 242 | ``` 243 | 244 | ### Response Assertions 245 | 246 | You can use the `assert` configuration to check the response values. `assert` now supports most of all the [condition checkers](https://docs.infinilabs.com/gateway/main/docs/references/flow/#condition-type) of INFINI Gateway. 247 | 248 | ```text 249 | GET http://localhost:8000/medcl/_search?q=name:$[[user]] 250 | # request: { 251 | # basic_auth: { 252 | # username: "test", 253 | # password: "testtest", 254 | # }, 255 | # }, 256 | # assert: { 257 | # _ctx.response.status: 201, 258 | # }, 259 | ``` 260 | 261 | The 262 | 263 | response value can be accessed from the `_ctx` value, currently it contains these values: 264 | 265 | | Parameter | Description | 266 | | ------------------------- | ----------------------------------------------------------------------------------------------- | 267 | | `_ctx.response.status` | HTTP response status code | 268 | | `_ctx.response.header` | HTTP response headers | 269 | | `_ctx.response.body` | HTTP response body text | 270 | | `_ctx.response.body_json` | If the HTTP response body is a valid JSON string, you can access the JSON fields by `body_json` | 271 | | `_ctx.elapsed` | The time elapsed since request sent to the server (milliseconds) | 272 | 273 | If the request failed (e.g. the host is not reachable), Loadgen will record it under `Number of Errors` as part of the testing output. If you configured `runner.assert_error: true`, Loadgen will exit as `exit(2)` when there're any requests failed. 274 | 275 | If the assertion failed, Loadgen will record it under `Number of Invalid` as part of the testing output and skip the subsequent requests in this round. If you configured `runner.assert_invalid: true`, Loadgen will exit as `exit(1)` when there're any assertions failed. 276 | 277 | ### Dynamic Variable Registration 278 | 279 | Each request can use `register` to dynamically set the variables based on the response value, a common usage is to update the parameters of the later requests based on the previous responses. 280 | 281 | In the below example, we're registering the response value `_ctx.response.body_json.test.settings.index.uuid` of the `$[[env.ES_ENDPOINT]]/test` to the `index_id` variable, then we can access it by `$[[index_id]]`. 282 | 283 | ```text 284 | GET $[[env.ES_ENDPOINT]]/test 285 | # register: [ 286 | # {index_id: "_ctx.response.body_json.test.settings.index.uuid"}, 287 | # ], 288 | # assert: (200, {}), 289 | ``` 290 | 291 | ## Running the Benchmark 292 | 293 | Run the Loadgen program to perform the benchmark test as follows: 294 | 295 | ```text 296 | $ loadgen -d 30 -c 100 -compress -run 297 | 298 | loadgen.dsl 299 | 300 | 301 | __ ___ _ ___ ___ __ __ 302 | / / /___\/_\ / \/ _ \ /__\/\ \ \ 303 | / / // ///_\\ / /\ / /_\//_\ / \/ / 304 | / /__/ \_// _ \/ /_// /_\\//__/ /\ / 305 | \____|___/\_/ \_/___,'\____/\__/\_\ \/ 306 | 307 | [LOADGEN] A http load generator and testing suit. 308 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files 309 | [07-19 16:15:00] [INF] [instance.go:24] workspace: data/loadgen/nodes/0 310 | [07-19 16:15:00] [INF] [loader.go:312] warmup started 311 | [07-19 16:15:00] [INF] [app.go:306] loadgen now started. 312 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search 313 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}} 314 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search?q=name:medcl 315 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}} 316 | [07-19 16:15:01] [INF] [loader.go:316] [POST] http://localhost:8000/_bulk 317 | [07-19 16:15:01] [INF] [loader.go:317] status: 200,,{"took":120,"errors":false,"items":[{"index":{"_index":"medcl-y4","_type":"doc","_id":"c3qj9123r0okahraiej0","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":5735852,"_primary_term":3,"status":201}}]} 318 | [07-19 16:15:01] [INF] [loader.go:325] warmup finished 319 | 320 | 5253 requests in 32.756483336s, 524.61KB sent, 2.49MB received 321 | 322 | [Loadgen Client Metrics] 323 | Requests/sec: 175.10 324 | Request Traffic/sec: 17.49KB 325 | Total Transfer/sec: 102.34KB 326 | Avg Req Time: 5.711022ms 327 | Fastest Request: 440.448µs 328 | Slowest Request: 3.624302658s 329 | Number of Errors: 0 330 | Number of Invalid: 0 331 | Status 200: 5253 332 | 333 | [Estimated Server Metrics] 334 | Requests/sec: 160.37 335 | Transfer/sec: 93.73KB 336 | Avg Req Time: 623.576686ms 337 | ``` 338 | 339 | Before the formal benchmark, Loadgen will execute all requests once for warm-up. If an error occurs, it will prompt whether to continue. The warm-up request results will also be output to the terminal. After execution, a summary of the execution will be output. You can skip this check phase by setting `runner.no_warm`. 340 | 341 | > Since the final result of Loadgen is the cumulative statistics after all requests are completed, there may be inaccuracies. It is recommended to monitor Elasticsearch's various operating indicators in real-time through the Kibana monitoring dashboard. 342 | 343 | ### Command Line Parameters 344 | 345 | Loadgen will loop through the requests defined in the configuration file. By default, Loadgen will only run for `5s` and then automatically exit. If you want to extend the runtime or increase concurrency, you can control it by setting parameters at startup. Check the help command as follows: 346 | 347 | ```text 348 | $ loadgen -help 349 | Usage of loadgen: 350 | -c int 351 | Number of concurrent threads (default 1) 352 | -compress 353 | Compress requests with gzip 354 | -config string 355 | the location of config file (default "loadgen.yml") 356 | -cpu int 357 | the number of CPUs to use (default -1) 358 | -d int 359 | Duration of tests in seconds (default 5) 360 | -debug 361 | run in debug mode, loadgen will quit on panic immediately with full stack trace 362 | -dial-timeout int 363 | Connection dial timeout in seconds, default 3s (default 3) 364 | -gateway-log string 365 | Log level of Gateway (default "debug") 366 | -l int 367 | Limit total requests (default -1) 368 | -log string 369 | the log level, options: trace,debug,info,warn,error,off 370 | -mem int 371 | the max size of Memory to use, soft limit in megabyte (default -1) 372 | -plugin value 373 | load additional plugins 374 | -r int 375 | Max requests per second (fixed QPS) (default -1) 376 | -read-timeout int 377 | Connection read timeout in seconds, default 0s (use -timeout) 378 | -run string 379 | DSL config to run tests (default "loadgen.dsl") 380 | -service string 381 | service management, options: install,uninstall,start,stop 382 | -timeout int 383 | Request timeout in seconds, default 60s (default 60) 384 | -v version 385 | -write-timeout int 386 | Connection write timeout in seconds, default 0s (use -timeout) 387 | ``` 388 | 389 | ### Limiting Client Workload 390 | 391 | Using Loadgen and setting the command line parameter `-r` can limit the number of requests sent by the client per second, thereby evaluating the response time and load of Elasticsearch under fixed pressure, as follows: 392 | 393 | ```bash 394 | loadgen -d 30 -c 100 -r 100 395 | ``` 396 | 397 | > Note: The client throughput limit may not be accurate enough in the case of massive concurrencies. 398 | 399 | ### Limiting the Total Number of Requests 400 | 401 | By setting the parameter `-l`, you can control the total number of requests sent by the client to generate fixed documents. Modify the configuration as follows: 402 | 403 | ```text 404 | #// loadgen-gw.dsl 405 | POST http://localhost:8000/medcl-test/doc2/_bulk 406 | {"index": {"_index": "medcl-test", "_id": "$[[uuid]]"}} 407 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]"} 408 | # request: { 409 | # basic_auth: { 410 | # username: "test", 411 | # password: "testtest", 412 | # }, 413 | # body_repeat_times: 1, 414 | # }, 415 | ``` 416 | 417 | Each request contains only one document, then execute Loadgen 418 | 419 | ```bash 420 | loadgen -run loadgen-gw.dsl -d 600 -c 100 -l 50000 421 | ``` 422 | 423 | After execution, the Elasticsearch index `medcl-test` will have `50000` more records. 424 | 425 | ### Using Auto Incremental IDs to Ensure the Document Sequence 426 | 427 | If you want the generated document IDs to increase regularly for easy comparison, you can use the `sequence` type auto incremental ID as the primary key and avoid using random numbers in the content, as follows: 428 | 429 | ```text 430 | POST http://localhost:8000/medcl-test/doc2/_bulk 431 | {"index": {"_index": "medcl-test", "_id": "$[[id]]"}} 432 | {"id": "$[[id]]"} 433 | # request: { 434 | # basic_auth: { 435 | # username: "test", 436 | # password: "testtest", 437 | # }, 438 | # body_repeat_times: 1, 439 | # }, 440 | ``` 441 | 442 | ### Reuse Variables in Request Context 443 | 444 | In a request, we might want to use the same variable value, such as the `routing` parameter to control the shard destination, also store the field in the JSON document. You can use `runtime_variables` to set request-level variables, or `runtime_body_line_variables` to define request-body-level variables. If the request body is replicated N times, each line will be different, as shown in the following example: 445 | 446 | ```text 447 | # variables: [ 448 | # {name: "id", type: "sequence"}, 449 | # {name: "uuid", type: "uuid"}, 450 | # {name: "now_local", type: "now_local"}, 451 | # {name: "now_utc", type: "now_utc"}, 452 | # {name: "now_unix", type: "now_unix"}, 453 | # {name: "suffix", type: "range", from: 10, to 15}, 454 | # ], 455 | 456 | POST http://192.168.3.188:9206/_bulk 457 | {"create": {"_index": "test-$[[suffix]]", "_type": "doc", "_id": "$[[uuid]]", "routing": "$[[routing_no]]"}} 458 | {"id": "$[[uuid]]", "routing_no": "$[[routing_no]]", "batch_number": "$[[batch_no]]", "random_no": "$[[suffix]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 459 | # request: { 460 | # runtime_variables: { 461 | # batch_no: "id", 462 | # }, 463 | # runtime_body_line_variables: { 464 | # routing_no: "uuid", 465 | # }, 466 | # basic_auth: { 467 | # username: "ingest", 468 | # password: "password", 469 | # }, 470 | # body_repeat_times: 10, 471 | # }, 472 | ``` 473 | 474 | We defined the `batch_no` variable to represent the same batch number in a batch of documents, and the `routing_no` variable to represent the routing value at each document level. 475 | 476 | ### Customize Header 477 | 478 | ```text 479 | GET http://localhost:8000/test/_search 480 | # request: { 481 | # headers: [ 482 | # {Agent: "Loadgen-1"}, 483 | # ], 484 | # disable_header_names_normalizing: false, 485 | # }, 486 | ``` 487 | 488 | By default, Loadgen will canonilize the HTTP header keys in the configuration (`user-agent: xxx` -> `User-Agent: xxx`). If you need to set the HTTP header keys exactly, you can disable this behavior by setting `disable_header_names_normalizing: true`. 489 | 490 | ## Running Test Suites 491 | 492 | Loadgen supports running test cases in batches without writing test cases repeatedly. You can quickly test different environment configurations by switching suite configurations: 493 | 494 | ```yaml 495 | # loadgen.yml 496 | env: 497 | # Set up environments to run test suite 498 | LR_TEST_DIR: ./testing # The path to the test cases. 499 | # If you want to start gateway dynamically and automatically: 500 | LR_GATEWAY_CMD: ./bin/gateway # The path to the executable of INFINI Gateway 501 | LR_GATEWAY_HOST: 0.0.0.0:18000 # The binding host of the INFINI Gateway 502 | LR_GATEWAY_API_HOST: 0.0.0.0:19000 # The binding host of the INFINI Gateway API server 503 | # Set up other environments for the gateway and loadgen 504 | LR_ELASTICSEARCH_ENDPOINT: http://localhost:19201 505 | CUSTOM_ENV: myenv 506 | tests: 507 | # The relative path of test cases under `LR_TEST_DIR` 508 | # 509 | # - gateway.yml: (Optional) the configuration to start the INFINI Gateway dynamically. 510 | # - loadgen.dsl: the configuration to run the loadgen tool. 511 | # 512 | # The environments set in `env` section will be passed to the INFINI Gateway and loadgen. 513 | - path: cases/gateway/echo/echo_with_context 514 | ``` 515 | 516 | ### Environment Variables Configuration 517 | 518 | Loadgen dynamically configures INFINI Gateway through environment variables specified in `env`. The following environment variables are required: 519 | 520 | | Variable Name | Description | 521 | | ------------- | ----------------------- | 522 | | `LR_TEST_DIR` | Directory of test cases | 523 | 524 | If you need `loadgen` to dynamically start INFINI Gateway based on the configuration, you need to set the following environment variables: 525 | 526 | | Variable Name | Description | 527 | | --------------------- | ---------------------------------------- | 528 | | `LR_GATEWAY_CMD` | Path to the executable of INFINI Gateway | 529 | | `LR_GATEWAY_HOST` | Binding host:port of INFINI Gateway | 530 | | `LR_GATEWAY_API_HOST` | Binding host:port of INFINI Gateway API | 531 | 532 | ### Test Case Configuration 533 | 534 | Test cases are configured in `tests`, each path points to a directory of a test case. Each test case needs to configure a `gateway.yml` (optional) and a `loadgen.dsl`. Configuration files can use environment variables configured in `env` (`$[[env.ENV_KEY]]`). 535 | 536 | Example `gateway.yml` configuration: 537 | 538 | ```yaml 539 | path.data: data 540 | path.logs: log 541 | 542 | entry: 543 | - name: my_es_entry 544 | enabled: true 545 | router: my_router 546 | max_concurrency: 200000 547 | network: 548 | binding: $[[env.LR_GATEWAY_HOST]] 549 | 550 | flow: 551 | - name: hello_world 552 | filter: 553 | - echo: 554 | message: "hello world" 555 | router: 556 | - name: my_router 557 | default_flow: hello_world 558 | ``` 559 | 560 | Example `loadgen.dsl` configuration: 561 | 562 | ``` 563 | # runner: { 564 | # total_rounds: 1, 565 | # no_warm: true, 566 | # log_requests: true, 567 | # assert_invalid: true, 568 | # assert_error: true, 569 | # }, 570 | 571 | GET http://$[[env.LR_GATEWAY_HOST]]/ 572 | # assert: { 573 | # _ctx.response: { 574 | # status: 200, 575 | # body: "hello world", 576 | # }, 577 | # }, 578 | ``` 579 | 580 | ### Running Test Suites 581 | 582 | After configuring `loadgen.yml`, you can run Loadgen with the following command: 583 | 584 | ```bash 585 | loadgen -config loadgen.yml 586 | ``` 587 | 588 | Loadgen will run all the test cases specified in the configuration and output the test results: 589 | 590 | ```text 591 | $ loadgen -config loadgen.yml 592 | __ ___ _ ___ ___ __ __ 593 | / / /___\/_\ / \/ _ \ /__\/\ \ \ 594 | / / // ///_\\ / /\ / /_\//_\ / \/ / 595 | / /__/ \_// _ \/ /_// /_\\//__/ /\ / 596 | \____|___/\_/ \_/___,'\____/\__/\_\ \/ 597 | 598 | [LOADGEN] A http load generator and testing suit. 599 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files 600 | [02-21 10:50:05] [INF] [app.go:192] initializing loadgen 601 | [02-21 10:50:05] [INF] [app.go:193] using config: /Users/kassian/Workspace/infini/src/infini.sh/testing/suites/dev.yml 602 | [02-21 10:50:05] [INF] [instance.go:78] workspace: /Users/kassian/Workspace/infini/src/infini.sh/testing/data/loadgen/nodes/cfpihf15k34iqhpd4d00 603 | [02-21 10:50:05] [INF] [app.go:399] loadgen is up and running now. 604 | [2023-02-21 10:50:05][TEST][SUCCESS] [setup/loadgen/cases/dummy] duration: 105(ms) 605 | 606 | 1 requests in 68.373875ms, 0.00bytes sent, 0.00bytes received 607 | 608 | [Loadgen Client Metrics] 609 | Requests/sec: 0.20 610 | Request Traffic/sec: 0.00bytes 611 | Total Transfer/sec: 0.00bytes 612 | Avg Req Time: 5s 613 | Fastest Request: 68.373875ms 614 | Slowest Request: 68.373875ms 615 | Number of Errors: 0 616 | Number of Invalid: 0 617 | Status 200: 1 618 | 619 | [Estimated Server Metrics] 620 | Requests/sec: 14.63 621 | Transfer/sec: 0.00bytes 622 | Avg Req Time: 68.373875ms 623 | 624 | 625 | [2023-02-21 10:50:06][TEST][FAILED] [setup/gateway/cases/echo/echo_with_context/] duration: 1274(ms) 626 | #0 request, GET http://$[[env.LR_GATEWAY_HOST]]/any/, assertion failed, skiping subsequent requests 627 | 1 requests in 1.255678s, 0.00bytes sent, 0.00bytes received 628 | 629 | [Loadgen Client Metrics] 630 | Requests/sec: 0.20 631 | Request Traffic/sec: 0.00bytes 632 | Total Transfer/sec: 0.00bytes 633 | Avg Req Time: 5s 634 | Fastest Request: 1.255678s 635 | Slowest Request: 1.255678s 636 | Number of Errors: 1 637 | Number of Invalid: 1 638 | Status 0: 1 639 | 640 | [Estimated Server Metrics] 641 | Requests/sec: 0.80 642 | Transfer/sec: 0.00bytes 643 | Avg Req Time: 1.255678s 644 | 645 | ``` 646 | -------------------------------------------------------------------------------- /docs/content.en/docs/getting-started/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 10 3 | title: Installing the Loadgen 4 | asciinema: true 5 | --- 6 | 7 | # Installing the Loadgen 8 | 9 | INFINI Loadgen supports mainstream operating systems and platforms. The program package is small, with no extra external dependency. So, the loadgen can be installed very rapidly. 10 | 11 | ## Downloading 12 | 13 | **Automatic install** 14 | 15 | ```bash 16 | curl -sSL http://get.infini.cloud | bash -s -- -p loadgen 17 | ``` 18 | 19 | The above script can automatically download the latest version of the corresponding platform's loadgen and extract it to /opt/loadgen 20 | 21 | The optional parameters for the script are as follows: 22 | 23 | > _-v [version number](Default to use the latest version number)_ 24 | > _-d [installation directory] (default installation to /opt/loadgen)_ 25 | 26 | ```bash 27 | ➜ /tmp mkdir loadgen 28 | ➜ /tmp curl -sSL http://get.infini.cloud | bash -s -- -p loadgen -d /tmp/loadgen 29 | 30 | @@@@@@@@@@@ 31 | @@@@@@@@@@@@ 32 | @@@@@@@@@@@@ 33 | @@@@@@@@@&@@@ 34 | #@@@@@@@@@@@@@ 35 | @@@ @@@@@@@@@@@@@ 36 | &@@@@@@@ &@@@@@@@@@@@@@ 37 | @&@@@@@@@&@ @@@&@@@@@@@&@ 38 | @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ 39 | @@@@@@@@@@@@@@@@@@& @@@@@@@@@@@@@ 40 | %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 41 | @@@@@@@@@@@@&@@@@@@@@@@@@@@@ 42 | @@ ,@@@@@@@@@@@@@@@@@@@@@@@& 43 | @@@@@. @@@@@&@@@@@@@@@@@@@@ 44 | @@@@@@@@@@ @@@@@@@@@@@@@@@# 45 | @&@@@&@@@&@@@ &@&@@@&@@@&@ 46 | @@@@@@@@@@@@@. @@@@@@@* 47 | @@@@@@@@@@@@@ %@@@ 48 | @@@@@@@@@@@@@ 49 | /@@@@@@@&@@@@@ 50 | @@@@@@@@@@@@@ 51 | @@@@@@@@@@@@@ 52 | @@@@@@@@@@@@ Welcome to INFINI Labs! 53 | 54 | 55 | Now attempting the installation... 56 | 57 | Name: [loadgen], Version: [1.26.1-598], Path: [/tmp/loadgen] 58 | File: [https://release.infinilabs.com/loadgen/stable/loadgen-1.26.1-598-mac-arm64.zip] 59 | ##=O#- # 60 | 61 | Installation complete. [loadgen] is ready to use! 62 | 63 | 64 | ---------------------------------------------------------------- 65 | cd /tmp/loadgen && ./loadgen-mac-arm64 66 | ---------------------------------------------------------------- 67 | 68 | 69 | __ _ __ ____ __ _ __ __ 70 | / // |/ // __// // |/ // / 71 | / // || // _/ / // || // / 72 | /_//_/|_//_/ /_//_/|_//_/ 73 | 74 | ©INFINI.LTD, All Rights Reserved. 75 | ``` 76 | 77 | **Manual install** 78 | 79 | Select a package for downloading in the following URL based on your operating system and platform: 80 | 81 | [https://release.infinilabs.com/loadgen/](https://release.infinilabs.com/loadgen/) 82 | -------------------------------------------------------------------------------- /docs/content.en/docs/release-notes/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 80 3 | title: "Release Notes" 4 | --- 5 | 6 | # Release Notes 7 | 8 | Information about release notes of INFINI Loadgen is provided here. 9 | 10 | ## Latest (In development) 11 | ### ❌ Breaking changes 12 | ### 🚀 Features 13 | ### 🐛 Bug fix 14 | ### ✈️ Improvements 15 | 16 | ## 1.29.4 (2025-05-16) 17 | ### ✈️ Improvements 18 | - This release includes updates from the underlying [Framework v1.1.7](https://docs.infinilabs.com/framework/v1.1.7/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Gateway itself, the improvements inherited from Framework benefit Loadgen indirectly. 19 | 20 | ## 1.29.3 (2025-04-27) 21 | This release includes updates from the underlying [Framework v1.1.6](https://docs.infinilabs.com/framework/v1.1.6/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Loadgen itself, the improvements inherited from Framework benefit Loadgen indirectly. 22 | 23 | 24 | ## 1.29.2 (2025-03-31) 25 | This release includes updates from the underlying [Framework v1.1.5](https://docs.infinilabs.com/framework/v1.1.5/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Loadgen itself, the improvements inherited from Framework benefit Loadgen indirectly. 26 | 27 | ## 1.29.1 (2025-03-14) 28 | This release includes updates from the underlying [Framework v1.1.4](https://docs.infinilabs.com/framework/v1.1.4/docs/references/http_client/), which resolves several common issues and enhances overall stability and performance. While there are no direct changes to Loadgen itself, the improvements inherited from Framework benefit Loadgen indirectly. 29 | 30 | 31 | ## 1.29.0 (2025-02-28) 32 | 33 | ### Improvements 34 | 35 | - Synchronize updates for known issues fixed in the Framework. 36 | 37 | ## 1.28.2 (2025-02-15) 38 | 39 | ### Improvements 40 | 41 | - Synchronize updates for known issues fixed in the Framework. 42 | 43 | ## 1.28.1 (2025-01-24) 44 | 45 | ### Improvements 46 | 47 | - Synchronize updates for known issues fixed in the Framework. 48 | 49 | ## 1.28.0 (2025-01-11) 50 | 51 | ### Improvements 52 | 53 | - Synchronize updates for known issues fixed in the Framework. 54 | 55 | ## 1.27.0 (2024-12-13) 56 | 57 | ### Improvements 58 | 59 | - The code is open source, and Github [repository](https://github.com/infinilabs/loadgen) is used for development. 60 | - Keep the same version number as INFINI Console. 61 | - Synchronize updates for known issues fixed in the Framework. 62 | 63 | ### Bug fix 64 | 65 | - Fix the abnormal problem of the API interface testing logic. 66 | 67 | ## 1.26.1 (2024-08-13) 68 | 69 | ### Improvements 70 | 71 | - Keep the same version number as INFINI Console. 72 | - Synchronize updates for known issues fixed in the Framework. 73 | 74 | ## 1.26.0 (2024-06-07) 75 | 76 | ### Improvements 77 | 78 | - Keep the same version number as INFINI Console. 79 | - Synchronize updates for known issues fixed in the Framework. 80 | 81 | ## 1.25.0 (2024-04-30) 82 | 83 | ### Improvements 84 | 85 | - Keep the same version number as INFINI Console. 86 | - Synchronize updates for known issues fixed in the Framework. 87 | 88 | ## 1.24.0 (2024-04-15) 89 | 90 | ### Improvements 91 | 92 | - Keep the same version number as INFINI Console. 93 | - Synchronize updates for known issues fixed in the Framework. 94 | 95 | ## 1.22.0 (2024-01-26) 96 | 97 | ### Improvements 98 | 99 | - Unified version number with INFINI Console 100 | 101 | ## 1.8.0 (2023-11-02) 102 | 103 | ### Breaking changes 104 | 105 | - The original Loadrun function is incorporated into Loadgen. 106 | - Test the requests, assertions, etc. that is configured using the new Loadgen DSL syntax. 107 | 108 | ## 1.7.0 (2023-04-20) 109 | 110 | ### Breaking changes 111 | 112 | - The variables with the same `name` are no longer allowed to be defined in `variables`. 113 | 114 | ### Features 115 | 116 | - Add the `log_status_code` configuration to support printing request logs of specific status codes. 117 | 118 | ## 1.6.0 (2023-04-06) 119 | 120 | ### Breaking ghanges 121 | 122 | - The `file` type variable by default no longer escapes the `"` and `\` characters. Use the `replace` function to manually set variable escaping. 123 | 124 | ### Features 125 | 126 | - The variable definition adds an optional `replace` option, which is used to escape characters such as `"` and `\`. 127 | 128 | ### Improvements 129 | 130 | - Optimize memory usage. 131 | 132 | ### Bug fix 133 | 134 | - Fix the problem that the `\n` cannot be used in the YAML strings. 135 | - Fix the problem that invalid assert configurations are ignored. 136 | 137 | ## 1.5.1 138 | 139 | ### Bug fix 140 | 141 | - [DOC] Fix invalid variable grammar in `loadgen.yml`. 142 | 143 | ## 1.5.0 144 | 145 | ### Features 146 | 147 | - Added `assert` configuration, support testing response data. 148 | - Added `register` configuration, support registering dynamic variables. 149 | - Added `env` configuration, support loading and using environment variables in `loadgen.yml`. 150 | - Support using dynamic variables in the `headers` configuration. 151 | 152 | ### Improvements 153 | 154 | - `-l` option: precisely control the number of requests to send. 155 | - Added `runner.no_warm` to skip warm-up stage. 156 | 157 | ### Bug fix 158 | -------------------------------------------------------------------------------- /docs/content.en/menu/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | headless: true 3 | --- 4 | 5 | - [**Documentation**]({{< relref "/docs/" >}}) 6 |
-------------------------------------------------------------------------------- /docs/content.zh/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: INFINI Loadgen 3 | type: docs 4 | bookCollapseSection: true 5 | weight: 4 6 | --- 7 | 8 | # INFINI Loadgen 9 | 10 | ## 介绍 11 | 12 | INFINI Loadgen 是一款专为 Easysearch、Elasticsearch、OpenSearch 设计的轻量级性能测试工具, 13 | 14 | ## 特性 15 | 16 | - 性能强劲 17 | - 轻量级无依赖 18 | - 支持模板化参数随机 19 | - 支持高并发 20 | - 支持压测端均衡流量控制 21 | - 支持服务端返回值校验 22 | 23 | {{< button relref="../docs/getting-started/install/" >}}即刻开始{{< /button >}} 24 | 25 | ## 社区 26 | 27 | [加入我们的 Discord Server](https://discord.gg/4tKTMkkvVX) 28 | 29 | ## 谁在用? 30 | 31 | 如果您正在使用 Loadgen,并且您觉得它还不错的话,请[告诉我们](https://discord.gg/4tKTMkkvVX),感谢您的支持。 32 | -------------------------------------------------------------------------------- /docs/content.zh/docs/getting-started/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 10 3 | title: 入门指南 4 | bookCollapseSection: true 5 | --- 6 | -------------------------------------------------------------------------------- /docs/content.zh/docs/getting-started/benchmark.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 50 3 | title: "性能测试" 4 | --- 5 | 6 | # 性能测试 7 | 8 | INFINI Loadgen 是一款专为 Easysearch、Elasticsearch、OpenSearch 设计的轻量级性能测试工具。 9 | 10 | Loadgen 的特点: 11 | 12 | - 性能强劲 13 | - 轻量级无依赖 14 | - 支持模板化参数随机 15 | - 支持高并发 16 | - 支持压测端均衡流量控制 17 | - 支持服务端返回值校验 18 | 19 | > 下载地址: 20 | 21 | ## Loadgen 22 | 23 | Loadgen 使用非常简单,下载解压之后会得到三个文件,一个可执行程序、一个配置文件 `loadgen.yml` 以及用于运行测试的 `loadgen.dsl`,配置文件样例如下: 24 | 25 | ```yaml 26 | env: 27 | ES_USERNAME: elastic 28 | ES_PASSWORD: elastic 29 | ES_ENDPOINT: http://localhost:8000 30 | ``` 31 | 32 | 测试文件样例如下: 33 | 34 | ```text 35 | # runner: { 36 | # // total_rounds: 1 37 | # no_warm: false, 38 | # // Whether to log all requests 39 | # log_requests: false, 40 | # // Whether to log all requests with the specified response status 41 | # log_status_codes: [0, 500], 42 | # assert_invalid: false, 43 | # assert_error: false, 44 | # }, 45 | # variables: [ 46 | # { 47 | # name: "ip", 48 | # type: "file", 49 | # path: "dict/ip.txt", 50 | # // Replace special characters in the value 51 | # replace: { 52 | # '"': '\\"', 53 | # '\\': '\\\\', 54 | # }, 55 | # }, 56 | # { 57 | # name: "id", 58 | # type: "sequence", 59 | # }, 60 | # { 61 | # name: "id64", 62 | # type: "sequence64", 63 | # }, 64 | # { 65 | # name: "uuid", 66 | # type: "uuid", 67 | # }, 68 | # { 69 | # name: "now_local", 70 | # type: "now_local", 71 | # }, 72 | # { 73 | # name: "now_utc", 74 | # type: "now_utc", 75 | # }, 76 | # { 77 | # name: "now_utc_lite", 78 | # type: "now_utc_lite", 79 | # }, 80 | # { 81 | # name: "now_unix", 82 | # type: "now_unix", 83 | # }, 84 | # { 85 | # name: "now_with_format", 86 | # type: "now_with_format", 87 | # // https://programming.guide/go/format-parse-string-time-date-example.html 88 | # format: "2006-01-02T15:04:05-0700", 89 | # }, 90 | # { 91 | # name: "suffix", 92 | # type: "range", 93 | # from: 10, 94 | # to: 1000, 95 | # }, 96 | # { 97 | # name: "bool", 98 | # type: "range", 99 | # from: 0, 100 | # to: 1, 101 | # }, 102 | # { 103 | # name: "list", 104 | # type: "list", 105 | # data: ["medcl", "abc", "efg", "xyz"], 106 | # }, 107 | # { 108 | # name: "id_list", 109 | # type: "random_array", 110 | # variable_type: "number", // number/string 111 | # variable_key: "suffix", // variable key to get array items 112 | # square_bracket: false, 113 | # size: 10, // how many items for array 114 | # }, 115 | # { 116 | # name: "str_list", 117 | # type: "random_array", 118 | # variable_type: "number", // number/string 119 | # variable_key: "suffix", // variable key to get array items 120 | # square_bracket: true, 121 | # size: 10, // how many items for array 122 | # replace: { 123 | # // Use ' instead of " for string quotes 124 | # '"': "'", 125 | # // Use {} instead of [] as array brackets 126 | # "[": "{", 127 | # "]": "}", 128 | # }, 129 | # }, 130 | # ], 131 | 132 | POST $[[env.ES_ENDPOINT]]/medcl/_search 133 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } } 134 | # request: { 135 | # runtime_variables: {batch_no: "uuid"}, 136 | # runtime_body_line_variables: {routing_no: "uuid"}, 137 | # basic_auth: { 138 | # username: "$[[env.ES_USERNAME]]", 139 | # password: "$[[env.ES_PASSWORD]]", 140 | # }, 141 | # }, 142 | ``` 143 | 144 | ### 运行模式设置 145 | 146 | 默认配置下,Loadgen 会以性能测试模式运行,在指定时间(`-d`)内重复执行 `requests` 里的所有请求。如果只需要检查一次测试结果,可以通过 `runner.total_rounds` 来设置 `requests` 的执行次数。 147 | 148 | ### HTTP 响应头处理 149 | 150 | 默认配置下,Loadgen 会自动格式化 HTTP 的响应头(`user-agent: xxx` -> `User-Agent: xxx`),如果需要精确判断服务器返回的响应头,可以通过 `runner.disable_header_names_normalizing` 来禁用这个行为。 151 | 152 | ## 变量的使用 153 | 154 | 上面的配置中,`variables` 用来定义变量参数,根据 `name` 来设置变量标识,在构造请求的使用 `$[[变量名]]` 即可访问该变量的值,变量目前支持的类型有: 155 | 156 | | 类型 | 说明 | 变量参数 | 157 | | ----------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 158 | | `file` | 文件型外部变量参数 | `path`: 数据文件路径
`data`: 数据列表,会被附加到`path`文件内容后读取 | 159 | | `list` | 自定义枚举变量参数 | `data`: 字符数组类型的枚举数据列表 | 160 | | `sequence` | 32 位自增数字类型的变量 | `from`: 初始值
`to`: 最大值 | 161 | | `sequence64` | 64 位自增数字类型的变量 | `from`: 初始值
`to`: 最大值 | 162 | | `range` | 数字范围类型的变量,支持参数 `from` 和 `to` 来限制范围 | `from`: 初始值
`to`: 最大值 | 163 | | `random_array` | 生成一个随机数组,数据元素来自`variable_key`指定的变量 | `variable_key`: 数据源变量
`size`: 输出数组的长度
`square_bracket`: `true/false`,输出值是否需要`[`和`]`
`string_bracket`: 字符串,输出元素前后会附加指定的字符串 | 164 | | `uuid` | UUID 字符类型的变量 | | 165 | | `now_local` | 当前时间、本地时区 | | 166 | | `now_utc` | 当前时间、UTC 时区。输出格式:`2006-01-02 15:04:05.999999999 -0700 MST` | | 167 | | `now_utc_lite` | 当前时间、UTC 时区。输出格式:`2006-01-02T15:04:05.000` | | 168 | | `now_unix` | 当前时间、Unix 时间戳 | | 169 | | `now_with_format` | 当前时间,支持自定义 `format` 参数来格式化时间字符串,如:`2006-01-02T15:04:05-0700` | `format`: 输出的时间格式 ([示例](https://www.geeksforgeeks.org/time-formatting-in-golang/)) | 170 | 171 | ### 变量使用示例 172 | 173 | `file` 类型变量参数加载自外部文本文件,每行一个变量参数,访问该变量时每次随机取其中一个,变量里面的定义格式举例如下: 174 | 175 | ```text 176 | # test/user.txt 177 | medcl 178 | elastic 179 | ``` 180 | 181 | 附生成固定长度的随机字符串,如 1024 个字符每行: 182 | 183 | ```bash 184 | LC_CTYPE=C tr -dc A-Za-z0-9_\!\@\#\$\%\^\&\*\(\)-+= < /dev/random | head -c 1024 >> 1k.txt 185 | ``` 186 | 187 | ### 环境变量 188 | 189 | Loadgen 支持自动读取环境变量,环境变量可以在运行 Loadgen 时通过命令行传入,也可以在 `loadgen.dsl` 里指定默认的环境变量值,Loadgen 运行时会使用命令行传入的环境变量覆盖 `loadgen.dsl` 里的默认值。 190 | 191 | 配置的环境变量可以通过 `$[[env.环境变量]]` 来使用: 192 | 193 | ```text 194 | #// 配置环境变量默认值 195 | # env: { 196 | # ES_USERNAME: "elastic", 197 | # ES_PASSWORD: "elastic", 198 | # ES_ENDPOINT: "http://localhost:8000", 199 | # }, 200 | 201 | #// 使用运行时变量 202 | GET $[[env.ES_ENDPOINT]]/medcl/_search 203 | {"query": {"match": {"name": "$[[user]]"}}} 204 | # request: { 205 | # // 使用运行时变量 206 | # basic_auth: { 207 | # username: "$[[env.ES_USERNAME]]", 208 | # password: "$[[env.ES_PASSWORD]]", 209 | # }, 210 | # }, 211 | ``` 212 | 213 | ## 请求的定义 214 | 215 | 配置节点 `requests` 用来设置 Loadgen 将依次执行的请求,支持固定参数的请求,也可支持模板变量参数化构造请求,以下是一个普通的查询请求: 216 | 217 | ```text 218 | GET http://localhost:8000/medcl/_search?q=name:$[[user]] 219 | # request: { 220 | # username: elastic, 221 | # password: pass, 222 | # }, 223 | ``` 224 | 225 | 上面的查询对 `medcl` 索引进行了查询,并对 `name` 字段执行一个查询,每次请求的值来自随机变量 `user`。 226 | 227 | ### 模拟批量写入 228 | 229 | 使用 Loadgen 来模拟 bulk 批量写入也非常简单,在请求体里面配置一条索引操作,然后使用 `body_repeat_times` 参数来随机参数化复制若干条请求即可完成一批请求的准备,如下: 230 | 231 | ```text 232 | POST http://localhost:8000/_bulk 233 | {"index": {"_index": "medcl-y4", "_type": "doc", "_id": "$[[uuid]]"}} 234 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 235 | # request: { 236 | # basic_auth: { 237 | # username: "test", 238 | # password: "testtest", 239 | # }, 240 | # body_repeat_times: 1000, 241 | # }, 242 | ``` 243 | 244 | ### 返回值判断 245 | 246 | 每个 `requests` 配置可以通过 `assert` 来设置是否需要检查返回值。`assert` 功能支持 INFINI Gateway 的大部分[条件判断功能](https://infinilabs.cn/docs/latest/gateway/references/flow/#条件类型)。 247 | 248 | > 请阅读[《借助 DSL 来简化 Loadgen 配置》](https://infinilabs.cn/blog/2023/simplify-loadgen-config-with-dsl/)来了解更多细节。 249 | 250 | ```text 251 | GET http://localhost:8000/medcl/_search?q=name:$[[user]] 252 | # request: { 253 | # basic_auth: { 254 | # username: "test", 255 | # password: "testtest", 256 | # }, 257 | # }, 258 | # assert: { 259 | # _ctx.response.status: 201, 260 | # }, 261 | ``` 262 | 263 | 请求返回值可以通过 `_ctx` 获取,`_ctx` 目前包含以下信息: 264 | 265 | | 参数 | 说明 | 266 | | ------------------------- | --------------------------------------------------------------------------------------- | 267 | | `_ctx.response.status` | HTTP 返回状态码 | 268 | | `_ctx.response.header` | HTTP 返回响应头 | 269 | | `_ctx.response.body` | HTTP 返回响应体 | 270 | | `_ctx.response.body_json` | 如果 HTTP 返回响应体是一个有效的 JSON 字符串,可以通过 `body_json` 来访问 JSON 内容字段 | 271 | | `_ctx.elapsed` | 当前请求发送到返回消耗的时间(毫秒) | 272 | 273 | 如果请求失败(请求地址无法访问等),Loadgen 无法获取 HTTP 请求返回值,Loadgen 会在输出日志里记录 `Number of Errors`。如果配置了 `runner.assert_error` 且存在请求失败的请求,Loadgen 会返回 `exit(2)` 错误码。 274 | 275 | 如果返回值不符合判断条件,Loadgen 会停止执行当前轮次后续请求,并在输出日志里记录 `Number of Invalid`。如果配置了 `runner.assert_invalid` 且存在判断失败的请求,Loadgen 会返回 `exit(1)` 错误码。 276 | 277 | ### 动态变量注册 278 | 279 | 每个 `requests` 配置可以通过 `register` 来动态添加运行时参数,一个常见的使用场景是根据前序请求的返回值来动态设置后序请求的参数。 280 | 281 | 这个示例调用 `$[[env.ES_ENDPOINT]]/test` 接口获取索引的 UUID,并注册到 `index_id` 变量。后续的请求定义可以通过 `$[[index_id]]` 来获取这个值。 282 | 283 | ```text 284 | GET $[[env.ES_ENDPOINT]]/test 285 | # register: [ 286 | # {index_id: "_ctx.response.body_json.test.settings.index.uuid"}, 287 | # ], 288 | # assert: (200, {}), 289 | ``` 290 | 291 | ## 执行压测 292 | 293 | 执行 Loadgen 程序即可执行压测,如下: 294 | 295 | ```text 296 | $ loadgen -d 30 -c 100 -compress -run loadgen.dsl 297 | __ ___ _ ___ ___ __ __ 298 | / / /___\/_\ / \/ _ \ /__\/\ \ \ 299 | / / // ///_\\ / /\ / /_\//_\ / \/ / 300 | / /__/ \_// _ \/ /_// /_\\//__/ /\ / 301 | \____|___/\_/ \_/___,'\____/\__/\_\ \/ 302 | 303 | [LOADGEN] A http load generator and testing suit. 304 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files 305 | [07-19 16:15:00] [INF] [instance.go:24] workspace: data/loadgen/nodes/0 306 | [07-19 16:15:00] [INF] [loader.go:312] warmup started 307 | [07-19 16:15:00] [INF] [app.go:306] loadgen now started. 308 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search 309 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}} 310 | [07-19 16:15:00] [INF] [loader.go:316] [GET] http://localhost:8000/medcl/_search?q=name:medcl 311 | [07-19 16:15:00] [INF] [loader.go:317] status: 200,,{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}} 312 | [07-19 16:15:01] [INF] [loader.go:316] [POST] http://localhost:8000/_bulk 313 | [07-19 16:15:01] [INF] [loader.go:317] status: 200,,{"took":120,"errors":false,"items":[{"index":{"_index":"medcl-y4","_type":"doc","_id":"c3qj9123r0okahraiej0","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":5735852,"_primary_term":3,"status":201}}]} 314 | [07-19 16:15:01] [INF] [loader.go:325] warmup finished 315 | 316 | 5253 requests in 32.756483336s, 524.61KB sent, 2.49MB received 317 | 318 | [Loadgen Client Metrics] 319 | Requests/sec: 175.10 320 | Request Traffic/sec: 17.49KB 321 | Total Transfer/sec: 102.34KB 322 | Avg Req Time: 5.711022ms 323 | Fastest Request: 440.448µs 324 | Slowest Request: 3.624302658s 325 | Number of Errors: 0 326 | Number of Invalid: 0 327 | Status 200: 5253 328 | 329 | [Estimated Server Metrics] 330 | Requests/sec: 160.37 331 | Transfer/sec: 93.73KB 332 | Avg Req Time: 623.576686ms 333 | ``` 334 | 335 | Loadgen 在正式压测之前会将所有的请求执行一次来进行预热,如果出现错误会提示是否继续,预热的请求结果也会输出到终端,执行完成之后会输出执行的摘要信息。可以通过设置 `runner.no_warm` 来跳过这个检查阶段。 336 | 337 | > 因为 Loadgen 最后的结果是所有请求全部执行完成之后的累计统计,可能存在不准的问题,建议通过打开 Kibana 的监控仪表板来实时查看 Elasticsearch 的各项运行指标。 338 | 339 | ### 命令行参数 340 | 341 | Loadgen 会循环执行配置文件里面定义的请求,默认 Loadgen 只会运行 `5s` 就自动退出了,如果希望延长运行时间或者加大并发可以通过启动的时候设置参数来控制,通过查看帮助命令如下: 342 | 343 | ```text 344 | $ loadgen -help 345 | Usage of loadgen: 346 | -c int 347 | Number of concurrent threads (default 1) 348 | -compress 349 | Compress requests with gzip 350 | -config string 351 | the location of config file (default "loadgen.yml") 352 | -cpu int 353 | the number of CPUs to use (default -1) 354 | -d int 355 | Duration of tests in seconds (default 5) 356 | -debug 357 | run in debug mode, loadgen will quit on panic immediately with full stack trace 358 | -dial-timeout int 359 | Connection dial timeout in seconds, default 3s (default 3) 360 | -gateway-log string 361 | Log level of Gateway (default "debug") 362 | -l int 363 | Limit total requests (default -1) 364 | -log string 365 | the log level, options: trace,debug,info,warn,error,off 366 | -mem int 367 | the max size of Memory to use, soft limit in megabyte (default -1) 368 | -plugin value 369 | load additional plugins 370 | -r int 371 | Max requests per second (fixed QPS) (default -1) 372 | -read-timeout int 373 | Connection read timeout in seconds, default 0s (use -timeout) 374 | -run string 375 | DSL config to run tests (default "loadgen.dsl") 376 | -service string 377 | service management, options: install,uninstall,start,stop 378 | -timeout int 379 | Request timeout in seconds, default 60s (default 60) 380 | -v version 381 | -write-timeout int 382 | Connection write timeout in seconds, default 0s (use -timeout) 383 | ``` 384 | 385 | ### 限制客户端压力 386 | 387 | 使用 Loadgen 并设置命令行参数 `-r` 可以限制客户端发送的每秒请求数,从而评估固定压力下 Elasticsearch 的响应时间和负载情况,如下: 388 | 389 | ```bash 390 | loadgen -d 30 -c 100 -r 100 391 | ``` 392 | 393 | > 注意,在大量并发下,此客户端吞吐限制可能不完全准确。 394 | 395 | ### 限制请求的总条数 396 | 397 | 通过设置参数 `-l` 可以控制客户端发送的请求总数,从而制造固定的文档,修改配置如下: 398 | 399 | ```text 400 | #// loadgen-gw.dsl 401 | POST http://localhost:8000/medcl-test/doc2/_bulk 402 | {"index": {"_index": "medcl-test", "_id": "$[[uuid]]"}} 403 | {"id": "$[[id]]", "field1": "$[[user]]", "ip": "$[[ip]]"} 404 | # request: { 405 | # basic_auth: { 406 | # username: "test", 407 | # password: "testtest", 408 | # }, 409 | # body_repeat_times: 1, 410 | # }, 411 | ``` 412 | 413 | 每次请求只有一个文档,然后执行 Loadgen 414 | 415 | ```bash 416 | loadgen -run loadgen-gw.dsl -d 600 -c 100 -l 50000 417 | ``` 418 | 419 | 执行完成之后,Elasticsearch 的索引 `medcl-test` 将增加 `50000` 条记录。 420 | 421 | ### 使用自增 ID 来确保文档的顺序性 422 | 423 | 如果希望生成的文档编号自增有规律,方便进行对比,可以使用 `sequence` 类型的自增 ID 来作为主键,内容也不要用随机数,如下: 424 | 425 | ```text 426 | POST http://localhost:8000/medcl-test/doc2/_bulk 427 | {"index": {"_index": "medcl-test", "_id": "$[[id]]"}} 428 | {"id": "$[[id]]"} 429 | # request: { 430 | # basic_auth: { 431 | # username: "test", 432 | # password: "testtest", 433 | # }, 434 | # body_repeat_times: 1, 435 | # }, 436 | ``` 437 | 438 | ### 上下文复用变量 439 | 440 | 在一个请求中,我们可能希望有相同的参数出现,比如 `routing` 参数用来控制分片的路由,同时我们又希望该参数也保存在文档的 JSON 里面, 441 | 可以使用 `runtime_variables` 来设置请求级别的变量,或者 `runtime_body_line_variables` 定义请求体级别的变量,如果请求体复制 N 份,每份的参数是不同的,举例如下: 442 | 443 | ```text 444 | # variables: [ 445 | # {name: "id", type: "sequence"}, 446 | # {name: "uuid", type: "uuid"}, 447 | # {name: "now_local", type: "now_local"}, 448 | # {name: "now_utc", type: "now_utc"}, 449 | # {name: "now_unix", type: "now_unix"}, 450 | # {name: "suffix", type: "range", from: 10, to 15}, 451 | # ], 452 | 453 | POST http://192.168.3.188:9206/_bulk 454 | {"create": {"_index": "test-$[[suffix]]", "_type": "doc", "_id": "$[[uuid]]", "routing": "$[[routing_no]]"}} 455 | {"id": "$[[uuid]]", "routing_no": "$[[routing_no]]", "batch_number": "$[[batch_no]]", "random_no": "$[[suffix]]", "ip": "$[[ip]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 456 | # request: { 457 | # runtime_variables: { 458 | # batch_no: "id", 459 | # }, 460 | # runtime_body_line_variables: { 461 | # routing_no: "uuid", 462 | # }, 463 | # basic_auth: { 464 | # username: "ingest", 465 | # password: "password", 466 | # }, 467 | # body_repeat_times: 10, 468 | # }, 469 | ``` 470 | 471 | 我们定义了 `batch_no` 变量来代表一批文档里面的相同批次号,同时又定义了 `routing_no` 变量来代表每个文档级别的 routing 值。 472 | 473 | ### 自定义 Header 474 | 475 | ```text 476 | GET http://localhost:8000/test/_search 477 | # request: { 478 | # headers: [ 479 | # {Agent: "Loadgen-1"}, 480 | # ], 481 | # disable_header_names_normalizing: false, 482 | # }, 483 | ``` 484 | 485 | 默认配置下,Loadgen 会自动格式化配置里的 HTTP 的请求头(`user-agent: xxx` -> `User-Agent: xxx`),如果需要精确设置 HTTP 请求头,可以通过设置 `disable_header_names_normalizing: true` 来禁用这个行为。 486 | 487 | ## 运行测试套件 488 | 489 | Loadgen 支持批量运行测试用例,不需要重复编写测试用例,通过切换套件配置来快速测试不同的环境配置: 490 | 491 | ```yaml 492 | # loadgen.yml 493 | env: 494 | # Set up envrionments to run test suite 495 | LR_TEST_DIR: ./testing # The path to the test cases. 496 | # If you want to start gateway dynamically and automatically: 497 | LR_GATEWAY_CMD: ./bin/gateway # The path to the executable of INFINI Gateway 498 | LR_GATEWAY_HOST: 0.0.0.0:18000 # The binding host of the INFINI Gateway 499 | LR_GATEWAY_API_HOST: 0.0.0.0:19000 # The binding host of the INFINI Gateway API server 500 | # Set up other envrionments for the gateway and loadgen 501 | LR_ELASTICSEARCH_ENDPOINT: http://localhost:19201 502 | CUSTOM_ENV: myenv 503 | tests: 504 | # The relative path of test cases under `LR_TEST_DIR` 505 | # 506 | # - gateway.yml: (Optional) the configuration to start the INFINI Gateway dynamically. 507 | # - loadgen.dsl: the configuration to run the loadgen tool. 508 | # 509 | # The environments set in `env` section will be passed to the INFINI Gateway and loadgen. 510 | - path: cases/gateway/echo/echo_with_context 511 | ``` 512 | 513 | ### 环境变量配置 514 | 515 | Loadgen 通过环境变量来动态配置 INFINI Gateway,环境变量在 `env` 里指定。以下环境变量是必选的: 516 | 517 | | 变量名 | 说明 | 518 | | ------------- | ---------------- | 519 | | `LR_TEST_DIR` | 测试用例所在目录 | 520 | 521 | 如果你需要 `loadgen` 根据配置动态启动 INFINI Gateway,需要设置以下环境变量: 522 | 523 | | 变量名 | 说明 | 524 | | --------------------- | ------------------------------------ | 525 | | `LR_GATEWAY_CMD` | INFINI Gateway 可执行文件的路径 | 526 | | `LR_GATEWAY_HOST` | INFINI Gateway 绑定的主机名:端口 | 527 | | `LR_GATEWAY_API_HOST` | INFINI Gateway API 绑定的主机名:端口 | 528 | 529 | ### 测试用例配置 530 | 531 | 测试用例在 `tests` 里配置,每个路径(`path`)指向一个测试用例的目录,每个测试用例需要配置一份 `gateway.yml`(可选)和 `loadgen.dsl`。配置文件可以使用 `env` 下配置的环境变量(`$[[env.ENV_KEY]]`)。 532 | 533 | `gateway.yml` 参考配置: 534 | 535 | ```yaml 536 | path.data: data 537 | path.logs: log 538 | 539 | entry: 540 | - name: my_es_entry 541 | enabled: true 542 | router: my_router 543 | max_concurrency: 200000 544 | network: 545 | binding: $[[env.LR_GATEWAY_HOST]] 546 | 547 | flow: 548 | - name: hello_world 549 | filter: 550 | - echo: 551 | message: "hello world" 552 | router: 553 | - name: my_router 554 | default_flow: hello_world 555 | ``` 556 | 557 | `loadgen.dsl` 参考配置: 558 | 559 | ``` 560 | # runner: { 561 | # total_rounds: 1, 562 | # no_warm: true, 563 | # log_requests: true, 564 | # assert_invalid: true, 565 | # assert_error: true, 566 | # }, 567 | 568 | GET http://$[[env.LR_GATEWAY_HOST]]/ 569 | # assert: { 570 | # _ctx.response: { 571 | # status: 200, 572 | # body: "hello world", 573 | # }, 574 | # }, 575 | ``` 576 | 577 | ### 测试套件运行 578 | 579 | 配置好测试 `loadgen.yml` 后,可以通过以下命令运行 Loadgen: 580 | 581 | ```bash 582 | loadgen -config loadgen.yml 583 | ``` 584 | 585 | Loadgen 会运行配置指定的所有测试用例,并输出测试结果: 586 | 587 | ```text 588 | $ loadgen -config loadgen.yml 589 | __ ___ _ ___ ___ __ __ 590 | / / /___\/_\ / \/ _ \ /__\/\ \ \ 591 | / / // ///_\\ / /\ / /_\//_\ / \/ / 592 | / /__/ \_// _ \/ /_// /_\\//__/ /\ / 593 | \____|___/\_/ \_/___,'\____/\__/\_\ \/ 594 | 595 | [LOADGEN] A http load generator and testing suit. 596 | [LOADGEN] 1.0.0_SNAPSHOT, 83f2cb9, Sun Jul 4 13:52:42 2021 +0800, medcl, support single item in dict files 597 | [02-21 10:50:05] [INF] [app.go:192] initializing loadgen 598 | [02-21 10:50:05] [INF] [app.go:193] using config: /Users/kassian/Workspace/infini/src/infini.sh/testing/suites/dev.yml 599 | [02-21 10:50:05] [INF] [instance.go:78] workspace: /Users/kassian/Workspace/infini/src/infini.sh/testing/data/loadgen/nodes/cfpihf15k34iqhpd4d00 600 | [02-21 10:50:05] [INF] [app.go:399] loadgen is up and running now. 601 | [2023-02-21 10:50:05][TEST][SUCCESS] [setup/loadgen/cases/dummy] duration: 105(ms) 602 | 603 | 1 requests in 68.373875ms, 0.00bytes sent, 0.00bytes received 604 | 605 | [Loadgen Client Metrics] 606 | Requests/sec: 0.20 607 | Request Traffic/sec: 0.00bytes 608 | Total Transfer/sec: 0.00bytes 609 | Avg Req Time: 5s 610 | Fastest Request: 68.373875ms 611 | Slowest Request: 68.373875ms 612 | Number of Errors: 0 613 | Number of Invalid: 0 614 | Status 200: 1 615 | 616 | [Estimated Server Metrics] 617 | Requests/sec: 14.63 618 | Transfer/sec: 0.00bytes 619 | Avg Req Time: 68.373875ms 620 | 621 | 622 | [2023-02-21 10:50:06][TEST][FAILED] [setup/gateway/cases/echo/echo_with_context/] duration: 1274(ms) 623 | #0 request, GET http://$[[env.LR_GATEWAY_HOST]]/any/, assertion failed, skiping subsequent requests 624 | 1 requests in 1.255678s, 0.00bytes sent, 0.00bytes received 625 | 626 | [Loadgen Client Metrics] 627 | Requests/sec: 0.20 628 | Request Traffic/sec: 0.00bytes 629 | Total Transfer/sec: 0.00bytes 630 | Avg Req Time: 5s 631 | Fastest Request: 1.255678s 632 | Slowest Request: 1.255678s 633 | Number of Errors: 1 634 | Number of Invalid: 1 635 | Status 0: 1 636 | 637 | [Estimated Server Metrics] 638 | Requests/sec: 0.80 639 | Transfer/sec: 0.00bytes 640 | Avg Req Time: 1.255678s 641 | 642 | ``` 643 | -------------------------------------------------------------------------------- /docs/content.zh/docs/getting-started/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 10 3 | title: 下载安装 4 | asciinema: true 5 | --- 6 | 7 | # 安装 INFINI Loadgen 8 | 9 | INFINI Loadgen 支持主流的操作系统和平台,程序包很小,没有任何额外的外部依赖,安装起来应该是很快的 :) 10 | 11 | ## 下载安装 12 | 13 | **自动安装** 14 | 15 | ```bash 16 | curl -sSL http://get.infini.cloud | bash -s -- -p loadgen 17 | ``` 18 | 19 | 通过以上脚本可自动下载相应平台的 loadgen 最新版本并解压到/opt/loadgen 20 | 21 | 脚本的可选参数如下: 22 | 23 | > _-v [版本号](默认采用最新版本号)_ 24 | > _-d [安装目录](默认安装到/opt/loadgen)_ 25 | 26 | ```bash 27 | ➜ /tmp mkdir loadgen 28 | ➜ /tmp curl -sSL http://get.infini.cloud | bash -s -- -p loadgen -d /tmp/loadgen 29 | 30 | @@@@@@@@@@@ 31 | @@@@@@@@@@@@ 32 | @@@@@@@@@@@@ 33 | @@@@@@@@@&@@@ 34 | #@@@@@@@@@@@@@ 35 | @@@ @@@@@@@@@@@@@ 36 | &@@@@@@@ &@@@@@@@@@@@@@ 37 | @&@@@@@@@&@ @@@&@@@@@@@&@ 38 | @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ 39 | @@@@@@@@@@@@@@@@@@& @@@@@@@@@@@@@ 40 | %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 41 | @@@@@@@@@@@@&@@@@@@@@@@@@@@@ 42 | @@ ,@@@@@@@@@@@@@@@@@@@@@@@& 43 | @@@@@. @@@@@&@@@@@@@@@@@@@@ 44 | @@@@@@@@@@ @@@@@@@@@@@@@@@# 45 | @&@@@&@@@&@@@ &@&@@@&@@@&@ 46 | @@@@@@@@@@@@@. @@@@@@@* 47 | @@@@@@@@@@@@@ %@@@ 48 | @@@@@@@@@@@@@ 49 | /@@@@@@@&@@@@@ 50 | @@@@@@@@@@@@@ 51 | @@@@@@@@@@@@@ 52 | @@@@@@@@@@@@ Welcome to INFINI Labs! 53 | 54 | 55 | Now attempting the installation... 56 | 57 | Name: [loadgen], Version: [1.26.1-598], Path: [/tmp/loadgen] 58 | File: [https://release.infinilabs.com/loadgen/stable/loadgen-1.26.1-598-mac-arm64.zip] 59 | ##=O#- # 60 | 61 | Installation complete. [loadgen] is ready to use! 62 | 63 | 64 | ---------------------------------------------------------------- 65 | cd /tmp/loadgen && ./loadgen-mac-arm64 66 | ---------------------------------------------------------------- 67 | 68 | 69 | __ _ __ ____ __ _ __ __ 70 | / // |/ // __// // |/ // / 71 | / // || // _/ / // || // / 72 | /_//_/|_//_/ /_//_/|_//_/ 73 | 74 | ©INFINI.LTD, All Rights Reserved. 75 | ``` 76 | 77 | **手动安装** 78 | 79 | 根据您所在的操作系统和平台选择下面相应的下载地址: 80 | 81 | [https://release.infinilabs.com/loadgen/](https://release.infinilabs.com/loadgen/) 82 | -------------------------------------------------------------------------------- /docs/content.zh/docs/release-notes/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 80 3 | title: "版本历史" 4 | --- 5 | 6 | # 版本发布日志 7 | 8 | 这里是`loadgen`历史版本发布的相关说明。 9 | 10 | ## Latest (In development) 11 | ### ❌ Breaking changes 12 | ### 🚀 Features 13 | ### 🐛 Bug fix 14 | ### ✈️ Improvements 15 | 16 | ## 1.29.4 (2025-05-16) 17 | ### ❌ Breaking changes 18 | ### 🚀 Features 19 | ### 🐛 Bug fix 20 | ### ✈️ Improvements 21 | - 同步更新 [Framework v1.1.7](https://docs.infinilabs.com/framework/v1.1.7/docs/references/http_client/) 修复的一些已知问题 22 | 23 | ## 1.29.3 (2025-04-27) 24 | - 同步更新 [Framework v1.1.6](https://docs.infinilabs.com/framework/v1.1.6/docs/references/http_client/) 修复的一些已知问题 25 | 26 | ## 1.29.2 (2025-03-31) 27 | - 同步更新 [Framework v1.1.5](https://docs.infinilabs.com/framework/v1.1.5/docs/references/http_client/) 修复的一些已知问题 28 | 29 | 30 | ## 1.29.1 (2025-03-14) 31 | - 同步更新 [Framework v1.1.4](https://docs.infinilabs.com/framework/v1.1.4/docs/references/http_client/) 修复的一些已知问题 32 | 33 | 34 | ## 1.29.0 (2025-02-28) 35 | 36 | - 同步更新 Framework 修复的一些已知问题 37 | 38 | ## 1.28.2 (2025-02-15) 39 | 40 | - 同步更新 Framework 修复的一些已知问题 41 | 42 | ## 1.28.1 (2025-01-24) 43 | 44 | - 同步更新 Framework 修复的一些已知问题 45 | 46 | ## 1.28.0 (2025-01-11) 47 | 48 | - 同步更新 Framework 修复的一些已知问题 49 | 50 | ## 1.27.0 (2024-12-13) 51 | 52 | ### Improvements 53 | 54 | - 代码开源,统一采用 Github [仓库](https://github.com/infinilabs/loadgen) 进行开发 55 | - 保持与 Console 相同版本 56 | - 同步更新 Framework 修复的已知问题 57 | 58 | ### Bug fix 59 | 60 | - 修复 API 接口测试逻辑异常问题 61 | 62 | ## 1.26.1 (2024-08-13) 63 | 64 | ### Improvements 65 | 66 | - 与 INFINI Console 统一版本号 67 | - 同步更新 Framework 修复的已知问题 68 | 69 | ## 1.26.0 (2024-06-07) 70 | 71 | ### Improvements 72 | 73 | - 与 INFINI Console 统一版本号 74 | - 同步更新 Framework 修复的已知问题 75 | 76 | ## 1.25.0 (2024-04-30) 77 | 78 | ### Improvements 79 | 80 | - 与 INFINI Console 统一版本号 81 | - 同步更新 Framework 修复的已知问题 82 | 83 | ## 1.24.0 (2024-04-15) 84 | 85 | ### Improvements 86 | 87 | - 与 INFINI Console 统一版本号 88 | - 同步更新 Framework 修复的已知问题 89 | 90 | ## 1.22.0 (2024-01-26) 91 | 92 | ### Improvements 93 | 94 | - 与 INFINI Console 统一版本号 95 | 96 | ## 1.8.0 (2023-11-02) 97 | 98 | ### Breaking changes 99 | 100 | - 原 Loadrun 功能并入 Loadgen 101 | - 测试请求、断言等使用新的 Loadgen DSL 语法来配置 102 | 103 | ## 1.7.0 (2023-04-20) 104 | 105 | ### Breaking changes 106 | 107 | - `variables` 不再允许定义相同 `name` 的变量。 108 | 109 | ### Features 110 | 111 | - 增加 `log_status_code` 配置,支持打印特定状态码的请求日志。 112 | 113 | ## 1.6.0 (2023-04-06) 114 | 115 | ### Breaking ghanges 116 | 117 | - `file` 类型变量默认不再转义 `"` `\` 字符,使用 `replace` 功能手动设置变量转义。 118 | 119 | ### Features 120 | 121 | - 变量定义增加 `replace` 选项,可选跳过 `"` `\` 转义。 122 | 123 | ### Improvements 124 | 125 | - 优化内存占用 126 | 127 | ### Bug fix 128 | 129 | - 修复 YAML 字符串无法使用 `\n` 的问题 130 | - 修复无效的 assert 配置被忽略的问题 131 | 132 | ## 1.5.1 133 | 134 | ### Bug fix 135 | 136 | - 修复配置文件中无效的变量语法。 137 | 138 | ## 1.5.0 139 | 140 | ### Features 141 | 142 | - 配置文件添加 `assert` 配置,支持验证访问数据。 143 | - 配置文件添加 `register` 配置,支持注册动态变量。 144 | - 配置文件添加 `env` 配置,支持加载使用环境变量。 145 | - 支持在 `headers` 配置中使用动态变量。 146 | 147 | ### Improvements 148 | 149 | - 启动参数添加 `-l`: 控制发送请求的数量。 150 | - 配置文件添加 `runner.no_warm` 参数跳过预热阶段。 151 | 152 | ### Bug fix 153 | -------------------------------------------------------------------------------- /docs/content.zh/docs/resources/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 100 3 | title: "其它资源" 4 | --- 5 | 6 | # 其它资源 7 | 8 | 这里是一些和 Loadgen 有关的外部有用资源。 9 | 10 | ## 文章 11 | 12 | - [Elasticsearch 性能测试工具 Loadgen 之 004——高级用法示例](https://mp.weixin.qq.com/s/LhNjooomUK16mTJPMF3kLw) | 2025-01-23 13 | - [Elasticsearch 性能测试工具 Loadgen 之 003——断言模式详解](https://mp.weixin.qq.com/s/-ILvyvAfw61mcQAZUzJRMQ) | 2025-01-23 14 | - [Elasticsearch 性能测试工具 Loadgen 之 002——命令行及参数详解](https://mp.weixin.qq.com/s/M4oE8F2ND63RlL7rAySliA) | 2025-01-23 15 | - [Elasticsearch 性能测试工具 Loadgen 之 001——部署及应用详解](https://mp.weixin.qq.com/s/q3XM6AeMQrTEcWgputABRw) | 2025-01-23 16 | - [Elasticsearch 性能测试工具全解析](https://mp.weixin.qq.com/s/8EpeGzmhwvOqwJv8oL1g-w) | 2025-01-23 17 | - [Loadgen 压测: Elasticsearch VS Easysearch 性能测试](https://infinilabs.cn/blog/2024/elasticsearch-vs-easysearch-stress-testing/) | 2024-12-19 18 | - [借助 DSL 来简化 Loadgen 配置](https://infinilabs.cn/blog/2023/simplify-loadgen-config-with-dsl/) | 2023-10-25 19 | - [如何使用 Loadgen 来简化 HTTP API 请求的集成测试](https://infinilabs.cn/blog/2023/integration-testing-with-loadgen/) | 2023-10-20 20 | - [更多文章 👉](https://infinilabs.cn/blog/) 21 | 22 | ## 视频 23 | 24 | - [Elasticsearch 压测用什么工具](https://www.bilibili.com/video/BV18Z421h7qi/) | 2024-03-15 25 | - [轻量、无依赖的 Elasticsearch 压测工具- INFINI Loadgen 使用教程](https://www.bilibili.com/video/BV16V4y1U7rZ) | 2023-06-06 26 | -------------------------------------------------------------------------------- /docs/content.zh/menu/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | headless: true 3 | --- 4 | 5 | - [**文档**]({{< relref "/docs/" >}}) 6 |
7 | -------------------------------------------------------------------------------- /docs/static/img/logo-en.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/static/img/logo-zh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /domain.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) INFINI Labs & INFINI LIMITED. 2 | // 3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0 4 | // and as commercial software. 5 | // 6 | // For commercial licensing, contact us at: 7 | // - Website: infinilabs.com 8 | // - Email: hello@infini.ltd 9 | // 10 | // Open Source licensed under AGPL V3: 11 | // This program is free software: you can redistribute it and/or modify 12 | // it under the terms of the GNU Affero General Public License as published by 13 | // the Free Software Foundation, either version 3 of the License, or 14 | // (at your option) any later version. 15 | // 16 | // This program is distributed in the hope that it will be useful, 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | // GNU Affero General Public License for more details. 20 | // 21 | // You should have received a copy of the GNU Affero General Public License 22 | // along with this program. If not, see . 23 | 24 | /* Copyright © INFINI Ltd. All rights reserved. 25 | * web: https://infinilabs.com 26 | * mail: hello#infini.ltd */ 27 | 28 | package main 29 | 30 | import ( 31 | "bytes" 32 | "encoding/base64" 33 | "fmt" 34 | "infini.sh/framework/core/model" 35 | "math/rand" 36 | "strings" 37 | "time" 38 | 39 | "github.com/RoaringBitmap/roaring" 40 | log "github.com/cihub/seelog" 41 | "infini.sh/framework/lib/fasttemplate" 42 | 43 | "infini.sh/framework/core/conditions" 44 | "infini.sh/framework/core/errors" 45 | "infini.sh/framework/core/util" 46 | "infini.sh/framework/lib/fasthttp" 47 | ) 48 | 49 | type valuesMap map[string]interface{} 50 | 51 | func (m valuesMap) GetValue(key string) (interface{}, error) { 52 | v, ok := m[key] 53 | if !ok { 54 | return nil, errors.New("key not found") 55 | } 56 | return v, nil 57 | } 58 | 59 | type Request struct { 60 | Method string `config:"method"` 61 | Url string `config:"url"` 62 | Body string `config:"body"` 63 | SimpleMode bool `config:"simple_mode"` 64 | 65 | RepeatBodyNTimes int `config:"body_repeat_times"` 66 | Headers []map[string]string `config:"headers"` 67 | BasicAuth *model.BasicAuth `config:"basic_auth"` 68 | 69 | // Disable fasthttp client's header names normalizing, preserve original header key, for requests 70 | DisableHeaderNamesNormalizing bool `config:"disable_header_names_normalizing"` 71 | 72 | RuntimeVariables map[string]string `config:"runtime_variables"` 73 | RuntimeBodyLineVariables map[string]string `config:"runtime_body_line_variables"` 74 | 75 | ExecuteRepeatTimes int `config:"execute_repeat_times"` 76 | 77 | urlHasTemplate bool 78 | headerHasTemplate bool 79 | bodyHasTemplate bool 80 | 81 | headerTemplates map[string]*fasttemplate.Template 82 | urlTemplate *fasttemplate.Template 83 | bodyTemplate *fasttemplate.Template 84 | } 85 | 86 | func (req *Request) HasVariable() bool { 87 | return req.urlHasTemplate || req.bodyHasTemplate || len(req.headerTemplates) > 0 88 | } 89 | 90 | type Variable struct { 91 | Type string `config:"type"` 92 | Name string `config:"name"` 93 | Path string `config:"path"` 94 | Data []string `config:"data"` 95 | Format string `config:"format"` 96 | 97 | //type: range 98 | From uint64 `config:"from"` 99 | To uint64 `config:"to"` 100 | 101 | Replace map[string]string `config:"replace"` 102 | 103 | Size int `config:"size"` 104 | 105 | //type: random_int_array 106 | RandomArrayKey string `config:"variable_key"` 107 | RandomArrayType string `config:"variable_type"` 108 | RandomSquareBracketChar bool `config:"square_bracket"` 109 | RandomStringBracketChar string `config:"string_bracket"` 110 | 111 | replacer *strings.Replacer 112 | } 113 | 114 | type AppConfig struct { 115 | Environments map[string]string `config:"env"` 116 | Tests []Test `config:"tests"` 117 | LoaderConfig 118 | } 119 | 120 | type LoaderConfig struct { 121 | // Access order: runtime_variables -> register -> variables 122 | Variable []Variable `config:"variables"` 123 | Requests []RequestItem `config:"requests"` 124 | RunnerConfig RunnerConfig `config:"runner"` 125 | } 126 | 127 | type RunnerConfig struct { 128 | // How many rounds of `requests` to run 129 | TotalRounds int `config:"total_rounds"` 130 | // Skip warming up round 131 | NoWarm bool `config:"no_warm"` 132 | 133 | ValidStatusCodesDuringWarmup []int `config:"valid_status_codes_during_warmup"` 134 | 135 | // Exit(1) if any assert failed 136 | AssertInvalid bool `config:"assert_invalid"` 137 | 138 | ContinueOnAssertInvalid bool `config:"continue_on_assert_invalid"` 139 | SkipInvalidAssert bool `config:"skip_invalid_assert"` 140 | 141 | // Exit(2) if any error occurred 142 | AssertError bool `config:"assert_error"` 143 | // Print the request sent to server 144 | LogRequests bool `config:"log_requests"` 145 | 146 | BenchmarkOnly bool `config:"benchmark_only"` 147 | DurationInUs bool `config:"duration_in_us"` 148 | NoStats bool `config:"no_stats"` 149 | NoSizeStats bool `config:"no_size_stats"` 150 | MetricSampleSize int `config:"metric_sample_size"` 151 | 152 | // Print the request sent to server if status code matched 153 | LogStatusCodes []int `config:"log_status_codes"` 154 | // Disable fasthttp client's header names normalizing, preserve original header key, for responses 155 | DisableHeaderNamesNormalizing bool `config:"disable_header_names_normalizing"` 156 | 157 | // Whether to reset the context, including variables, runtime KV pairs, etc., 158 | // before this test run. 159 | ResetContext bool `config:"reset_context"` 160 | 161 | // Default endpoint if not specified in a request 162 | DefaultEndpoint string `config:"default_endpoint"` 163 | DefaultBasicAuth *model.BasicAuth `config:"default_basic_auth"` 164 | defaultEndpoint *fasthttp.URI 165 | } 166 | 167 | func (config *RunnerConfig) parseDefaultEndpoint() (*fasthttp.URI, error) { 168 | if config.defaultEndpoint != nil { 169 | return config.defaultEndpoint, nil 170 | } 171 | 172 | if config.DefaultEndpoint != "" { 173 | uri := &fasthttp.URI{} 174 | err := uri.Parse(nil, []byte(config.DefaultEndpoint)) 175 | if err != nil { 176 | return nil, err 177 | } 178 | config.defaultEndpoint = uri 179 | return config.defaultEndpoint, err 180 | } 181 | 182 | return config.defaultEndpoint, errors.New("no valid default endpoint") 183 | } 184 | 185 | /* 186 | A test case is a standalone directory containing the following configs: 187 | - gateway.yml: The configuration to start the gateway server 188 | - loadgen.yml: The configuration to define the test cases 189 | */ 190 | type Test struct { 191 | // The directory of the test configurations 192 | Path string `config:"path"` 193 | // Whether to use --compress for loadgen 194 | Compress bool `config:"compress"` 195 | } 196 | 197 | const ( 198 | // Gateway-related configurations 199 | env_LR_GATEWAY_CMD = "LR_GATEWAY_CMD" 200 | env_LR_GATEWAY_HOST = "LR_GATEWAY_HOST" 201 | env_LR_GATEWAY_API_HOST = "LR_GATEWAY_API_HOST" 202 | ) 203 | 204 | var ( 205 | dict = map[string][]string{} 206 | variables map[string]Variable 207 | ) 208 | 209 | func (config *AppConfig) Init() { 210 | 211 | } 212 | 213 | func (config *AppConfig) testEnv(envVars ...string) bool { 214 | for _, envVar := range envVars { 215 | if v, ok := config.Environments[envVar]; !ok || v == "" { 216 | return false 217 | } 218 | } 219 | return true 220 | } 221 | 222 | func (config *LoaderConfig) Init() error { 223 | // As we do not allow duplicate variable definitions, it is necessary to clear 224 | // any previously defined variables. 225 | variables = map[string]Variable{} 226 | if config.RunnerConfig.ResetContext { 227 | dict = map[string][]string{} 228 | util.ClearAllID() 229 | } 230 | 231 | for _, i := range config.Variable { 232 | i.Name = util.TrimSpaces(i.Name) 233 | _, ok := variables[i.Name] 234 | if ok { 235 | return fmt.Errorf("variable [%s] defined twice", i.Name) 236 | } 237 | var lines []string 238 | if len(i.Path) > 0 { 239 | lines = util.FileGetLines(i.Path) 240 | log.Debugf("path:%v, num of lines:%v", i.Path, len(lines)) 241 | } 242 | 243 | if len(i.Data) > 0 { 244 | for _, v := range i.Data { 245 | v = util.TrimSpaces(v) 246 | if len(v) > 0 { 247 | lines = append(lines, v) 248 | } 249 | } 250 | } 251 | 252 | if len(i.Replace) > 0 { 253 | var replaces []string 254 | 255 | for k, v := range i.Replace { 256 | replaces = append(replaces, k, v) 257 | } 258 | i.replacer = strings.NewReplacer(replaces...) 259 | } 260 | 261 | dict[i.Name] = lines 262 | 263 | variables[i.Name] = i 264 | } 265 | 266 | var err error 267 | for _, v := range config.Requests { 268 | if v.Request == nil { 269 | continue 270 | } 271 | v.Request.headerTemplates = map[string]*fasttemplate.Template{} 272 | if util.ContainStr(v.Request.Url, "$[[") { 273 | v.Request.urlHasTemplate = true 274 | v.Request.urlTemplate, err = fasttemplate.NewTemplate(v.Request.Url, "$[[", "]]") 275 | if err != nil { 276 | return err 277 | } 278 | } 279 | 280 | if v.Request.RepeatBodyNTimes <= 0 && len(v.Request.Body) > 0 { 281 | v.Request.RepeatBodyNTimes = 1 282 | } 283 | 284 | if util.ContainStr(v.Request.Body, "$") { 285 | v.Request.bodyHasTemplate = true 286 | v.Request.bodyTemplate, err = fasttemplate.NewTemplate(v.Request.Body, "$[[", "]]") 287 | if err != nil { 288 | return err 289 | } 290 | } 291 | 292 | for _, headers := range v.Request.Headers { 293 | for headerK, headerV := range headers { 294 | if util.ContainStr(headerV, "$") { 295 | v.Request.headerHasTemplate = true 296 | v.Request.headerTemplates[headerK], err = fasttemplate.NewTemplate(headerV, "$[[", "]]") 297 | if err != nil { 298 | return err 299 | } 300 | } 301 | } 302 | } 303 | 304 | ////if there is no $[[ in the request, then we can assume that the request is in simple mode 305 | //if !v.Request.urlHasTemplate && !v.Request.bodyHasTemplate&& !v.Request.headerHasTemplate { 306 | // v.Request.SimpleMode = true 307 | //} 308 | } 309 | 310 | return nil 311 | } 312 | 313 | // "2021-08-23T11:13:36.274" 314 | const TsLayout = "2006-01-02T15:04:05.000" 315 | 316 | func GetVariable(runtimeKV util.MapStr, key string) string { 317 | 318 | if runtimeKV != nil { 319 | x, err := runtimeKV.GetValue(key) 320 | if err == nil { 321 | return util.ToString(x) 322 | } 323 | } 324 | 325 | return getVariable(key) 326 | } 327 | 328 | func getVariable(key string) string { 329 | x, ok := variables[key] 330 | if !ok { 331 | return "not_found" 332 | } 333 | 334 | rawValue := buildVariableValue(x) 335 | if x.replacer == nil { 336 | return rawValue 337 | } 338 | return x.replacer.Replace(rawValue) 339 | } 340 | 341 | func buildVariableValue(x Variable) string { 342 | switch x.Type { 343 | case "sequence": 344 | return util.ToString(util.GetAutoIncrement32ID(x.Name, uint32(x.From), uint32(x.To)).Increment()) 345 | case "sequence64": 346 | return util.ToString(util.GetAutoIncrement64ID(x.Name, x.From, x.To).Increment64()) 347 | case "uuid": 348 | return util.GetUUID() 349 | case "now_local": 350 | return time.Now().Local().String() 351 | case "now_with_format": 352 | if x.Format == "" { 353 | panic(errors.Errorf("date format is not set, [%v]", x)) 354 | } 355 | return time.Now().Format(x.Format) 356 | case "now_utc": 357 | return time.Now().UTC().String() 358 | case "now_utc_lite": 359 | return time.Now().UTC().Format(TsLayout) 360 | case "now_unix": 361 | return util.IntToString(int(time.Now().Local().Unix())) 362 | case "now_unix_in_ms": 363 | return util.IntToString(int(time.Now().Local().UnixMilli())) 364 | case "now_unix_in_micro": 365 | return util.IntToString(int(time.Now().Local().UnixMicro())) 366 | case "now_unix_in_nano": 367 | return util.IntToString(int(time.Now().Local().UnixNano())) 368 | case "int_array_bitmap": 369 | rb3 := roaring.New() 370 | if x.Size > 0 { 371 | for y := 0; y < x.Size; y++ { 372 | v := rand.Intn(int(x.To-x.From+1)) + int(x.From) 373 | rb3.Add(uint32(v)) 374 | } 375 | } 376 | buf := new(bytes.Buffer) 377 | rb3.WriteTo(buf) 378 | return base64.StdEncoding.EncodeToString(buf.Bytes()) 379 | case "range": 380 | return util.IntToString(rand.Intn(int(x.To-x.From+1)) + int(x.From)) 381 | case "random_array": 382 | str := bytes.Buffer{} 383 | 384 | if x.RandomSquareBracketChar { 385 | str.WriteString("[") 386 | } 387 | 388 | if x.RandomArrayKey != "" { 389 | if x.Size > 0 { 390 | for y := 0; y < x.Size; y++ { 391 | if x.RandomSquareBracketChar && str.Len() > 1 || (!x.RandomSquareBracketChar && str.Len() > 0) { 392 | str.WriteString(",") 393 | } 394 | 395 | v := getVariable(x.RandomArrayKey) 396 | 397 | //left " 398 | if x.RandomArrayType == "string" { 399 | if x.RandomStringBracketChar != "" { 400 | str.WriteString(x.RandomStringBracketChar) 401 | } else { 402 | str.WriteString("\"") 403 | } 404 | } 405 | 406 | str.WriteString(v) 407 | 408 | // right " 409 | if x.RandomArrayType == "string" { 410 | if x.RandomStringBracketChar != "" { 411 | str.WriteString(x.RandomStringBracketChar) 412 | } else { 413 | str.WriteString("\"") 414 | } 415 | } 416 | } 417 | } 418 | } 419 | 420 | if x.RandomSquareBracketChar { 421 | str.WriteString("]") 422 | } 423 | return str.String() 424 | case "file", "list": 425 | d, ok := dict[x.Name] 426 | if ok { 427 | 428 | if len(d) == 1 { 429 | return d[0] 430 | } 431 | offset := rand.Intn(len(d)) 432 | return d[offset] 433 | } 434 | } 435 | return "invalid_variable_type" 436 | } 437 | 438 | type RequestItem struct { 439 | Request *Request `config:"request"` 440 | // TODO: mask invalid gateway fields 441 | Assert *conditions.Config `config:"assert"` 442 | AssertDsl string `config:"assert_dsl"` 443 | Sleep *SleepAction `config:"sleep"` 444 | // Populate global context with `_ctx` values 445 | Register []map[string]string `config:"register"` 446 | } 447 | 448 | type SleepAction struct { 449 | SleepInMilliSeconds int64 `config:"sleep_in_milli_seconds"` 450 | } 451 | 452 | type RequestResult struct { 453 | RequestCount int 454 | RequestSize int 455 | ResponseSize int 456 | Status int 457 | Error bool 458 | Invalid bool 459 | Duration time.Duration 460 | } 461 | 462 | func (result *RequestResult) Reset() { 463 | result.Error = false 464 | result.Status = 0 465 | result.RequestCount = 0 466 | result.RequestSize = 0 467 | result.ResponseSize = 0 468 | result.Invalid = false 469 | result.Duration = 0 470 | } 471 | -------------------------------------------------------------------------------- /domain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) INFINI Labs & INFINI LIMITED. 2 | // 3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0 4 | // and as commercial software. 5 | // 6 | // For commercial licensing, contact us at: 7 | // - Website: infinilabs.com 8 | // - Email: hello@infini.ltd 9 | // 10 | // Open Source licensed under AGPL V3: 11 | // This program is free software: you can redistribute it and/or modify 12 | // it under the terms of the GNU Affero General Public License as published by 13 | // the Free Software Foundation, either version 3 of the License, or 14 | // (at your option) any later version. 15 | // 16 | // This program is distributed in the hope that it will be useful, 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | // GNU Affero General Public License for more details. 20 | // 21 | // You should have received a copy of the GNU Affero General Public License 22 | // along with this program. If not, see . 23 | 24 | package main 25 | 26 | import ( 27 | "fmt" 28 | "infini.sh/framework/lib/fasttemplate" 29 | "io" 30 | "log" 31 | "math/rand" 32 | "testing" 33 | ) 34 | 35 | func TestVariable(t *testing.T) { 36 | array := []string{"1", "2", "3"} 37 | 38 | for i := 0; i < 100; i++ { 39 | offset := rand.Intn(len(array)) 40 | fmt.Println(offset) 41 | } 42 | } 43 | 44 | func TestTemplate(t1 *testing.T) { 45 | template := "Hello, $[[user]]! You won $[[prize]]!!! $[[foobar]]" 46 | t, err := fasttemplate.NewTemplate(template, "$[[", "]]") 47 | if err != nil { 48 | log.Fatalf("unexpected error when parsing template: %s", err) 49 | } 50 | s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { 51 | switch tag { 52 | case "user": 53 | return w.Write([]byte("John")) 54 | case "prize": 55 | return w.Write([]byte("$100500")) 56 | default: 57 | 58 | return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag))) 59 | } 60 | }) 61 | fmt.Printf("%s", s) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /loader.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) INFINI Labs & INFINI LIMITED. 2 | // 3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0 4 | // and as commercial software. 5 | // 6 | // For commercial licensing, contact us at: 7 | // - Website: infinilabs.com 8 | // - Email: hello@infini.ltd 9 | // 10 | // Open Source licensed under AGPL V3: 11 | // This program is free software: you can redistribute it and/or modify 12 | // it under the terms of the GNU Affero General Public License as published by 13 | // the Free Software Foundation, either version 3 of the License, or 14 | // (at your option) any later version. 15 | // 16 | // This program is distributed in the hope that it will be useful, 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | // GNU Affero General Public License for more details. 20 | // 21 | // You should have received a copy of the GNU Affero General Public License 22 | // along with this program. If not, see . 23 | 24 | /* Copyright © INFINI Ltd. All rights reserved. 25 | * web: https://infinilabs.com 26 | * mail: hello#infini.ltd */ 27 | 28 | package main 29 | 30 | import ( 31 | "bufio" 32 | "compress/gzip" 33 | "crypto/tls" 34 | "encoding/json" 35 | "io" 36 | "net" 37 | "os" 38 | "strconv" 39 | "sync" 40 | "sync/atomic" 41 | "time" 42 | 43 | "github.com/jamiealquiza/tachymeter" 44 | 45 | log "github.com/cihub/seelog" 46 | "infini.sh/framework/core/conditions" 47 | "infini.sh/framework/core/global" 48 | "infini.sh/framework/core/rate" 49 | "infini.sh/framework/core/stats" 50 | "infini.sh/framework/core/util" 51 | "infini.sh/framework/lib/fasthttp" 52 | ) 53 | 54 | type LoadGenerator struct { 55 | duration int //seconds 56 | goroutines int 57 | statsAggregator chan *LoadStats 58 | interrupted int32 59 | } 60 | 61 | type LoadStats struct { 62 | TotReqSize int64 63 | TotRespSize int64 64 | TotDuration time.Duration 65 | MinRequestTime time.Duration 66 | MaxRequestTime time.Duration 67 | NumRequests int 68 | NumErrs int 69 | NumAssertInvalid int 70 | NumAssertSkipped int 71 | StatusCode map[int]int 72 | } 73 | 74 | var ( 75 | httpClient fasthttp.Client 76 | resultPool = &sync.Pool{ 77 | New: func() interface{} { 78 | return &RequestResult{} 79 | }, 80 | } 81 | ) 82 | 83 | func NewLoadGenerator(duration int, goroutines int, statsAggregator chan *LoadStats, disableHeaderNamesNormalizing bool) (rt *LoadGenerator) { 84 | if readTimeout <= 0 { 85 | readTimeout = timeout 86 | } 87 | if writeTimeout <= 0 { 88 | writeTimeout = timeout 89 | } 90 | if dialTimeout <= 0 { 91 | dialTimeout = timeout 92 | } 93 | 94 | httpClient = fasthttp.Client{ 95 | MaxConnsPerHost: goroutines, 96 | //MaxConns: goroutines, 97 | NoDefaultUserAgentHeader: false, 98 | DisableHeaderNamesNormalizing: disableHeaderNamesNormalizing, 99 | Name: global.Env().GetAppLowercaseName() + "/" + global.Env().GetVersion() + "/" + global.Env().GetBuildNumber(), 100 | TLSConfig: &tls.Config{InsecureSkipVerify: true}, 101 | } 102 | 103 | if readTimeout > 0 { 104 | httpClient.ReadTimeout = time.Second * time.Duration(readTimeout) 105 | } 106 | if writeTimeout > 0 { 107 | httpClient.WriteTimeout = time.Second * time.Duration(writeTimeout) 108 | } 109 | if dialTimeout > 0 { 110 | httpClient.Dial = func(addr string) (net.Conn, error) { 111 | return fasthttp.DialTimeout(addr, time.Duration(dialTimeout)*time.Second) 112 | } 113 | } 114 | 115 | rt = &LoadGenerator{duration, goroutines, statsAggregator, 0} 116 | return 117 | } 118 | 119 | var defaultHTTPPool = fasthttp.NewRequestResponsePool("default_http") 120 | 121 | func doRequest(config *LoaderConfig, globalCtx util.MapStr, req *fasthttp.Request, resp *fasthttp.Response, item *RequestItem, loadStats *LoadStats, timer *tachymeter.Tachymeter) (continueNext bool, err error) { 122 | 123 | if item.Request != nil { 124 | 125 | if item.Request.ExecuteRepeatTimes < 1 { 126 | item.Request.ExecuteRepeatTimes = 1 127 | } 128 | 129 | for i := 0; i < item.Request.ExecuteRepeatTimes; i++ { 130 | resp.Reset() 131 | resp.ResetBody() 132 | start := time.Now() 133 | 134 | if global.Env().IsDebug { 135 | log.Info(req.String()) 136 | } 137 | 138 | if timeout > 0 { 139 | err = httpClient.DoTimeout(req, resp, time.Duration(timeout)*time.Second) 140 | } else { 141 | err = httpClient.Do(req, resp) 142 | } 143 | 144 | if global.Env().IsDebug { 145 | log.Info(resp.String()) 146 | } 147 | 148 | duration := time.Since(start) 149 | statsCode := resp.StatusCode() 150 | 151 | if !config.RunnerConfig.BenchmarkOnly && timer != nil { 152 | timer.AddTime(duration) 153 | } 154 | 155 | if !config.RunnerConfig.NoStats { 156 | if config.RunnerConfig.DurationInUs { 157 | stats.Timing("request", "duration_in_us", duration.Microseconds()) 158 | } else { 159 | stats.Timing("request", "duration", duration.Milliseconds()) 160 | } 161 | 162 | stats.Increment("request", "total") 163 | stats.Increment("request", strconv.Itoa(resp.StatusCode())) 164 | 165 | if err != nil { 166 | loadStats.NumErrs++ 167 | loadStats.NumAssertInvalid++ 168 | } 169 | 170 | if !config.RunnerConfig.NoSizeStats { 171 | loadStats.TotReqSize += int64(req.GetRequestLength()) //TODO inaccurate 172 | loadStats.TotRespSize += int64(resp.GetResponseLength()) //TODO inaccurate 173 | } 174 | 175 | loadStats.NumRequests++ 176 | loadStats.TotDuration += duration 177 | loadStats.MaxRequestTime = util.MaxDuration(duration, loadStats.MaxRequestTime) 178 | loadStats.MinRequestTime = util.MinDuration(duration, loadStats.MinRequestTime) 179 | loadStats.StatusCode[statsCode] += 1 180 | } 181 | 182 | if config.RunnerConfig.BenchmarkOnly { 183 | return true, err 184 | } 185 | 186 | if item.Register != nil || item.Assert != nil || config.RunnerConfig.LogRequests { 187 | //only use last request and response 188 | reqBody := req.GetRawBody() 189 | respBody := resp.GetRawBody() 190 | if global.Env().IsDebug { 191 | log.Debugf("final response code: %v, body: %s", resp.StatusCode(), string(respBody)) 192 | } 193 | 194 | if item.Request != nil && config.RunnerConfig.LogRequests || util.ContainsInAnyInt32Array(statsCode, config.RunnerConfig.LogStatusCodes) { 195 | log.Infof("[%v] %v, %v - %v", item.Request.Method, item.Request.Url, item.Request.Headers, util.SubString(string(reqBody), 0, 512)) 196 | log.Infof("status: %v, error: %v, response: %v", statsCode, err, util.SubString(string(respBody), 0, 512)) 197 | } 198 | 199 | if err != nil { 200 | continue 201 | } 202 | 203 | event := buildCtx(resp, respBody, duration) 204 | if item.Register != nil { 205 | log.Debugf("registering %+v, event: %+v", item.Register, event) 206 | for _, item := range item.Register { 207 | for dest, src := range item { 208 | val, valErr := event.GetValue(src) 209 | if valErr != nil { 210 | log.Errorf("failed to get value with key: %s", src) 211 | } 212 | log.Debugf("put globalCtx %+v, %+v", dest, val) 213 | globalCtx.Put(dest, val) 214 | } 215 | } 216 | } 217 | 218 | if item.Assert != nil { 219 | // Dump globalCtx into assert event 220 | event.Update(globalCtx) 221 | if len(respBody) < 4096 { 222 | log.Debugf("assert _ctx: %+v", event) 223 | } 224 | condition, buildErr := conditions.NewCondition(item.Assert) 225 | if buildErr != nil { 226 | if config.RunnerConfig.SkipInvalidAssert { 227 | loadStats.NumAssertSkipped++ 228 | continue 229 | } 230 | log.Errorf("failed to build conditions while assert existed, error: %+v", buildErr) 231 | loadStats.NumAssertInvalid++ 232 | return 233 | } 234 | if !condition.Check(event) { 235 | loadStats.NumAssertInvalid++ 236 | if item.Request != nil { 237 | log.Errorf("%s %s, assertion failed, skipping subsequent requests", item.Request.Method, item.Request.Url) 238 | } 239 | 240 | if !config.RunnerConfig.ContinueOnAssertInvalid { 241 | log.Info("assertion failed, skipping subsequent requests,", util.MustToJSON(item.Assert), ", event:", util.MustToJSON(event)) 242 | return false, err 243 | } 244 | } 245 | } 246 | } 247 | 248 | if item.Sleep != nil { 249 | time.Sleep(time.Duration(item.Sleep.SleepInMilliSeconds) * time.Millisecond) 250 | } 251 | } 252 | } 253 | 254 | return true, nil 255 | } 256 | 257 | func buildCtx(resp *fasthttp.Response, respBody []byte, duration time.Duration) util.MapStr { 258 | var statusCode int 259 | header := map[string]interface{}{} 260 | if resp != nil { 261 | resp.Header.VisitAll(func(k, v []byte) { 262 | header[string(k)] = string(v) 263 | }) 264 | statusCode = resp.StatusCode() 265 | } 266 | event := util.MapStr{ 267 | "_ctx": map[string]interface{}{ 268 | "response": map[string]interface{}{ 269 | "status": statusCode, 270 | "header": header, 271 | "body": string(respBody), 272 | "body_length": len(respBody), 273 | }, 274 | "elapsed": int64(duration / time.Millisecond), 275 | }, 276 | } 277 | bodyJson := map[string]interface{}{} 278 | jsonErr := json.Unmarshal(respBody, &bodyJson) 279 | if jsonErr == nil { 280 | event.Put("_ctx.response.body_json", bodyJson) 281 | } 282 | return event 283 | } 284 | 285 | func (cfg *LoadGenerator) Run(config *LoaderConfig, countLimit int, timer *tachymeter.Tachymeter) { 286 | loadStats := &LoadStats{MinRequestTime: time.Millisecond, StatusCode: map[int]int{}} 287 | start := time.Now() 288 | 289 | limiter := rate.GetRateLimiter("loadgen", "requests", int(rateLimit), 1, time.Second*1) 290 | 291 | // TODO: support concurrent access 292 | globalCtx := util.MapStr{} 293 | req := defaultHTTPPool.AcquireRequest() 294 | defer defaultHTTPPool.ReleaseRequest(req) 295 | resp := defaultHTTPPool.AcquireResponse() 296 | defer defaultHTTPPool.ReleaseResponse(resp) 297 | 298 | totalRequests := 0 299 | totalRounds := 0 300 | 301 | for time.Since(start).Seconds() <= float64(cfg.duration) && atomic.LoadInt32(&cfg.interrupted) == 0 { 302 | if config.RunnerConfig.TotalRounds > 0 && totalRounds >= config.RunnerConfig.TotalRounds { 303 | goto END 304 | } 305 | totalRounds += 1 306 | 307 | for i, item := range config.Requests { 308 | 309 | if !config.RunnerConfig.BenchmarkOnly { 310 | if countLimit > 0 && totalRequests >= countLimit { 311 | goto END 312 | } 313 | totalRequests += 1 314 | 315 | if rateLimit > 0 { 316 | RetryRateLimit: 317 | if !limiter.Allow() { 318 | time.Sleep(10 * time.Millisecond) 319 | goto RetryRateLimit 320 | } 321 | } 322 | } 323 | 324 | item.prepareRequest(config, globalCtx, req) 325 | 326 | next, err := doRequest(config, globalCtx, req, resp, &item, loadStats, timer) 327 | if global.Env().IsDebug { 328 | log.Debugf("#%v,contine: %v, err:%v", i, next, err) 329 | } 330 | if !next { 331 | break 332 | } 333 | 334 | } 335 | } 336 | 337 | END: 338 | cfg.statsAggregator <- loadStats 339 | } 340 | 341 | func (v *RequestItem) prepareRequest(config *LoaderConfig, globalCtx util.MapStr, req *fasthttp.Request) { 342 | //cleanup 343 | req.Reset() 344 | req.ResetBody() 345 | 346 | if v.Request.BasicAuth != nil && v.Request.BasicAuth.Username != "" { 347 | req.SetBasicAuth(v.Request.BasicAuth.Username, v.Request.BasicAuth.Password.Get()) 348 | } else { 349 | //try use default auth 350 | if config.RunnerConfig.DefaultBasicAuth != nil && config.RunnerConfig.DefaultBasicAuth.Username != "" { 351 | req.SetBasicAuth(config.RunnerConfig.DefaultBasicAuth.Username, config.RunnerConfig.DefaultBasicAuth.Password.Get()) 352 | } 353 | } 354 | 355 | if v.Request.SimpleMode { 356 | req.Header.SetMethod(v.Request.Method) 357 | req.SetRequestURI(v.Request.Url) 358 | return 359 | } 360 | 361 | bodyBuffer := req.BodyBuffer() 362 | var bodyWriter io.Writer = bodyBuffer 363 | if v.Request.DisableHeaderNamesNormalizing { 364 | req.Header.DisableNormalizing() 365 | } 366 | 367 | if compress { 368 | var err error 369 | gzipWriter, err := gzip.NewWriterLevel(bodyBuffer, fasthttp.CompressBestCompression) 370 | if err != nil { 371 | panic("failed to create gzip writer") 372 | } 373 | defer gzipWriter.Close() 374 | bodyWriter = gzipWriter 375 | } 376 | 377 | //init runtime variables 378 | // TODO: optimize overall variable populate flow 379 | runtimeVariables := util.MapStr{} 380 | runtimeVariables.Update(globalCtx) 381 | 382 | if v.Request.HasVariable() { 383 | if len(v.Request.RuntimeVariables) > 0 { 384 | for k, v := range v.Request.RuntimeVariables { 385 | runtimeVariables.Put(k, GetVariable(runtimeVariables, v)) 386 | } 387 | } 388 | 389 | } 390 | 391 | //prepare url 392 | url := v.Request.Url 393 | if v.Request.urlHasTemplate { 394 | url = v.Request.urlTemplate.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { 395 | variable := GetVariable(runtimeVariables, tag) 396 | return w.Write(util.UnsafeStringToBytes(variable)) 397 | }) 398 | } 399 | 400 | //set default endpoint 401 | parsedUrl := fasthttp.URI{} 402 | err := parsedUrl.Parse(nil, []byte(url)) 403 | if err != nil { 404 | panic(err) 405 | } 406 | if parsedUrl.Host() == nil || len(parsedUrl.Host()) == 0 { 407 | path, err := config.RunnerConfig.parseDefaultEndpoint() 408 | //log.Infof("default endpoint: %v, %v",path,err) 409 | if err == nil { 410 | parsedUrl.SetSchemeBytes(path.Scheme()) 411 | parsedUrl.SetHostBytes(path.Host()) 412 | } 413 | } 414 | url = parsedUrl.String() 415 | 416 | req.SetRequestURI(url) 417 | 418 | if global.Env().IsDebug { 419 | log.Debugf("final request url: %v %s", v.Request.Method, url) 420 | } 421 | 422 | //prepare method 423 | req.Header.SetMethod(v.Request.Method) 424 | 425 | if len(v.Request.Headers) > 0 { 426 | for _, headers := range v.Request.Headers { 427 | for headerK, headerV := range headers { 428 | if tmpl, ok := v.Request.headerTemplates[headerK]; ok { 429 | headerV = tmpl.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { 430 | variable := GetVariable(runtimeVariables, tag) 431 | return w.Write(util.UnsafeStringToBytes(variable)) 432 | }) 433 | } 434 | req.Header.Set(headerK, headerV) 435 | } 436 | } 437 | } 438 | if global.Env().IsDebug { 439 | log.Debugf("final request headers: %s", req.Header.String()) 440 | } 441 | 442 | //req.Header.Set("User-Agent", UserAgent) 443 | 444 | //prepare request body 445 | for i := 0; i < v.Request.RepeatBodyNTimes; i++ { 446 | body := v.Request.Body 447 | if len(body) > 0 { 448 | if v.Request.bodyHasTemplate { 449 | if len(v.Request.RuntimeBodyLineVariables) > 0 { 450 | for k, v := range v.Request.RuntimeBodyLineVariables { 451 | runtimeVariables[k] = GetVariable(runtimeVariables, v) 452 | } 453 | } 454 | 455 | v.Request.bodyTemplate.ExecuteFuncStringExtend(bodyWriter, func(w io.Writer, tag string) (int, error) { 456 | variable := GetVariable(runtimeVariables, tag) 457 | return w.Write([]byte(variable)) 458 | }) 459 | } else { 460 | bodyWriter.Write(util.UnsafeStringToBytes(body)) 461 | } 462 | } 463 | } 464 | 465 | req.Header.Set("X-PayLoad-Size", util.ToString(bodyBuffer.Len())) 466 | 467 | if bodyBuffer.Len() > 0 && compress { 468 | req.Header.Set(fasthttp.HeaderAcceptEncoding, "gzip") 469 | req.Header.Set(fasthttp.HeaderContentEncoding, "gzip") 470 | req.Header.Set("X-PayLoad-Compressed", util.ToString(true)) 471 | } 472 | } 473 | 474 | func (cfg *LoadGenerator) Warmup(config *LoaderConfig) int { 475 | log.Info("warmup started") 476 | loadStats := &LoadStats{MinRequestTime: time.Millisecond, StatusCode: map[int]int{}} 477 | req := defaultHTTPPool.AcquireRequest() 478 | defer defaultHTTPPool.ReleaseRequest(req) 479 | resp := defaultHTTPPool.AcquireResponse() 480 | defer defaultHTTPPool.ReleaseResponse(resp) 481 | globalCtx := util.MapStr{} 482 | for _, v := range config.Requests { 483 | v.prepareRequest(config, globalCtx, req) 484 | 485 | if !req.Validate() { 486 | log.Errorf("invalid request: %v", req.String()) 487 | panic("invalid request") 488 | } 489 | 490 | next, err := doRequest(config, globalCtx, req, resp, &v, loadStats, nil) 491 | for k, _ := range loadStats.StatusCode { 492 | if len(config.RunnerConfig.ValidStatusCodesDuringWarmup) > 0 { 493 | if util.ContainsInAnyInt32Array(k, config.RunnerConfig.ValidStatusCodesDuringWarmup) { 494 | continue 495 | } 496 | } 497 | if k >= 400 || k == 0 || err != nil { 498 | log.Infof("requests seems failed to process, err: %v, are you sure to continue?\nPress `Ctrl+C` to skip or press 'Enter' to continue...", err) 499 | reader := bufio.NewReader(os.Stdin) 500 | reader.ReadString('\n') 501 | break 502 | } 503 | } 504 | if !next { 505 | break 506 | } 507 | } 508 | 509 | log.Info("warmup finished") 510 | return loadStats.NumRequests 511 | } 512 | 513 | func (cfg *LoadGenerator) Stop() { 514 | atomic.StoreInt32(&cfg.interrupted, 1) 515 | } 516 | -------------------------------------------------------------------------------- /loadgen.dsl: -------------------------------------------------------------------------------- 1 | # // How to use DSL to simplify requests, requests defined in loadgen.yml will be skipped in this mode 2 | # // $ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -run loadgen.dsl 3 | 4 | # runner: { 5 | # total_rounds: 1, 6 | # no_warm: true, 7 | # // Whether to log all requests 8 | # log_requests: false, 9 | # // Whether to log all requests with the specified response status 10 | # log_status_codes: [0, 500], 11 | # assert_invalid: false, 12 | # assert_error: false, 13 | # // Whether to reset the context, including variables, runtime KV pairs, 14 | # // etc., before this test run. 15 | # reset_context: false, 16 | # default_endpoint: "$[[env.ES_ENDPOINT]]", 17 | # default_basic_auth: { 18 | # username: "$[[env.ES_USERNAME]]", 19 | # password: "$[[env.ES_PASSWORD]]", 20 | # } 21 | # }, 22 | # variables: [ 23 | # { 24 | # name: "ip", 25 | # type: "file", 26 | # path: "dict/ip.txt", 27 | # // Replace special characters in the value 28 | # replace: { 29 | # '"': '\\"', 30 | # '\\': '\\\\', 31 | # }, 32 | # }, 33 | # { 34 | # name: "id", 35 | # type: "sequence", 36 | # }, 37 | # { 38 | # name: "id64", 39 | # type: "sequence64", 40 | # }, 41 | # { 42 | # name: "uuid", 43 | # type: "uuid", 44 | # }, 45 | # { 46 | # name: "now_local", 47 | # type: "now_local", 48 | # }, 49 | # { 50 | # name: "now_utc", 51 | # type: "now_utc", 52 | # }, 53 | # { 54 | # name: "now_utc_lite", 55 | # type: "now_utc_lite", 56 | # }, 57 | # { 58 | # name: "now_unix", 59 | # type: "now_unix", 60 | # }, 61 | # { 62 | # name: "now_with_format", 63 | # type: "now_with_format", 64 | # // https://programming.guide/go/format-parse-string-time-date-example.html 65 | # format: "2006-01-02T15:04:05-0700", 66 | # }, 67 | # { 68 | # name: "suffix", 69 | # type: "range", 70 | # from: 10, 71 | # to: 1000, 72 | # }, 73 | # { 74 | # name: "bool", 75 | # type: "range", 76 | # from: 0, 77 | # to: 1, 78 | # }, 79 | # { 80 | # name: "list", 81 | # type: "list", 82 | # data: ["medcl", "abc", "efg", "xyz"], 83 | # }, 84 | # { 85 | # name: "id_list", 86 | # type: "random_array", 87 | # variable_type: "number", // number/string 88 | # variable_key: "suffix", // variable key to get array items 89 | # square_bracket: false, 90 | # size: 10, // how many items for array 91 | # }, 92 | # { 93 | # name: "str_list", 94 | # type: "random_array", 95 | # variable_type: "number", // number/string 96 | # variable_key: "suffix", // variable key to get array items 97 | # square_bracket: true, 98 | # size: 10, // how many items for array 99 | # replace: { 100 | # // Use ' instead of " for string quotes 101 | # '"': "'", 102 | # // Use {} instead of [] as array brackets 103 | # "[": "{", 104 | # "]": "}", 105 | # }, 106 | # }, 107 | # ], 108 | 109 | DELETE /medcl 110 | 111 | PUT /medcl 112 | 113 | POST /medcl/_doc/1 114 | { 115 | "name": "medcl" 116 | } 117 | 118 | POST /medcl/_search 119 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } } 120 | -------------------------------------------------------------------------------- /loadgen.yml: -------------------------------------------------------------------------------- 1 | ## How to use loadgen? 2 | ## $ES_ENDPOINT=https://localhost:9200 ES_USERNAME=admin ES_PASSWORD=b14612393da0d4e7a70b ./bin/loadgen -config loadgen.yml 3 | 4 | env: 5 | ES_USERNAME: username 6 | ES_PASSWORD: password 7 | ES_ENDPOINT: http://localhost:9200 8 | 9 | runner: 10 | # total_rounds: 1 11 | no_warm: true 12 | valid_status_codes_during_warmup: [ 200,201,404 ] 13 | # Whether to log all requests 14 | log_requests: false 15 | # Whether to log all requests with the specified response status 16 | log_status_codes: 17 | - 0 18 | - 500 19 | assert_invalid: false 20 | assert_error: false 21 | # Whether to reset the context, including variables, runtime KV pairs, etc., 22 | # before this test run. 23 | reset_context: false 24 | default_endpoint: $[[env.ES_ENDPOINT]] 25 | default_basic_auth: 26 | username: $[[env.ES_USERNAME]] 27 | password: $[[env.ES_PASSWORD]] 28 | 29 | variables: 30 | # - name: ip 31 | # type: file 32 | # path: dict/ip.txt 33 | # replace: # replace special characters in the value 34 | # '"': '\"' 35 | # '\': '\\' 36 | - name: id 37 | type: sequence 38 | - name: id64 39 | type: sequence64 40 | - name: uuid 41 | type: uuid 42 | - name: now_local 43 | type: now_local 44 | - name: now_utc 45 | type: now_utc 46 | - name: now_utc_lite 47 | type: now_utc_lite 48 | - name: now_unix 49 | type: now_unix 50 | - name: now_with_format 51 | type: now_with_format #https://programming.guide/go/format-parse-string-time-date-example.html 52 | format: "2006-01-02T15:04:05-0700" #2006-01-02T15:04:05 53 | - name: suffix 54 | type: range 55 | from: 10 56 | to: 1000 57 | - name: bool 58 | type: range 59 | from: 0 60 | to: 1 61 | - name: list 62 | type: list 63 | data: 64 | - "medcl" 65 | - "abc" 66 | - "efg" 67 | - "xyz" 68 | - name: id_list 69 | type: random_array 70 | variable_type: number # number/string 71 | variable_key: suffix # variable key to get array items 72 | square_bracket: false 73 | size: 10 # how many items for array 74 | - name: str_list 75 | type: random_array 76 | variable_type: string # number/string 77 | variable_key: suffix #variable key to get array items 78 | square_bracket: true 79 | size: 10 # how many items for array 80 | replace: 81 | '"': "'" # use ' instead of " for string quotes 82 | # use {} instead of [] as array brackets 83 | "[": "{" 84 | "]": "}" 85 | 86 | requests: 87 | - request: #prepare some docs 88 | method: POST 89 | runtime_variables: 90 | batch_no: uuid 91 | runtime_body_line_variables: 92 | routing_no: uuid 93 | url: /_bulk 94 | body: | 95 | {"index": {"_index": "medcl", "_type": "_doc", "_id": "$[[uuid]]"}} 96 | {"id": "$[[id]]", "field1": "$[[list]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 97 | {"index": {"_index": "infinilabs", "_type": "_doc", "_id": "$[[uuid]]"}} 98 | {"id": "$[[id]]", "field1": "$[[list]]", "now_local": "$[[now_local]]", "now_unix": "$[[now_unix]]"} 99 | - request: #search this index 100 | method: POST 101 | runtime_variables: 102 | batch_no: uuid 103 | runtime_body_line_variables: 104 | routing_no: uuid 105 | basic_auth: #override default auth 106 | username: $[[env.ES_USERNAME]] 107 | password: $[[env.ES_PASSWORD]] 108 | url: $[[env.ES_ENDPOINT]]/medcl/_search #override with full request url 109 | body: | 110 | { "track_total_hits": true, "size": 0, "query": { "terms": { "patent_id": [ $[[id_list]] ] } } } 111 | 112 | #add more requests -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) INFINI Labs & INFINI LIMITED. 2 | // 3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0 4 | // and as commercial software. 5 | // 6 | // For commercial licensing, contact us at: 7 | // - Website: infinilabs.com 8 | // - Email: hello@infini.ltd 9 | // 10 | // Open Source licensed under AGPL V3: 11 | // This program is free software: you can redistribute it and/or modify 12 | // it under the terms of the GNU Affero General Public License as published by 13 | // the Free Software Foundation, either version 3 of the License, or 14 | // (at your option) any later version. 15 | // 16 | // This program is distributed in the hope that it will be useful, 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | // GNU Affero General Public License for more details. 20 | // 21 | // You should have received a copy of the GNU Affero General Public License 22 | // along with this program. If not, see . 23 | 24 | /* Copyright © INFINI Ltd. All rights reserved. 25 | * web: https://infinilabs.com 26 | * mail: hello#infini.ltd */ 27 | 28 | package main 29 | 30 | import ( 31 | "context" 32 | _ "embed" 33 | E "errors" 34 | "flag" 35 | "fmt" 36 | "os" 37 | "os/signal" 38 | "strings" 39 | "time" 40 | 41 | "github.com/jamiealquiza/tachymeter" 42 | 43 | log "github.com/cihub/seelog" 44 | wasm "github.com/tetratelabs/wazero" 45 | wasmAPI "github.com/tetratelabs/wazero/api" 46 | "infini.sh/framework" 47 | coreConfig "infini.sh/framework/core/config" 48 | "infini.sh/framework/core/env" 49 | "infini.sh/framework/core/global" 50 | "infini.sh/framework/core/module" 51 | "infini.sh/framework/core/util" 52 | stats "infini.sh/framework/plugins/stats_statsd" 53 | "infini.sh/loadgen/config" 54 | ) 55 | 56 | //go:embed plugins/loadgen_dsl.wasm 57 | var loadgenDSL []byte 58 | 59 | var maxDuration int = 10 60 | var goroutines int = 2 61 | var rateLimit int = -1 62 | var reqLimit int = -1 63 | var timeout int = 60 64 | var readTimeout int = 0 65 | var writeTimeout int = 0 66 | var dialTimeout int = 3 67 | var compress bool = false 68 | var mixed bool = false 69 | var totalRounds int = -1 70 | var dslFileToRun string 71 | var statsAggregator chan *LoadStats 72 | 73 | func init() { 74 | flag.IntVar(&goroutines, "c", 1, "Number of concurrent threads to use") 75 | flag.IntVar(&maxDuration, "d", 5, "Duration of the test in seconds") 76 | flag.IntVar(&rateLimit, "r", -1, "Maximum requests per second (fixed QPS), default: -1 (unlimited)") 77 | flag.IntVar(&reqLimit, "l", -1, "Total number of requests to send, default: -1 (unlimited)") 78 | flag.IntVar(&timeout, "timeout", 0, "Request timeout in seconds, default: 0 (no timeout)") 79 | flag.IntVar(&readTimeout, "read-timeout", 0, "Connection read timeout in seconds, default: 0 (inherits -timeout)") 80 | flag.IntVar(&writeTimeout, "write-timeout", 0, "Connection write timeout in seconds, default: 0 (inherits -timeout)") 81 | flag.IntVar(&dialTimeout, "dial-timeout", 3, "Connection dial timeout in seconds, default: 3") 82 | flag.BoolVar(&compress, "compress", false, "Enable gzip compression for requests") 83 | flag.BoolVar(&mixed, "mixed", false, "Enable mixed requests from YAML/DSL") 84 | flag.IntVar(&totalRounds, "total-rounds", -1, "Number of rounds for each request configuration, default: -1 (unlimited)") 85 | flag.StringVar(&dslFileToRun, "run", "", "Path to a DSL-based request file to execute") 86 | } 87 | 88 | func startLoader(cfg *LoaderConfig) *LoadStats { 89 | defer log.Flush() 90 | 91 | statsAggregator = make(chan *LoadStats, goroutines) 92 | sigChan := make(chan os.Signal, 1) 93 | 94 | signal.Notify(sigChan, os.Interrupt) 95 | 96 | flag.Parse() 97 | 98 | if cfg.RunnerConfig.MetricSampleSize <= 0 { 99 | cfg.RunnerConfig.MetricSampleSize = 10000 100 | } 101 | 102 | //override the total 103 | if totalRounds > 0 { 104 | cfg.RunnerConfig.TotalRounds = totalRounds 105 | } 106 | 107 | // Initialize tachymeter. 108 | timer := tachymeter.New(&tachymeter.Config{Size: cfg.RunnerConfig.MetricSampleSize}) 109 | 110 | loadGen := NewLoadGenerator(maxDuration, goroutines, statsAggregator, cfg.RunnerConfig.DisableHeaderNamesNormalizing) 111 | 112 | leftDoc := reqLimit 113 | 114 | if !cfg.RunnerConfig.NoWarm { 115 | reqCount := loadGen.Warmup(cfg) 116 | leftDoc -= reqCount 117 | } 118 | 119 | if reqLimit >= 0 && leftDoc <= 0 { 120 | log.Warn("No request to execute, exit now\n") 121 | return nil 122 | } 123 | 124 | var reqPerGoroutines int 125 | if reqLimit > 0 { 126 | if goroutines > leftDoc { 127 | goroutines = leftDoc 128 | } 129 | 130 | reqPerGoroutines = int((leftDoc + 1) / goroutines) 131 | } 132 | 133 | // Start wall time for all Goroutines. 134 | wallTimeStart := time.Now() 135 | 136 | for i := 0; i < goroutines; i++ { 137 | thisDoc := -1 138 | if reqPerGoroutines > 0 { 139 | if leftDoc > reqPerGoroutines { 140 | thisDoc = reqPerGoroutines 141 | } else { 142 | thisDoc = leftDoc 143 | } 144 | leftDoc -= thisDoc 145 | } 146 | 147 | go loadGen.Run(cfg, thisDoc, timer) 148 | } 149 | 150 | responders := 0 151 | aggStats := LoadStats{MinRequestTime: time.Millisecond, StatusCode: map[int]int{}} 152 | 153 | for responders < goroutines { 154 | select { 155 | case <-sigChan: 156 | loadGen.Stop() 157 | case stats := <-statsAggregator: 158 | aggStats.NumErrs += stats.NumErrs 159 | aggStats.NumAssertInvalid += stats.NumAssertInvalid 160 | aggStats.NumAssertSkipped += stats.NumAssertSkipped 161 | aggStats.NumRequests += stats.NumRequests 162 | aggStats.TotReqSize += stats.TotReqSize 163 | aggStats.TotRespSize += stats.TotRespSize 164 | aggStats.TotDuration += stats.TotDuration 165 | aggStats.MaxRequestTime = util.MaxDuration(aggStats.MaxRequestTime, stats.MaxRequestTime) 166 | aggStats.MinRequestTime = util.MinDuration(aggStats.MinRequestTime, stats.MinRequestTime) 167 | 168 | for k, v := range stats.StatusCode { 169 | oldV, ok := aggStats.StatusCode[k] 170 | if !ok { 171 | oldV = 0 172 | } 173 | aggStats.StatusCode[k] = oldV + v 174 | } 175 | 176 | responders++ 177 | } 178 | } 179 | 180 | if aggStats.NumRequests == 0 { 181 | log.Error("Error: No statistics collected / no requests found") 182 | return nil 183 | } 184 | 185 | finalDuration := time.Since(wallTimeStart) 186 | 187 | // When finished, set elapsed wall time. 188 | timer.SetWallTime(finalDuration) 189 | 190 | avgThreadDur := aggStats.TotDuration / time.Duration(responders) //need to average the aggregated duration 191 | 192 | roughReqRate := float64(aggStats.NumRequests) / float64(finalDuration.Seconds()) 193 | roughReqBytesRate := float64(aggStats.TotReqSize) / float64(finalDuration.Seconds()) 194 | roughBytesRate := float64(aggStats.TotRespSize+aggStats.TotReqSize) / float64(finalDuration.Seconds()) 195 | 196 | reqRate := float64(aggStats.NumRequests) / avgThreadDur.Seconds() 197 | avgReqTime := aggStats.TotDuration / time.Duration(aggStats.NumRequests) 198 | bytesRate := float64(aggStats.TotRespSize+aggStats.TotReqSize) / avgThreadDur.Seconds() 199 | 200 | // Flush before printing stats to avoid logging mixing with stats 201 | log.Flush() 202 | 203 | if cfg.RunnerConfig.NoSizeStats { 204 | fmt.Printf("\n%v requests finished in %v\n", aggStats.NumRequests, avgThreadDur) 205 | } else { 206 | fmt.Printf("\n%v requests finished in %v, %v sent, %v received\n", aggStats.NumRequests, avgThreadDur, util.ByteValue{Size: float64(aggStats.TotReqSize)}, util.ByteValue{Size: float64(aggStats.TotRespSize)}) 207 | } 208 | 209 | fmt.Println("\n[Loadgen Client Metrics]") 210 | 211 | fmt.Printf("Requests/sec:\t\t%.2f\n", roughReqRate) 212 | 213 | if !cfg.RunnerConfig.BenchmarkOnly && !cfg.RunnerConfig.NoSizeStats { 214 | fmt.Printf( 215 | "Request Traffic/sec:\t%v\n"+ 216 | "Total Transfer/sec:\t%v\n", 217 | util.ByteValue{Size: roughReqBytesRate}, 218 | util.ByteValue{Size: roughBytesRate}) 219 | } 220 | 221 | fmt.Printf("Fastest Request:\t%v\n", aggStats.MinRequestTime) 222 | fmt.Printf("Slowest Request:\t%v\n", aggStats.MaxRequestTime) 223 | 224 | if cfg.RunnerConfig.AssertError { 225 | fmt.Printf("Number of Errors:\t%v\n", aggStats.NumErrs) 226 | } 227 | 228 | if cfg.RunnerConfig.AssertInvalid { 229 | fmt.Printf("Assert Invalid:\t\t%v\n", aggStats.NumAssertInvalid) 230 | fmt.Printf("Assert Skipped:\t\t%v\n", aggStats.NumAssertSkipped) 231 | } 232 | 233 | for k, v := range aggStats.StatusCode { 234 | fmt.Printf("Status %v:\t\t%v\n", k, v) 235 | } 236 | 237 | if !cfg.RunnerConfig.BenchmarkOnly && !cfg.RunnerConfig.NoStats { 238 | // Rate outputs will be accurate. 239 | fmt.Println("\n[Latency Metrics]") 240 | fmt.Println(timer.Calc().String()) 241 | 242 | fmt.Println("\n[Latency Distribution]") 243 | fmt.Println(timer.Calc().Histogram.String(30)) 244 | } 245 | 246 | fmt.Printf("\n[Estimated Server Metrics]\nRequests/sec:\t\t%.2f\nAvg Req Time:\t\t%v\n", reqRate, avgReqTime) 247 | if !cfg.RunnerConfig.BenchmarkOnly && !cfg.RunnerConfig.NoSizeStats { 248 | fmt.Printf("Transfer/sec:\t\t%v\n", util.ByteValue{Size: bytesRate}) 249 | } 250 | 251 | fmt.Println("") 252 | 253 | return &aggStats 254 | } 255 | 256 | //func addProcessToCgroup(filepath string, pid int) { 257 | // file, err := os.OpenFile(filepath, os.O_WRONLY, 0644) 258 | // if err != nil { 259 | // fmt.Println(err) 260 | // os.Exit(1) 261 | // } 262 | // defer file.Close() 263 | // 264 | // if _, err := file.WriteString(fmt.Sprintf("%d", pid)); err != nil { 265 | // fmt.Println("failed to setup cgroup for the container: ", err) 266 | // os.Exit(1) 267 | // } 268 | //} 269 | // 270 | //func cgroupSetup(pid int) { 271 | // for _, c := range []string{"cpu", "memory"} { 272 | // cpath := fmt.Sprintf("/sys/fs/cgroup/%s/mycontainer/", c) 273 | // if err := os.MkdirAll(cpath, 0644); err != nil { 274 | // fmt.Println("failed to create cpu cgroup for my container: ", err) 275 | // os.Exit(1) 276 | // } 277 | // addProcessToCgroup(cpath+"cgroup.procs", pid) 278 | // } 279 | //} 280 | 281 | func main() { 282 | 283 | terminalHeader := (" __ ___ _ ___ ___ __ __\n") 284 | terminalHeader += (" / / /___\\/_\\ / \\/ _ \\ /__\\/\\ \\ \\\n") 285 | terminalHeader += (" / / // ///_\\\\ / /\\ / /_\\//_\\ / \\/ /\n") 286 | terminalHeader += ("/ /__/ \\_// _ \\/ /_// /_\\\\//__/ /\\ /\n") 287 | terminalHeader += ("\\____|___/\\_/ \\_/___,'\\____/\\__/\\_\\ \\/\n\n") 288 | terminalHeader += ("HOME: https://github.com/infinilabs/loadgen/\n\n") 289 | 290 | terminalFooter := ("") 291 | 292 | app := framework.NewApp("loadgen", "A http load generator and testing suite, open-sourced under the GNU AGPLv3.", 293 | config.Version, config.BuildNumber, config.LastCommitLog, config.BuildDate, config.EOLDate, terminalHeader, terminalFooter) 294 | 295 | app.IgnoreMainConfigMissing() 296 | app.Init(nil) 297 | 298 | defer app.Shutdown() 299 | appConfig := AppConfig{} 300 | 301 | if app.Setup(func() { 302 | module.RegisterUserPlugin(&stats.StatsDModule{}) 303 | module.Start() 304 | 305 | environments := map[string]string{} 306 | ok, err := env.ParseConfig("env", &environments) 307 | if ok && err != nil { 308 | if ok && err != nil { 309 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 310 | panic(err) 311 | } else { 312 | log.Error(err) 313 | } 314 | } 315 | } 316 | 317 | // Append system environment variables. 318 | environs := os.Environ() 319 | for _, env := range environs { 320 | kv := strings.Split(env, "=") 321 | if len(kv) == 2 { 322 | k, v := kv[0], kv[1] 323 | if _, ok := environments[k]; ok { 324 | environments[k] = v 325 | } 326 | } 327 | } 328 | 329 | tests := []Test{} 330 | ok, err = env.ParseConfig("tests", &tests) 331 | if ok && err != nil { 332 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 333 | panic(err) 334 | } else { 335 | log.Error(err) 336 | } 337 | } 338 | 339 | requests := []RequestItem{} 340 | ok, err = env.ParseConfig("requests", &requests) 341 | if ok && err != nil { 342 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 343 | panic(err) 344 | } else { 345 | log.Error(err) 346 | } 347 | } 348 | 349 | variables := []Variable{} 350 | ok, err = env.ParseConfig("variables", &variables) 351 | if ok && err != nil { 352 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 353 | panic(err) 354 | } else { 355 | log.Error(err) 356 | } 357 | } 358 | 359 | runnerConfig := RunnerConfig{ 360 | ValidStatusCodesDuringWarmup: []int{}, 361 | } 362 | ok, err = env.ParseConfig("runner", &runnerConfig) 363 | if ok && err != nil { 364 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 365 | panic(err) 366 | } else { 367 | log.Error(err) 368 | } 369 | } 370 | 371 | appConfig.Environments = environments 372 | appConfig.Tests = tests 373 | appConfig.Requests = requests 374 | appConfig.Variable = variables 375 | appConfig.RunnerConfig = runnerConfig 376 | appConfig.Init() 377 | }, func() { 378 | go func() { 379 | //dsl go first 380 | if dslFileToRun != "" { 381 | log.Debugf("running DSL based requests from %s", dslFileToRun) 382 | if status := runDSLFile(&appConfig, dslFileToRun); status != 0 { 383 | os.Exit(status) 384 | } 385 | if !mixed { 386 | os.Exit(0) 387 | return 388 | } 389 | } 390 | 391 | if len(appConfig.Requests) != 0 { 392 | log.Debugf("running YAML based requests") 393 | if status := runLoaderConfig(&appConfig.LoaderConfig); status != 0 { 394 | os.Exit(status) 395 | } 396 | if !mixed { 397 | os.Exit(0) 398 | return 399 | } 400 | } 401 | 402 | //test suit go last 403 | if len(appConfig.Tests) != 0 { 404 | log.Debugf("running test suite") 405 | if !startRunner(&appConfig) { 406 | os.Exit(1) 407 | } 408 | if !mixed { 409 | os.Exit(0) 410 | return 411 | } 412 | } 413 | 414 | os.Exit(0) 415 | }() 416 | 417 | }, nil) { 418 | app.Run() 419 | } 420 | 421 | } 422 | 423 | func runDSLFile(appConfig *AppConfig, path string) int { 424 | 425 | path = util.TryGetFileAbsPath(path, false) 426 | dsl, err := env.LoadConfigContents(path) 427 | if err != nil { 428 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 429 | panic(err) 430 | } else { 431 | log.Error(err) 432 | } 433 | } 434 | log.Infof("loading config: %s", path) 435 | 436 | return runDSL(appConfig, dsl) 437 | } 438 | 439 | func runDSL(appConfig *AppConfig, dsl string) int { 440 | loaderConfig := parseDSL(appConfig, dsl) 441 | return runLoaderConfig(&loaderConfig) 442 | } 443 | 444 | func runLoaderConfig(config *LoaderConfig) int { 445 | err := config.Init() 446 | if err != nil { 447 | panic(err) 448 | } 449 | 450 | aggStats := startLoader(config) 451 | if aggStats != nil { 452 | if config.RunnerConfig.AssertInvalid && aggStats.NumAssertInvalid > 0 { 453 | return 1 454 | } 455 | if config.RunnerConfig.AssertError && aggStats.NumErrs > 0 { 456 | return 2 457 | } 458 | } 459 | 460 | return 0 461 | } 462 | 463 | // parseDSL parses a DSL string to LoaderConfig. 464 | func parseDSL(appConfig *AppConfig, input string) (output LoaderConfig) { 465 | output = LoaderConfig{} 466 | output.RunnerConfig = appConfig.RunnerConfig 467 | output.Variable = appConfig.Variable 468 | 469 | outputStr, err := loadPlugins([][]byte{loadgenDSL}, input) 470 | if err != nil { 471 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 472 | panic(err) 473 | } else { 474 | log.Error(err) 475 | } 476 | } 477 | log.Debugf("using config:\n%s", outputStr) 478 | 479 | outputParser, err := coreConfig.NewConfigWithYAML([]byte(outputStr), "loadgen-dsl") 480 | if err != nil { 481 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 482 | panic(err) 483 | } else { 484 | log.Error(err) 485 | } 486 | } 487 | 488 | if err := outputParser.Unpack(&output); err != nil { 489 | if global.Env().SystemConfig.Configs.PanicOnConfigError { 490 | panic(err) 491 | } else { 492 | log.Error(err) 493 | } 494 | } 495 | 496 | return 497 | } 498 | 499 | func loadPlugins(plugins [][]byte, input string) (output string, err error) { 500 | // init runtime 501 | ctx := context.Background() 502 | r := wasm.NewRuntime(ctx) 503 | defer r.Close(ctx) 504 | 505 | var mod wasmAPI.Module 506 | for _, plug := range plugins { 507 | // load plugin 508 | mod, err = r.Instantiate(ctx, plug) 509 | if err != nil { 510 | return 511 | } 512 | // call plugin 513 | output, err = callPlugin(ctx, mod, string(input)) 514 | if err != nil { 515 | break 516 | } 517 | // pipe output 518 | input = output 519 | } 520 | return 521 | } 522 | 523 | func callPlugin(ctx context.Context, mod wasmAPI.Module, input string) (output string, err error) { 524 | alloc := mod.ExportedFunction("allocate") 525 | free := mod.ExportedFunction("deallocate") 526 | process := mod.ExportedFunction("process") 527 | 528 | // 1) Plugins do not have access to host memory, so the first step is to copy 529 | // the input string to the WASM VM. 530 | inputSize := uint32(len(input)) 531 | ret, err := alloc.Call(ctx, uint64(inputSize)) 532 | if err != nil { 533 | return 534 | } 535 | inputPtr := ret[0] 536 | defer free.Call(ctx, inputPtr) 537 | _, inputAddr, _ := decodePtr(inputPtr) 538 | mod.Memory().Write(inputAddr, []byte(input)) 539 | 540 | // 2) Invoke the `process` function to handle the input string, which returns 541 | // a result pointer (referred to as `decodePtr` in the following text) 542 | // representing the processing result. 543 | ret, err = process.Call(ctx, inputPtr) 544 | if err != nil { 545 | return 546 | } 547 | outputPtr := ret[0] 548 | defer free.Call(ctx, outputPtr) 549 | 550 | // 3) Check the processing result. 551 | errors, outputAddr, outputSize := decodePtr(outputPtr) 552 | bytes, _ := mod.Memory().Read(outputAddr, outputSize) 553 | 554 | if errors { 555 | err = E.New(string(bytes)) 556 | } else { 557 | output = string(bytes) 558 | } 559 | return 560 | } 561 | 562 | // decodePtr decodes error state and memory address of a result pointer. 563 | // 564 | // Some functions may return success or failure as a result. In such cases, a 565 | // special 64-bit pointer is returned. The highest bit in the upper 32 bits 566 | // serves as a boolean value indicating success (1) or failure (0), while the 567 | // remaining 31 bits represent the length of the message. The lower 32 bits of 568 | // the pointer represent the memory address of the specific message. 569 | func decodePtr(ptr uint64) (errors bool, addr, size uint32) { 570 | const SIZE_MASK uint32 = (^uint32(0)) >> 1 571 | addr = uint32(ptr) 572 | size = uint32(ptr>>32) & SIZE_MASK 573 | errors = (ptr >> 63) != 0 574 | return 575 | } 576 | -------------------------------------------------------------------------------- /plugins/loadgen_dsl.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinilabs/loadgen/2f372fc70da65ae78580ccb778ba7976139150e5/plugins/loadgen_dsl.wasm -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) INFINI Labs & INFINI LIMITED. 2 | // 3 | // The INFINI Loadgen is offered under the GNU Affero General Public License v3.0 4 | // and as commercial software. 5 | // 6 | // For commercial licensing, contact us at: 7 | // - Website: infinilabs.com 8 | // - Email: hello@infini.ltd 9 | // 10 | // Open Source licensed under AGPL V3: 11 | // This program is free software: you can redistribute it and/or modify 12 | // it under the terms of the GNU Affero General Public License as published by 13 | // the Free Software Foundation, either version 3 of the License, or 14 | // (at your option) any later version. 15 | // 16 | // This program is distributed in the hope that it will be useful, 17 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | // GNU Affero General Public License for more details. 20 | // 21 | // You should have received a copy of the GNU Affero General Public License 22 | // along with this program. If not, see . 23 | 24 | /* Copyright © INFINI Ltd. All rights reserved. 25 | * web: https://infinilabs.com 26 | * mail: hello#infini.ltd */ 27 | 28 | package main 29 | 30 | import ( 31 | "bytes" 32 | "context" 33 | "errors" 34 | "infini.sh/framework/core/global" 35 | "net" 36 | "os" 37 | "os/exec" 38 | "path" 39 | "path/filepath" 40 | "sync/atomic" 41 | "time" 42 | 43 | log "github.com/cihub/seelog" 44 | "infini.sh/framework/core/util" 45 | ) 46 | 47 | type TestResult struct { 48 | Failed bool `json:"failed"` 49 | Time time.Time `json:"time"` 50 | DurationInMs int64 `json:"duration_in_ms"` 51 | Error error `json:"error"` 52 | } 53 | 54 | type TestMsg struct { 55 | Time time.Time `json:"time"` 56 | Path string `json:"path"` 57 | Status string `json:"status"` // ABORTED/FAILED/SUCCESS 58 | DurationInMs int64 `json:"duration_in_ms"` 59 | } 60 | 61 | const ( 62 | portTestTimeout = 100 * time.Millisecond 63 | ) 64 | 65 | func startRunner(config *AppConfig) bool { 66 | defer log.Flush() 67 | 68 | cwd, err := os.Getwd() 69 | if err != nil { 70 | log.Infof("failed to get working directory, err: %v", err) 71 | return false 72 | } 73 | msgs := make([]*TestMsg, len(config.Tests)) 74 | for i, test := range config.Tests { 75 | // Wait for the last process to get fully killed if not existed cleanly 76 | time.Sleep(time.Second) 77 | result, err := runTest(config, cwd, test) 78 | msg := &TestMsg{ 79 | Path: test.Path, 80 | } 81 | if result == nil || err != nil { 82 | log.Debugf("failed to run test, error: %+v", err) 83 | msg.Status = "ABORTED" 84 | } else if result.Failed { 85 | msg.Status = "FAILED" 86 | } else { 87 | msg.Status = "SUCCESS" 88 | } 89 | if result != nil { 90 | msg.DurationInMs = result.DurationInMs 91 | msg.Time = result.Time 92 | } 93 | msgs[i] = msg 94 | } 95 | ok := true 96 | for _, msg := range msgs { 97 | log.Infof("[%s][TEST][%s] [%s] duration: %d(ms)", msg.Time.Format("2006-01-02 15:04:05"), msg.Status, msg.Path, msg.DurationInMs) 98 | if msg.Status != "SUCCESS" { 99 | ok = false 100 | } 101 | } 102 | return ok 103 | } 104 | 105 | func runTest(config *AppConfig, cwd string, test Test) (*TestResult, error) { 106 | // To kill gateway/other command automatically 107 | ctx, cancel := context.WithCancel(context.Background()) 108 | defer cancel() 109 | 110 | //if err := os.Chdir(cwd); err != nil { 111 | // return nil, err 112 | //} 113 | 114 | testPath := test.Path 115 | var gatewayPath string 116 | if config.Environments[env_LR_GATEWAY_CMD] != "" { 117 | gatewayPath, _ = filepath.Abs(config.Environments[env_LR_GATEWAY_CMD]) 118 | } 119 | 120 | loaderConfigPath := path.Join(testPath, "loadgen.dsl") 121 | //auto resolve the loaderConfigPath 122 | if !util.FileExists(loaderConfigPath) { 123 | temp := path.Join(filepath.Dir(global.Env().GetConfigFile()), loaderConfigPath) 124 | if util.FileExists(temp) { 125 | loaderConfigPath = temp 126 | } else { 127 | temp := path.Join(filepath.Dir(global.Env().GetConfigDir()), loaderConfigPath) 128 | if util.FileExists(temp) { 129 | loaderConfigPath = temp 130 | } 131 | } 132 | } 133 | loaderConfigPath, _ = filepath.Abs(loaderConfigPath) 134 | 135 | //log.Debugf("Executing gateway within %s", testPath) 136 | //if err := os.Chdir(filepath.Dir(loaderConfigPath)); err != nil { 137 | // return nil, err 138 | //} 139 | //// Revert cwd change 140 | //defer os.Chdir(cwd) 141 | 142 | env := generateEnv(config) 143 | log.Debugf("Executing gateway with environment [%+v]", env) 144 | 145 | gatewayConfigPath := path.Join(testPath, "gateway.yml") 146 | if _, err := os.Stat(gatewayConfigPath); err == nil { 147 | if gatewayPath == "" { 148 | return nil, errors.New("invalid LR_GATEWAY_CMD, cannot find gateway") 149 | } 150 | gatewayOutput := &bytes.Buffer{} 151 | // Start gateway server 152 | gatewayHost, gatewayApiHost := config.Environments[env_LR_GATEWAY_HOST], config.Environments[env_LR_GATEWAY_API_HOST] 153 | gatewayCmd, gatewayExited, err := runGateway(ctx, gatewayPath, gatewayConfigPath, gatewayHost, gatewayApiHost, env, gatewayOutput) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | defer func() { 159 | log.Debug("waiting for 5s to stop the gateway") 160 | gatewayCmd.Process.Signal(os.Interrupt) 161 | timeout := time.NewTimer(5 * time.Second) 162 | select { 163 | case <-gatewayExited: 164 | case <-timeout.C: 165 | } 166 | log.Debug("============================== Gateway Exit Info [Start] =============================") 167 | log.Debug(util.UnsafeBytesToString(gatewayOutput.Bytes())) 168 | log.Debug("============================== Gateway Exit Info [End] =============================") 169 | }() 170 | } 171 | 172 | startTime := time.Now() 173 | testResult := &TestResult{} 174 | defer func() { 175 | testResult.Time = time.Now() 176 | testResult.DurationInMs = int64(testResult.Time.Sub(startTime) / time.Millisecond) 177 | }() 178 | 179 | status := runDSLFile(config, loaderConfigPath) 180 | if status != 0 { 181 | testResult.Failed = true 182 | } 183 | return testResult, nil 184 | } 185 | 186 | func runGateway(ctx context.Context, gatewayPath, gatewayConfigPath, gatewayHost, gatewayApiHost string, env []string, gatewayOutput *bytes.Buffer) (*exec.Cmd, chan int, error) { 187 | gatewayCmdArgs := []string{"-config", gatewayConfigPath, "-log", "debug"} 188 | log.Debugf("Executing gateway with args [%+v]", gatewayCmdArgs) 189 | gatewayCmd := exec.CommandContext(ctx, gatewayPath, gatewayCmdArgs...) 190 | gatewayCmd.Env = env 191 | gatewayCmd.Stdout = gatewayOutput 192 | gatewayCmd.Stderr = gatewayOutput 193 | 194 | gatewayFailed := int32(0) 195 | gatewayExited := make(chan int) 196 | 197 | go func() { 198 | err := gatewayCmd.Run() 199 | if err != nil { 200 | log.Debugf("gateway server exited with non-zero code: %+v", err) 201 | atomic.StoreInt32(&gatewayFailed, 1) 202 | } 203 | gatewayExited <- 1 204 | }() 205 | 206 | gatewayReady := false 207 | 208 | // Check whether gateway is ready. 209 | for i := 0; i < 10; i += 1 { 210 | if atomic.LoadInt32(&gatewayFailed) == 1 { 211 | break 212 | } 213 | log.Debugf("Checking whether %s or %s is ready...", gatewayHost, gatewayApiHost) 214 | entryReady, apiReady := testPort(gatewayHost), testPort(gatewayApiHost) 215 | if entryReady || apiReady { 216 | log.Debugf("gateway is started, entry: %+v, api: %+v", entryReady, apiReady) 217 | gatewayReady = true 218 | break 219 | } 220 | log.Debugf("failed to probe gateway, retrying") 221 | time.Sleep(100 * time.Millisecond) 222 | } 223 | 224 | if !gatewayReady { 225 | return nil, nil, errors.New("can't start gateway") 226 | } 227 | 228 | return gatewayCmd, gatewayExited, nil 229 | } 230 | 231 | func testPort(host string) bool { 232 | conn, err := net.DialTimeout("tcp", host, portTestTimeout) 233 | if err != nil { 234 | return false 235 | } 236 | conn.Close() 237 | return true 238 | } 239 | 240 | func generateEnv(config *AppConfig) (env []string) { 241 | for k, v := range config.Environments { 242 | env = append(env, k+"="+v) 243 | } 244 | // Disable greeting messages 245 | env = append(env, "SILENT_GREETINGS=1") 246 | return 247 | } 248 | --------------------------------------------------------------------------------