Repository: locustio/locust Branch: master Commit: 9de4b47e81d4 Files: 389 Total size: 4.6 MB Directory structure: gitextract_78ea2ktw/ ├── .dockerignore ├── .git-blame-ignore-revs ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── dependabot.yaml │ └── workflows/ │ ├── stale.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── launch_locust.json │ └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.ci ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── Vagrantfile ├── benchmarks/ │ └── dispatch.py ├── docs/ │ ├── _static/ │ │ └── theme-overrides.css │ ├── _templates/ │ │ └── footer.html │ ├── api.rst │ ├── changelog.rst │ ├── conf.py │ ├── configuration.rst │ ├── custom-load-shape.rst │ ├── developing-locust.rst │ ├── extending-locust.rst │ ├── extensions.rst │ ├── faq.rst │ ├── further-reading.rst │ ├── history.rst │ ├── increase-performance.rst │ ├── increasing-request-rate.rst │ ├── index.rst │ ├── installation.rst │ ├── kubernetes-operator.rst │ ├── logging.rst │ ├── quickstart.rst │ ├── retrieving-stats.rst │ ├── running-distributed.rst │ ├── running-in-debugger.rst │ ├── running-in-docker.rst │ ├── running-without-web-ui.rst │ ├── tasksets.rst │ ├── telemetry.rst │ ├── testing-other-systems.rst │ ├── use-as-lib.rst │ ├── vscode-extension.rst │ ├── what-is-locust.rst │ └── writing-a-locustfile.rst ├── examples/ │ ├── add_command_line_argument.py │ ├── basic.py │ ├── bottlenecked_server.py │ ├── browse_docs_sequence_test.py │ ├── browse_docs_test.py │ ├── csrf_form_authentication.py │ ├── custom_messages.py │ ├── custom_shape/ │ │ ├── double_wave.py │ │ ├── stages.py │ │ ├── staging_user_classes.py │ │ ├── step_load.py │ │ └── wait_user_count.py │ ├── custom_wait_function.py │ ├── custom_xmlrpc_client/ │ │ ├── server.py │ │ └── xmlrpc_locustfile.py │ ├── debugging.py │ ├── debugging_advanced.py │ ├── dispatch_test_scripts/ │ │ ├── locustfile.py │ │ ├── run-disributed-headless.sh │ │ ├── run-disributed-web.sh │ │ ├── run-local-headless.sh │ │ └── run-local-web.sh │ ├── dns_ex.py │ ├── docker-compose/ │ │ └── docker-compose.yml │ ├── dynamic_user_credentials.py │ ├── events.py │ ├── extend_web_ui.py │ ├── fast_http_locust.py │ ├── grpc/ │ │ ├── grpc_user.py │ │ ├── hello.proto │ │ ├── hello_pb2.py │ │ ├── hello_pb2_grpc.py │ │ ├── hello_server.py │ │ └── locustfile.py │ ├── locustfile.py │ ├── manual_stats_reporting.py │ ├── markov_taskset.py │ ├── milvus/ │ │ ├── README.md │ │ └── locustfile.py │ ├── mongodb/ │ │ ├── README.md │ │ └── locustfile.py │ ├── mqtt/ │ │ ├── README.md │ │ ├── locustfile.py │ │ ├── locustfile_custom_mqtt_client.py │ │ └── mosquitto_config/ │ │ └── mosquitto.conf │ ├── multiple_hosts.py │ ├── nested_inline_tasksets.py │ ├── open_closed_workload.py │ ├── openai_ex.py │ ├── postgres/ │ │ ├── README.md │ │ └── locustfile.py │ ├── qdrant/ │ │ ├── README.md │ │ └── locustfile.py │ ├── response_validations.py │ ├── rest.py │ ├── sdk_session_patching/ │ │ └── session_patch_locustfile.py │ ├── semaphore_wait.py │ ├── socketio/ │ │ ├── echo_server.py │ │ └── socketio_ex.py │ ├── stop_on_threshold.py │ ├── terraform/ │ │ └── aws/ │ │ ├── README.md │ │ ├── data_subnet.tf │ │ ├── main.tf │ │ ├── output.tf │ │ ├── plan/ │ │ │ └── basic.py │ │ ├── provisioner.tf │ │ └── variables.tf │ ├── test_data_management.py │ ├── test_pytest.py │ ├── testdata_from_csv.csv │ ├── testdata_from_csv.py │ ├── use_as_lib.py │ ├── vagrant/ │ │ ├── README.md │ │ └── supervisord.conf │ ├── web_ui_auth/ │ │ ├── basic.py │ │ └── custom_form.py │ ├── web_ui_cache_stats.py │ ├── worker_index.py │ └── x-forwarded-for.py ├── generate_changelog.py ├── hatch_build.py ├── locust/ │ ├── __init__.py │ ├── __main__.py │ ├── argument_parser.py │ ├── clients.py │ ├── contrib/ │ │ ├── __init__.py │ │ ├── dns.py │ │ ├── fasthttp.py │ │ ├── milvus.py │ │ ├── mongodb.py │ │ ├── mqtt.py │ │ ├── oai.py │ │ ├── postgres.py │ │ ├── qdrant.py │ │ └── socketio.py │ ├── debug.py │ ├── dispatch.py │ ├── env.py │ ├── event.py │ ├── exception.py │ ├── html.py │ ├── input_events.py │ ├── log.py │ ├── main.py │ ├── opentelemetry.py │ ├── py.typed │ ├── rpc/ │ │ ├── __init__.py │ │ ├── protocol.py │ │ └── zmqrpc.py │ ├── runners.py │ ├── shape.py │ ├── stats.py │ ├── test/ │ │ ├── __init__.py │ │ ├── fake_module1_for_env_test.py │ │ ├── fake_module2_for_env_test.py │ │ ├── subprocess_utils.py │ │ ├── test_date.py │ │ ├── test_debugging.py │ │ ├── test_dispatch.py │ │ ├── test_env.py │ │ ├── test_fasthttp.py │ │ ├── test_html_filename.py │ │ ├── test_http.py │ │ ├── test_interruptable_task.py │ │ ├── test_load_locustfile.py │ │ ├── test_locust_class.py │ │ ├── test_log.py │ │ ├── test_main.py │ │ ├── test_markov_taskset.py │ │ ├── test_old_wait_api.py │ │ ├── test_parser.py │ │ ├── test_pytest_locustfile.py │ │ ├── test_runners.py │ │ ├── test_sequential_taskset.py │ │ ├── test_socketio.py │ │ ├── test_stats.py │ │ ├── test_tags.py │ │ ├── test_taskratio.py │ │ ├── test_users.py │ │ ├── test_util.py │ │ ├── test_wait_time.py │ │ ├── test_web.py │ │ ├── test_zmqrpc.py │ │ ├── testcases.py │ │ └── util.py │ ├── user/ │ │ ├── __init__.py │ │ ├── inspectuser.py │ │ ├── markov_taskset.py │ │ ├── sequential_taskset.py │ │ ├── task.py │ │ ├── users.py │ │ └── wait_time.py │ ├── util/ │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── date.py │ │ ├── deprecation.py │ │ ├── directory.py │ │ ├── exception_handler.py │ │ ├── load_locustfile.py │ │ ├── rounding.py │ │ ├── timespan.py │ │ └── url.py │ ├── web.py │ └── webui/ │ ├── .gitignore │ ├── .prettierrc │ ├── .yarn/ │ │ └── releases/ │ │ └── yarn-4.12.0.cjs │ ├── .yarnrc.yml │ ├── LICENSE │ ├── README.md │ ├── auth.html │ ├── dev.html │ ├── eslint.config.mjs │ ├── index.html │ ├── package.json │ ├── package.tgz │ ├── report.html │ ├── src/ │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── HtmlReport.tsx │ │ ├── assets/ │ │ │ └── Logo.tsx │ │ ├── components/ │ │ │ ├── DataTable/ │ │ │ │ ├── DataTable.test.tsx │ │ │ │ └── DataTable.tsx │ │ │ ├── ExceptionsTab/ │ │ │ │ └── ExceptionsTab.tsx │ │ │ ├── ExceptionsTable/ │ │ │ │ ├── ExceptionsTable.tsx │ │ │ │ └── ExceptionsTableContainer.tsx │ │ │ ├── FadeInBox.tsx │ │ │ ├── FailuresTable/ │ │ │ │ ├── FailuresTable.tsx │ │ │ │ └── FailuresTableContainer.tsx │ │ │ ├── FallbackRender/ │ │ │ │ ├── FallbackRender.test.tsx │ │ │ │ └── FallbackRender.tsx │ │ │ ├── Form/ │ │ │ │ ├── CustomInput.tsx │ │ │ │ ├── Form.tsx │ │ │ │ ├── NumericField.tsx │ │ │ │ ├── PasswordField.tsx │ │ │ │ ├── Select.tsx │ │ │ │ └── tests/ │ │ │ │ ├── CustomInput.test.tsx │ │ │ │ ├── Form.test.tsx │ │ │ │ ├── NumericField.test.tsx │ │ │ │ ├── PasswordField.test.tsx │ │ │ │ └── Select.test.tsx │ │ │ ├── Layout/ │ │ │ │ ├── Footer/ │ │ │ │ │ ├── About.tsx │ │ │ │ │ └── Footer.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ └── Navbar/ │ │ │ │ ├── DarkLightToggle.tsx │ │ │ │ ├── Navbar.tsx │ │ │ │ ├── SwarmMonitor.test.tsx │ │ │ │ └── SwarmMonitor.tsx │ │ │ ├── LineChart/ │ │ │ │ ├── LineChart.constants.ts │ │ │ │ ├── LineChart.tsx │ │ │ │ ├── LineChart.types.ts │ │ │ │ ├── LineChart.utils.ts │ │ │ │ └── tests/ │ │ │ │ ├── LineChart.mocks.ts │ │ │ │ ├── LineChart.test.tsx │ │ │ │ └── LineChartUtils.test.tsx │ │ │ ├── LogViewer/ │ │ │ │ ├── LogDisplay.tsx │ │ │ │ ├── LogViewer.tsx │ │ │ │ ├── LogViewer.utils.tsx │ │ │ │ ├── WorkerLogs.tsx │ │ │ │ ├── tests/ │ │ │ │ │ ├── LogViewer.test.tsx │ │ │ │ │ ├── WorkerLogs.test.tsx │ │ │ │ │ └── useLogViewer.test.tsx │ │ │ │ └── useLogViewer.ts │ │ │ ├── Modal/ │ │ │ │ └── Modal.tsx │ │ │ ├── Reports/ │ │ │ │ ├── Reports.test.tsx │ │ │ │ └── Reports.tsx │ │ │ ├── ResponseTimeTable/ │ │ │ │ ├── ResponseTimeTable.test.tsx │ │ │ │ └── ResponseTimeTable.tsx │ │ │ ├── StateButtons/ │ │ │ │ ├── EditButton.tsx │ │ │ │ ├── NewTestButton.tsx │ │ │ │ ├── ResetButton.tsx │ │ │ │ ├── StateButtons.tsx │ │ │ │ ├── StopButton.tsx │ │ │ │ └── tests/ │ │ │ │ ├── ResetButton.test.tsx │ │ │ │ ├── StateButtons.test.tsx │ │ │ │ └── StopButton.test.tsx │ │ │ ├── StatsTable/ │ │ │ │ ├── StatsTable.tsx │ │ │ │ └── StatsTableContainer.tsx │ │ │ ├── SwarmCharts/ │ │ │ │ ├── SwarmCharts.tsx │ │ │ │ └── SwarmChartsContainer.tsx │ │ │ ├── SwarmForm/ │ │ │ │ ├── LoadingButton.tsx │ │ │ │ ├── SwarmCustomParameters.tsx │ │ │ │ ├── SwarmForm.tsx │ │ │ │ ├── SwarmUserClassPicker.tsx │ │ │ │ └── tests/ │ │ │ │ ├── SwarmCustomParameters.test.tsx │ │ │ │ ├── SwarmForm.test.tsx │ │ │ │ └── SwarmUserClassPicker.test.tsx │ │ │ ├── SwarmRatios/ │ │ │ │ ├── SwarmRatios.test.tsx │ │ │ │ ├── SwarmRatios.tsx │ │ │ │ └── SwarmRatiosContainer.tsx │ │ │ ├── SwarmRatiosTab/ │ │ │ │ └── SwarmRatiosTab.tsx │ │ │ ├── Table/ │ │ │ │ ├── Table.test.tsx │ │ │ │ └── Table.tsx │ │ │ ├── Tabs/ │ │ │ │ ├── Tabs.constants.tsx │ │ │ │ ├── Tabs.test.tsx │ │ │ │ └── Tabs.tsx │ │ │ ├── ViewColumnSelector/ │ │ │ │ ├── ViewColumnSelector.test.tsx │ │ │ │ └── ViewColumnSelector.tsx │ │ │ └── WorkersTable/ │ │ │ └── WorkersTable.tsx │ │ ├── constants/ │ │ │ ├── auth.ts │ │ │ ├── logs.ts │ │ │ ├── swarm.ts │ │ │ └── theme.ts │ │ ├── global.d.ts │ │ ├── hooks/ │ │ │ ├── tests/ │ │ │ │ ├── useFetchExceptions.test.tsx │ │ │ │ ├── useFetchStats.test.tsx │ │ │ │ ├── useFetchTasks.test.tsx │ │ │ │ ├── useNotifications.test.tsx │ │ │ │ ├── useSelecteViewColumns.test.tsx │ │ │ │ └── useSortByField.test.tsx │ │ │ ├── useCreateTheme.ts │ │ │ ├── useFetchExceptions.ts │ │ │ ├── useFetchStats.ts │ │ │ ├── useFetchTasks.ts │ │ │ ├── useFetchWorkerCount.ts │ │ │ ├── useForm.ts │ │ │ ├── useInterval.ts │ │ │ ├── useNotifications.ts │ │ │ ├── useSelectViewColumns.ts │ │ │ └── useSortByField.ts │ │ ├── images.d.ts │ │ ├── index.tsx │ │ ├── lib.tsx │ │ ├── pages/ │ │ │ ├── Auth.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── HtmlReport.tsx │ │ │ └── tests/ │ │ │ ├── Auth.test.tsx │ │ │ ├── Dashboard.test.tsx │ │ │ └── HtmlReport.test.tsx │ │ ├── redux/ │ │ │ ├── api/ │ │ │ │ └── swarm.ts │ │ │ ├── hooks.ts │ │ │ ├── slice/ │ │ │ │ ├── logViewer.slice.ts │ │ │ │ ├── notification.slice.ts │ │ │ │ ├── root.slice.ts │ │ │ │ ├── swarm.slice.ts │ │ │ │ ├── tests/ │ │ │ │ │ └── ui.slice.test.ts │ │ │ │ ├── theme.slice.ts │ │ │ │ ├── ui.slice.ts │ │ │ │ └── url.slice.ts │ │ │ ├── store.ts │ │ │ └── utils.ts │ │ ├── styles/ │ │ │ └── theme.ts │ │ ├── test/ │ │ │ ├── constants.ts │ │ │ ├── mocks/ │ │ │ │ ├── statsRequest.mock.ts │ │ │ │ └── swarmState.mock.ts │ │ │ ├── setup.ts │ │ │ └── testUtils.tsx │ │ ├── types/ │ │ │ ├── auth.types.ts │ │ │ ├── form.types.ts │ │ │ ├── swarm.types.ts │ │ │ ├── tab.types.ts │ │ │ ├── table.types.ts │ │ │ ├── ui.types.ts │ │ │ └── window.types.ts │ │ └── utils/ │ │ ├── array.ts │ │ ├── date.ts │ │ ├── number.ts │ │ ├── object.ts │ │ ├── string.ts │ │ ├── tests/ │ │ │ ├── number.test.ts │ │ │ ├── object.test.ts │ │ │ ├── string.test.ts │ │ │ └── url.test.ts │ │ └── url.ts │ ├── tsconfig.json │ ├── vite.config.ts │ ├── vite.lib.config.ts │ ├── vite.report.config.ts │ └── vitest.config.ts ├── package.json ├── pyproject.toml └── pytest_locust/ └── plugin.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ locust.egg-info/** locustio.egg-info/** build/ .coverage docs/_build # Dockerfile # We'd like to ignore this, but it messes up scm_version's detection because it thinks the checkout is dirty ================================================ FILE: .git-blame-ignore-revs ================================================ # Migrate code style to Black 7c0fcc213d3988f6e7c6ffef63b24afe00e5fbd9 2e7a8b5697a98d1d314d6fc3ef0589f81f09d7fe # upgrade code style to 3.6 using pyupgrade 6ec972f4dbb880bf0c7a11809e6c1ba194c9784c # upgrade code style to use f-strings using flynt 313b80f27f525441c449593a3aeaf38389f63c13 # upgrade typing annotations using fix-future-annotations b5324820b299b1fe7da0608f0cc8ec47f58b1e40 # upgrade code style to 3.8 using pyupgrade 60f3bceacc4ab9567433d40ae3ed280750f55ff1 # sort imports using ruff f99e9df700a8020e4c1967eb42dcb37ddd26e296 # apply ruff 0.3.0 64428a0b4dfc75a00b175b4231db33704d8f5d36 # apply ruff 0.10.0 92268e16d5666d429ccea1742ecb90475f284c89 496c5bd40347b97f917ccacfaf57ceed8f6690b5 ================================================ FILE: .gitattributes ================================================ # Set default behaviour, in case users don't have core.autocrlf set. * text=auto # Explicitly declare text files we want to always be normalized and converted # to native line endings on checkout. *.c text *.h text *.py text *.js text *.rst text *.css text *.html text # Declare files that will always have CRLF line endings on checkout. *.sln text eol=crlf # Denote all files that are truly binary and should not be modified. *.png binary *.jpg binary *.gif binary ================================================ FILE: .github/CONTRIBUTING.md ================================================ ## Release Process * Install github_changelog_generator (https://github.com/github-changelog-generator/github-changelog-generator/) if not installed * Run github_changelog_generator to update `CHANGELOG.md` - `make changelog` * Update `locust/__init__.py` with new version number: `__version__ = "VERSION"` * Make git tag * Push git tag * Update Automated Builds configuration in Docker Hub so that the newly created git tag is built as the "latest" docker tag ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug description: Report an error labels: ["bug"] body: - type: checkboxes attributes: label: Prerequisites options: - label: I am using [the latest version of Locust](https://github.com/locustio/locust/releases/) required: true - label: I am reporting a bug, not asking a question required: true - type: markdown attributes: value: | If you just need help with something, then: * [Check the documentation](https://docs.locust.io/en/stable/) * [Check the FAQ in the wiki](https://github.com/locustio/locust/wiki/FAQ) * [Check the Github Discussions](https://github.com/orgs/locustio/discussions) * Search [Stack Overflow](https://stackoverflow.com/questions/tagged/locust), or [ask there](https://stackoverflow.com/questions/ask) yourself. If you tag your question with `locust` we will see it. * Ask on [Locust's slack](https://locustio.slack.com) [(sign up here)](https://communityinviter.com/apps/locustio/locust) - type: textarea attributes: label: Description description: What happened, and what did you want/expect to happen? validations: required: true - type: input attributes: label: Command line description: | For example: locust -f mylocustfile.py -t 10s --headless validations: required: true - type: textarea attributes: label: Locustfile contents description: Please remove everything that isn't necessary to trigger the issue. render: python3 validations: required: true - type: input attributes: label: Python version validations: required: true - type: input attributes: label: Locust version validations: required: true - type: input attributes: label: Operating system validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: Suggest an improvement labels: ["feature request"] body: - type: checkboxes attributes: label: Prerequisites options: - label: I am using [the latest version of Locust](https://github.com/locustio/locust/releases/) required: true - label: I am suggesting a new feature, not asking a question required: true - type: markdown attributes: value: | If you just need help with something, then: * [Check the documentation](https://docs.locust.io/en/stable/) * [Check the FAQ in the wiki](https://github.com/locustio/locust/wiki/FAQ) * [Check the Github Discussions](https://github.com/orgs/locustio/discussions) * Search [Stack Overflow](https://stackoverflow.com/questions/tagged/locust), or [ask there](https://stackoverflow.com/questions/ask) yourself. If you tag your question with `locust` we will see it. * Ask on [Locust's slack](https://locustio.slack.com) [(sign up here)](https://communityinviter.com/apps/locustio/locust) - type: textarea attributes: label: Description description: Describe what feature you are missing, and what alternatives you have considered. validations: required: true ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: semiannually groups: all_dependencies: patterns: - "*" - package-ecosystem: uv directory: / schedule: interval: semiannually - package-ecosystem: npm directory: /locust/webui schedule: interval: semiannually groups: eslint: patterns: - "*eslint*" vite: patterns: - "*vite*" react: patterns: - "*react*" emotion: patterns: - "*emotion*" mui: patterns: - "*mui*" ================================================ FILE: .github/workflows/stale.yml ================================================ name: Mark stale issues on: schedule: - cron: "30 1 * * *" workflow_dispatch: permissions: contents: read jobs: stale: permissions: issues: write # for actions/stale to close stale issues pull-requests: write # for actions/stale to close stale PRs runs-on: ubuntu-latest steps: - uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 20 days." close-issue-message: "This issue was closed because it has been marked stale for 20 days with no activity. This does not necessarily mean that the issue is bad, but it most likely means that nobody is willing to take the time to fix it. If you have found Locust useful, then consider contributing a fix yourself!" stale-pr-message: "This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 20 days." close-pr-message: "This PR was closed because it has been marked stale for 20 days with no activity." days-before-close: 20 ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: - master tags: - "*" paths-ignore: - "**.md" pull_request: paths-ignore: - "**.md" permissions: contents: read defaults: run: shell: bash jobs: #------------------------- # Building #------------------------- build_package: name: Build and Cache Packages runs-on: ubuntu-latest outputs: tag: ${{ steps.set_tag.outputs.tag }} tag_short: ${{ steps.set_tag_short.outputs.tag_short }} branch: ${{ steps.set_branch.outputs.branch }} is_merge_commit: ${{ steps.set_is_merge_commit.outputs.is_merge_commit }} is_tag_build: ${{ steps.set_is_tag_build.outputs.is_tag_build }} python_version: ${{ steps.set_python_version.outputs.python_version }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 fetch-tags: true - uses: actions/setup-python@v6 with: python-version: "3.11" - uses: astral-sh/setup-uv@v7 with: version: "0.9.7" enable-cache: true cache-dependency-glob: | "pyproject.toml" "uv.lock" - run: uv venv # # ensure uv.lock doesnt contain anything not in pyproject.toml # - run: uv lock --check - name: Install the project run: uv sync # any local changes would make hatch-vcs set a "local version" (+dev0...), so we ignore any uv.lock updates: - run: git update-index --assume-unchanged uv.lock # Install node and yarn in order to build the front end during packaging - name: Set Node.js 22.x uses: actions/setup-node@v6 with: node-version: 22.x cache: "yarn" cache-dependency-path: locust/webui/yarn.lock # Build and upload the project artifacts only once - name: Build Python project and front end run: uv build - name: Upload Python dist as Artifact uses: actions/upload-artifact@v7 with: name: python-dist path: dist/* - name: Upload Web UI as Artifact uses: actions/upload-artifact@v7 with: name: webui-dist path: locust/webui/dist/* - name: Build UI library run: yarn webui:build:lib - name: Upload web UI library as Artifact uses: actions/upload-artifact@v7 with: name: webui-lib-dist path: locust/webui/lib # Set workflow metadata in one place so we can pull it out later - id: set_tag run: echo "tag=$(uv run hatch version)" | tee -a "$GITHUB_OUTPUT" - id: set_tag_short run: echo "tag_short=$(uv run hatch version | cut -d '.' -f1-3)" | tee -a "$GITHUB_OUTPUT" - id: set_branch run: echo "branch=${{ github.head_ref || github.ref_name }}" | tee -a "$GITHUB_OUTPUT" - id: set_is_merge_commit run: echo "is_merge_commit=$( [ $(git rev-list --count $GITHUB_SHA^@) -eq 2 ] && echo 'true' || echo 'false' )" | tee -a "$GITHUB_OUTPUT" - id: set_is_tag_build run: echo "is_tag_build=${{ startsWith(github.event.ref, 'refs/tags') }}" | tee -a "$GITHUB_OUTPUT" - id: set_python_version run: echo "python_version=$(python -VV | sha256sum | cut -d' ' -f1)" | tee -a "$GITHUB_OUTPUT" print_metadata: name: Display metadata for build runs-on: ubuntu-latest needs: build_package steps: - run: | echo "tag: ${{ needs.build_package.outputs.tag }}" echo "tag_short: ${{ needs.build_package.outputs.tag_short }}" echo "branch: ${{ needs.build_package.outputs.branch }}" echo "is_merge_commit: ${{ needs.build_package.outputs.is_merge_commit }}" echo "is_tag_build: ${{ needs.build_package.outputs.is_tag_build }}" echo "python_version: ${{ needs.build_package.outputs.python_version }}" #------------------------- # Testing #------------------------- tests: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} needs: build_package strategy: fail-fast: false matrix: include: # Static analysis and utilities - { name: "Ruff", python: "3.12", os: ubuntu-latest, group: "lint", env: "lint:format", } - { name: "Mypy", python: "3.12", os: ubuntu-latest, group: "lint", env: "lint:types", } # Verification of builds and other aspects - { name: "Docs Build", python: "3.12", os: ubuntu-latest, group: "docs", env: "docs:build", } # OS Integration tests - { name: "Linux", python: "3.12", os: ubuntu-latest, group: "test", env: "integration_test_ci:fail_fast", } - { name: "Windows", python: "3.12", os: windows-latest, group: "test", env: "integration_test_ci:fail_fast", } - { name: "Linux_pytest_locustfile", python: "3.12", os: ubuntu-latest, group: "test", env: "integration_test_ci:test_pytest", } # MacOS behaves super badly on GH actions right now, so let's disable it for the time being # - { name: "MacOS", python: '3.12', os: macos-latest, group: "test", env: "integration_test_ci:fail_fast" } # Unit tests on Python versions - { name: "Python 3.14", python: "3.14", os: ubuntu-latest, group: "test", env: "test:all", } - { name: "Python 3.13", python: "3.13", os: ubuntu-latest, group: "test", env: "test:all", } - { name: "Python 3.12", python: "3.12", os: ubuntu-latest, group: "test", env: "test:all", } - { name: "Python 3.11", python: "3.11", os: ubuntu-latest, group: "test", env: "test:all", } - { name: "Python 3.10", python: "3.10", os: ubuntu-latest, group: "test", env: "test:all", } steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - uses: astral-sh/setup-uv@v7 with: version: "0.9.7" enable-cache: true cache-dependency-glob: | "pyproject.toml" "uv.lock" - run: uv venv --python ${{ matrix.python }} # Install what we need to run hatch envs but not the project itself - name: Install CI dependencies run: uv sync --group ${{ matrix.group }} --no-install-package locust # Grab the built artifacts to ensure we're testing what we eventually publish - name: Download Python dist uses: actions/download-artifact@v8 with: name: python-dist path: dist - name: Download WebUI dist uses: actions/download-artifact@v8 with: name: webui-dist path: locust/webui/dist - name: Run tests run: uv run --group ${{ matrix.group }} hatch run +py=${{ matrix.python }} ${{ matrix.env }} test_docker_image: name: Test Docker Image runs-on: ubuntu-latest needs: build_package steps: - uses: actions/checkout@v6 with: fetch-depth: 0 # Grab the built artifacts to ensure we're testing what we eventually publish - name: Download Python dist uses: actions/download-artifact@v8 with: name: python-dist path: dist # Set up Docker daemon dependencies for building and publishing - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # Build and load Docker image to the local daemon - name: Build and push uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile.ci platforms: linux/amd64 load: true tags: locustio/locust:${{ github.sha }}-test # Run a basic test on the image - name: Test docker image run: | docker run --rm locustio/locust:${{ github.sha }}-test --version test_webui: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 22.x cache: "yarn" cache-dependency-path: locust/webui/yarn.lock - name: yarn install uses: borales/actions-yarn@v5 with: cmd: install dir: locust/webui - name: yarn build uses: borales/actions-yarn@v5 with: cmd: build dir: locust/webui - name: yarn test uses: borales/actions-yarn@v5 with: cmd: test dir: locust/webui - name: yarn lint uses: borales/actions-yarn@v5 with: cmd: lint dir: locust/webui - name: yarn type-check uses: borales/actions-yarn@v5 with: cmd: type-check dir: locust/webui # ------------------------- # Publishing # ------------------------- publish-docker: needs: [tests, test_webui, test_docker_image, build_package] if: github.repository_owner == 'locustio' && ( github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags') ) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 # Download Python dist artifact - name: Download Python dist uses: actions/download-artifact@v8 with: name: python-dist path: dist # Download Web UI lib artifact - name: Download UI lib uses: actions/download-artifact@v8 with: name: webui-lib-dist path: locust/webui/lib # Staged docker builds using exports/artifacts is currently difficult using multi-arch builds with buildx # So let's just build it here # Set docker image and tag values - name: Docker meta id: docker_meta uses: docker/metadata-action@v5 with: images: locustio/locust tags: | type=raw,value=latest,enable=${{ needs.build_package.outputs.is_tag_build }} type=raw,value=${{ needs.build_package.outputs.tag }} type=raw,value=${{ needs.build_package.outputs.branch }} - uses: docker/login-action@v3 with: username: locustbuild password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and (optionally) push docker image uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile.ci platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} publish-pypi: needs: [tests, test_webui, test_docker_image, build_package] if: github.repository_owner == 'locustio' && ( github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags') ) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 # Download Python dist artifact - name: Download Python dist uses: actions/download-artifact@v8 with: name: python-dist path: dist # Download Web UI lib artifact - name: Download UI lib uses: actions/download-artifact@v8 with: name: webui-lib-dist path: locust/webui/lib - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} skip-existing: true - name: yarn install uses: borales/actions-yarn@v5 with: cmd: install dir: locust/webui # Set pre-release version - name: yarn version uses: borales/actions-yarn@v5 if: github.ref == 'refs/heads/master' with: cmd: version ${{ needs.build_package.outputs.tag_short }}-next-${{ github.run_id }} dir: locust/webui # Set tag build version - name: yarn version uses: borales/actions-yarn@v5 if: startsWith(github.event.ref, 'refs/tags') with: cmd: version ${{ github.ref_name }} dir: locust/webui - name: yarn config uses: borales/actions-yarn@v5 with: cmd: config set npmAuthToken ${{ secrets.NPM_AUTH_TOKEN }} dir: locust/webui # Publish UI lib - uses: borales/actions-yarn@v5 name: Publish package on NPM if: github.ref == 'refs/heads/master' with: cmd: npm publish --tag next dir: locust/webui # On tag builds - uses: borales/actions-yarn@v5 name: Publish package on NPM if: startsWith(github.event.ref, 'refs/tags') with: cmd: npm publish dir: locust/webui ================================================ FILE: .gitignore ================================================ *.pyc locust.wpr locust.egg-info/** locustio.egg-info/** locust/_version.py locust/test/mock_*.py docs/_build/** docs/cli-help-output.txt docs/config-options.rst mock.*.egg web_test_*.csv err.txt out.txt .eggs/ dist/** .idea/** *.iml *.ipr .vagrant build/ .coverage docs/env-options.rst .editorconfig __pycache__ .pytest_cache .sass-cache/ .env yarn-error.log .venv .DS_Store .python-version locust/webui/dist/** .coverage* ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.10.0 # Pin the same version in the pyproject.toml file hooks: # Run the linter. - id: ruff args: [--fix] # Run the formatter. - id: ruff-format - repo: local hooks: - id: yarn-lint name: yarn lint files: ^locust/webui/ entry: bash -c "cd locust/webui && yarn lint" language: system pass_filenames: false ================================================ FILE: .readthedocs.yaml ================================================ version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" jobs: post_checkout: - git fetch --unshallow || true post_install: - python -m pip install uv - SKIP_PRE_BUILD=true UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs sphinx: configuration: docs/conf.py ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "charliermarsh.ruff", "ms-python.mypy-type-checker" ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Run current file", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "gevent": true }, { "name": "Run current locust scenario headless", "type": "debugpy", "request": "launch", "module": "locust", "args": [ "-f", "${file}", "--headless", "--users=5" ], "console": "integratedTerminal", "gevent": true }, { "name": "Run current locust scenario, autostart", "type": "debugpy", "request": "launch", "module": "locust", "args": [ "-f", "${file}", "--users=5", "--autostart", "--print-stats", "-L=ERROR" ], "console": "integratedTerminal", "gevent": true }, { "name": "(test debug only)", "type": "debugpy", "request": "launch", "gevent": true, "purpose": ["debug-test"] } ] } ================================================ FILE: .vscode/launch_locust.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Locust: 5 users, with specific config file", "type": "python", "request": "launch", "module": "locust", "args": [ "-f", "${file}", "--headless", "--users=5", "--config=${fileDirname}/../locust.conf" ], "console": "integratedTerminal", "gevent": true } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "files.exclude": { ".pytest_cache/**/*": true, "**/*.pyc": true, "**/locust.wpr": true, "locust.egg-info/**": true, "locustio.egg-info/**": true, "docs/_build/**": true, "docs/cli-help-output.txt": true, "docs/config-options.rst": true, "**/mock.*.egg": true, "dist/**": true, ".idea/**": true, "**/*.iml": true, "**/*.ipr": true, "**/.vagrant": true, "**/build/": true, "**/.coverage": true, "**/.tox/": true, "docs/env-options.rst": true, "**/.editorconfig": true, "**/__pycache__": true, "locust/_version.py": true, "**/web_test_*.csv": true, "**/.eggs/": true, "**/.pytest_cache": true, "**/.sass-cache/": true, "locust/contrib/.mypy_cache/**/*": true, "locust/user/.mypy_cache/**/*": true, "**/.mypy_cache": true, "**/.ruff_cache": true, "locust/test/mock_*.py": true, "**/err.txt": true, "**/out.txt": true, "**/.venv": true }, "restructuredtext.confPath": "${workspaceFolder}/docs", "[python]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.organizeImports": "explicit" }, "editor.defaultFormatter": "charliermarsh.ruff" }, "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false } ================================================ FILE: CHANGELOG.md ================================================ # Detailed changelog The most important changes can also be found in [the documentation](https://docs.locust.io/en/latest/changelog.html). ## [2.43.3](https://github.com/locustio/locust/tree/2.43.3) (2026-02-12) [Full Changelog](https://github.com/locustio/locust/compare/2.43.2...2.43.3) **Closed issues:** - Documentation Improvement - Event hooks [\#3338](https://github.com/locustio/locust/issues/3338) **Merged pull requests:** - Bump diff from 8.0.2 to 8.0.3 in /locust/webui [\#3349](https://github.com/locustio/locust/pull/3349) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump lodash from 4.17.21 to 4.17.23 in /locust/webui [\#3348](https://github.com/locustio/locust/pull/3348) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump cryptography from 43.0.3 to 46.0.5 [\#3347](https://github.com/locustio/locust/pull/3347) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump tar from 7.4.3 to 7.5.7 in /locust/webui [\#3346](https://github.com/locustio/locust/pull/3346) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 in /locust/webui [\#3345](https://github.com/locustio/locust/pull/3345) ([dependabot[bot]](https://github.com/apps/dependabot)) - Add missing event hook parameter documentation [\#3344](https://github.com/locustio/locust/pull/3344) ([veeceey](https://github.com/veeceey)) ## [2.43.2](https://github.com/locustio/locust/tree/2.43.2) (2026-02-01) [Full Changelog](https://github.com/locustio/locust/compare/2.43.1...2.43.2) **Merged pull requests:** - Bump flask-cors from 6.0.1 to 6.0.2 [\#3343](https://github.com/locustio/locust/pull/3343) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump hatch from 1.16.2 to 1.16.3 [\#3342](https://github.com/locustio/locust/pull/3342) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump werkzeug from 3.1.4 to 3.1.5 [\#3341](https://github.com/locustio/locust/pull/3341) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump opentelemetry-exporter-otlp-proto-grpc from 1.39.0 to 1.39.1 [\#3340](https://github.com/locustio/locust/pull/3340) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump sphinx-rtd-theme from 3.0.2 to 3.1.0 [\#3339](https://github.com/locustio/locust/pull/3339) ([dependabot[bot]](https://github.com/apps/dependabot)) - Update operator docs [\#3333](https://github.com/locustio/locust/pull/3333) ([amadeuppereira](https://github.com/amadeuppereira)) ## [2.43.1](https://github.com/locustio/locust/tree/2.43.1) (2026-01-12) [Full Changelog](https://github.com/locustio/locust/compare/2.43.0...2.43.1) **Merged pull requests:** - Bump packages [\#3331](https://github.com/locustio/locust/pull/3331) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Stabilize tests [\#3326](https://github.com/locustio/locust/pull/3326) ([amadeuppereira](https://github.com/amadeuppereira)) - Stabilize tests [\#3325](https://github.com/locustio/locust/pull/3325) ([amadeuppereira](https://github.com/amadeuppereira)) - Bump @emotion/styled from 11.14.0 to 11.14.1 in /locust/webui in the emotion group [\#3322](https://github.com/locustio/locust/pull/3322) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump the eslint group in /locust/webui with 5 updates [\#3319](https://github.com/locustio/locust/pull/3319) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump the all\_dependencies group with 2 updates [\#3318](https://github.com/locustio/locust/pull/3318) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [2.43.0](https://github.com/locustio/locust/tree/2.43.0) (2025-12-30) [Full Changelog](https://github.com/locustio/locust/compare/2.42.6...2.43.0) **Closed issues:** - Support for requests \>=2.32.5 \(compatibility with LangChain/AI ecosystem\) [\#3307](https://github.com/locustio/locust/issues/3307) - Multiple select in web UI for custom arguments [\#3260](https://github.com/locustio/locust/issues/3260) - Suggestion to Add "iter\_lines" Support for "FastHttpUser" in Locust [\#3018](https://github.com/locustio/locust/issues/3018) **Merged pull requests:** - Provide a better error message when spawn rate is set to zero [\#3317](https://github.com/locustio/locust/pull/3317) ([amadeuppereira](https://github.com/amadeuppereira)) - Support requests\>=2.32.5, reimplement the fix previously there for only loading ssl certificates once [\#3316](https://github.com/locustio/locust/pull/3316) ([amadeuppereira](https://github.com/amadeuppereira)) - Remove references to locust.cloud now that it is shutting down [\#3314](https://github.com/locustio/locust/pull/3314) ([amadeuppereira](https://github.com/amadeuppereira)) - Allow users to stop test run by raising StopTest, use it on missing host in locustfile \(and no --host param\) [\#3313](https://github.com/locustio/locust/pull/3313) ([amadeuppereira](https://github.com/amadeuppereira)) - Locust Cloud demo tab: update domain from auth.locust.cloud to app.locust.cloud [\#3312](https://github.com/locustio/locust/pull/3312) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Solving the iter\_lines problem [\#3311](https://github.com/locustio/locust/pull/3311) ([MasterKey-Pro](https://github.com/MasterKey-Pro)) - Refactor parse\_options [\#3310](https://github.com/locustio/locust/pull/3310) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Capture stacktrace on KeyboardInterrupt on greenlets [\#3306](https://github.com/locustio/locust/pull/3306) ([amadeuppereira](https://github.com/amadeuppereira)) - Bump js-yaml from 4.1.0 to 4.1.1 in /locust/webui [\#3305](https://github.com/locustio/locust/pull/3305) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bump glob from 10.4.5 to 10.5.0 in /locust/webui [\#3304](https://github.com/locustio/locust/pull/3304) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [2.42.6](https://github.com/locustio/locust/tree/2.42.6) (2025-11-29) [Full Changelog](https://github.com/locustio/locust/compare/2.42.5...2.42.6) **Fixed bugs:** - After Locust upgrade, parameter 'headless = true' in conf file does not work as expected. [\#3225](https://github.com/locustio/locust/issues/3225) **Merged pull requests:** - Don't import pytest unless it is really needed, to speed up startup [\#3302](https://github.com/locustio/locust/pull/3302) ([cyberw](https://github.com/cyberw)) - refactor case statements and update to use 3.10 set syntax [\#3301](https://github.com/locustio/locust/pull/3301) ([cyberw](https://github.com/cyberw)) - Update OTel setup [\#3300](https://github.com/locustio/locust/pull/3300) ([amadeuppereira](https://github.com/amadeuppereira)) - Use match-case instead of gigantic if-elif statement when handling zmq messages in master-worker communication [\#3299](https://github.com/locustio/locust/pull/3299) ([cyberw](https://github.com/cyberw)) - Add OTel documentation [\#3298](https://github.com/locustio/locust/pull/3298) ([amadeuppereira](https://github.com/amadeuppereira)) - Improve tests [\#3297](https://github.com/locustio/locust/pull/3297) ([amadeuppereira](https://github.com/amadeuppereira)) - Log duplicate client\_ready messages as debug instead of info level [\#3296](https://github.com/locustio/locust/pull/3296) ([cyberw](https://github.com/cyberw)) - Add otel unit tests [\#3295](https://github.com/locustio/locust/pull/3295) ([amadeuppereira](https://github.com/amadeuppereira)) - Only log "OpenTelemetry enabled" message when success [\#3294](https://github.com/locustio/locust/pull/3294) ([amadeuppereira](https://github.com/amadeuppereira)) - Fix Toml Parser Being Called on Conf Files [\#3293](https://github.com/locustio/locust/pull/3293) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - GH Actions: Bump actions/checkout from 5 to 6 in the all\_dependencies group [\#3287](https://github.com/locustio/locust/pull/3287) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [2.42.5](https://github.com/locustio/locust/tree/2.42.5) (2025-11-20) [Full Changelog](https://github.com/locustio/locust/compare/2.42.4...2.42.5) **Merged pull requests:** - Log when otel is enabled [\#3284](https://github.com/locustio/locust/pull/3284) ([amadeuppereira](https://github.com/amadeuppereira)) ## [2.42.4](https://github.com/locustio/locust/tree/2.42.4) (2025-11-20) [Full Changelog](https://github.com/locustio/locust/compare/2.42.3...2.42.4) **Closed issues:** - bumb pytest to support 9.x [\#3262](https://github.com/locustio/locust/issues/3262) **Merged pull requests:** - Avoid duplicated action run on pull requests [\#3282](https://github.com/locustio/locust/pull/3282) ([jairhenrique](https://github.com/jairhenrique)) - Forward explicitly set request name to OTEL span [\#3281](https://github.com/locustio/locust/pull/3281) ([amadeuppereira](https://github.com/amadeuppereira)) - Verbose request span name [\#3279](https://github.com/locustio/locust/pull/3279) ([amadeuppereira](https://github.com/amadeuppereira)) - Add OpenTelemetry flag [\#3278](https://github.com/locustio/locust/pull/3278) ([amadeuppereira](https://github.com/amadeuppereira)) - Disable uv.lock check in GH action [\#3277](https://github.com/locustio/locust/pull/3277) ([cyberw](https://github.com/cyberw)) - Add otlp http exporter dependency [\#3276](https://github.com/locustio/locust/pull/3276) ([amadeuppereira](https://github.com/amadeuppereira)) - Add OpenTelemetry support [\#3275](https://github.com/locustio/locust/pull/3275) ([amadeuppereira](https://github.com/amadeuppereira)) - Bump the eslint group in /locust/webui with 11 updates [\#3270](https://github.com/locustio/locust/pull/3270) ([dependabot[bot]](https://github.com/apps/dependabot)) - Includes npm and uv on dependabot file [\#3269](https://github.com/locustio/locust/pull/3269) ([jairhenrique](https://github.com/jairhenrique)) - Enables FURB ruff lint [\#3265](https://github.com/locustio/locust/pull/3265) ([jairhenrique](https://github.com/jairhenrique)) - Updates pytest dependency range [\#3263](https://github.com/locustio/locust/pull/3263) ([jairhenrique](https://github.com/jairhenrique)) - Allow multiple dropdown in Web UI [\#3261](https://github.com/locustio/locust/pull/3261) ([jFompe](https://github.com/jFompe)) ## [2.42.3](https://github.com/locustio/locust/tree/2.42.3) (2025-11-15) [Full Changelog](https://github.com/locustio/locust/compare/2.42.2...2.42.3) **Merged pull requests:** - Bump the all\_dependencies group with 7 updates [\#3266](https://github.com/locustio/locust/pull/3266) ([dependabot[bot]](https://github.com/apps/dependabot)) - Creates dependabot file to keep gh actions updated [\#3264](https://github.com/locustio/locust/pull/3264) ([jairhenrique](https://github.com/jairhenrique)) - GH Actions: update uv version, ensure uv.lock doesn't contain anything not in pyproject.toml [\#3259](https://github.com/locustio/locust/pull/3259) ([cyberw](https://github.com/cyberw)) - Fix single line .conf files incorrectly being treated as toml [\#3257](https://github.com/locustio/locust/pull/3257) ([cyberw](https://github.com/cyberw)) ## [2.42.2](https://github.com/locustio/locust/tree/2.42.2) (2025-11-06) [Full Changelog](https://github.com/locustio/locust/compare/2.42.1...2.42.2) **Merged pull requests:** - Bump locust-cloud version, fixing an issue with .conf-files [\#3256](https://github.com/locustio/locust/pull/3256) ([cyberw](https://github.com/cyberw)) - Revert "modified ui\_extra\_args\_dict function to pick arguments" [\#3255](https://github.com/locustio/locust/pull/3255) ([cyberw](https://github.com/cyberw)) - Update uv.lock [\#3254](https://github.com/locustio/locust/pull/3254) ([cyberw](https://github.com/cyberw)) - remove setuptools from dependencies [\#3253](https://github.com/locustio/locust/pull/3253) ([dotlambda](https://github.com/dotlambda)) - fix: MQTT client\_id and protocol not passed down to Client [\#3252](https://github.com/locustio/locust/pull/3252) ([ionutab](https://github.com/ionutab)) ## [2.42.1](https://github.com/locustio/locust/tree/2.42.1) (2025-10-27) [Full Changelog](https://github.com/locustio/locust/compare/2.42.0...2.42.1) **Merged pull requests:** - Add VS Code Extension and k8s operator to documentation [\#3251](https://github.com/locustio/locust/pull/3251) ([cyberw](https://github.com/cyberw)) - Bump vite from 6.3.5 to 6.4.1 in /locust/webui [\#3249](https://github.com/locustio/locust/pull/3249) ([dependabot[bot]](https://github.com/apps/dependabot)) - Bumped the gRPC example server’s worker pool to 100 [\#3248](https://github.com/locustio/locust/pull/3248) ([sonianuj287](https://github.com/sonianuj287)) - modified ui\_extra\_args\_dict function to pick arguments [\#3245](https://github.com/locustio/locust/pull/3245) ([sonianuj287](https://github.com/sonianuj287)) ## [2.42.0](https://github.com/locustio/locust/tree/2.42.0) (2025-10-17) [Full Changelog](https://github.com/locustio/locust/compare/2.41.6...2.42.0) **Fixed bugs:** - Extend Locust UI with new tab does not work [\#3240](https://github.com/locustio/locust/issues/3240) - Reset button not working after stopping the run [\#3197](https://github.com/locustio/locust/issues/3197) **Merged pull requests:** - Avoid using most recent python-requests because it may introduce performance issues [\#3244](https://github.com/locustio/locust/pull/3244) ([cyberw](https://github.com/cyberw)) - Introduce DNSUser [\#3243](https://github.com/locustio/locust/pull/3243) ([cyberw](https://github.com/cyberw)) - Fix reset button not working after stopping the run [\#3238](https://github.com/locustio/locust/pull/3238) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Added readme badge for supported python versions [\#3237](https://github.com/locustio/locust/pull/3237) ([Nirzak](https://github.com/Nirzak)) - loosen dependency on gevent [\#3236](https://github.com/locustio/locust/pull/3236) ([bollwyvl](https://github.com/bollwyvl)) ## [2.41.6](https://github.com/locustio/locust/tree/2.41.6) (2025-10-10) [Full Changelog](https://github.com/locustio/locust/compare/2.41.5...2.41.6) **Merged pull requests:** - Officially support Python 3.14 and test it [\#3235](https://github.com/locustio/locust/pull/3235) ([cyberw](https://github.com/cyberw)) ## [2.41.5](https://github.com/locustio/locust/tree/2.41.5) (2025-10-06) [Full Changelog](https://github.com/locustio/locust/compare/2.41.4...2.41.5) **Merged pull requests:** - Use www host instead of bare locust.cloud in examples and tests [\#3234](https://github.com/locustio/locust/pull/3234) ([cyberw](https://github.com/cyberw)) ## [2.41.4](https://github.com/locustio/locust/tree/2.41.4) (2025-10-06) [Full Changelog](https://github.com/locustio/locust/compare/2.41.3...2.41.4) **Merged pull requests:** - Fix Unsafe Template Arg [\#3232](https://github.com/locustio/locust/pull/3232) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.41.3](https://github.com/locustio/locust/tree/2.41.3) (2025-10-01) [Full Changelog](https://github.com/locustio/locust/compare/2.41.2...2.41.3) **Merged pull requests:** - Make workers correctly ignore the --run-time flag [\#3230](https://github.com/locustio/locust/pull/3230) ([cyberw](https://github.com/cyberw)) - Enhance MilvusUser constructor to support additional collection parameters [\#3229](https://github.com/locustio/locust/pull/3229) ([zhuwenxing](https://github.com/zhuwenxing)) ## [2.41.2](https://github.com/locustio/locust/tree/2.41.2) (2025-09-29) [Full Changelog](https://github.com/locustio/locust/compare/2.41.1...2.41.2) **Fixed bugs:** - Locust pytest plugin option '--host' conflicts with common user options; suggest renaming to '--locust-host' [\#3227](https://github.com/locustio/locust/issues/3227) **Merged pull requests:** - Pytest plugin: Workaround issue with potential duplicate --host argument definition [\#3228](https://github.com/locustio/locust/pull/3228) ([cyberw](https://github.com/cyberw)) - Fix Alignment of View Column Selector [\#3226](https://github.com/locustio/locust/pull/3226) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.41.1](https://github.com/locustio/locust/tree/2.41.1) (2025-09-25) [Full Changelog](https://github.com/locustio/locust/compare/2.41.0...2.41.1) ## [2.41.0](https://github.com/locustio/locust/tree/2.41.0) (2025-09-25) [Full Changelog](https://github.com/locustio/locust/compare/2.40.5...2.41.0) **Fixed bugs:** - Master doesn't distribute user equally among workers [\#3209](https://github.com/locustio/locust/issues/3209) **Merged pull requests:** - Suggest possibly misspelled command line arguments \(Did you mean ...\) [\#3224](https://github.com/locustio/locust/pull/3224) ([cyberw](https://github.com/cyberw)) - Add Locust Feedback Form [\#3223](https://github.com/locustio/locust/pull/3223) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add MqttUser [\#3216](https://github.com/locustio/locust/pull/3216) ([ionutab](https://github.com/ionutab)) ## [2.40.5](https://github.com/locustio/locust/tree/2.40.5) (2025-09-17) [Full Changelog](https://github.com/locustio/locust/compare/2.40.4...2.40.5) **Merged pull requests:** - Avoid wrapping fast response in response context manager when not needed [\#3222](https://github.com/locustio/locust/pull/3222) ([cyberw](https://github.com/cyberw)) ## [2.40.4](https://github.com/locustio/locust/tree/2.40.4) (2025-09-11) [Full Changelog](https://github.com/locustio/locust/compare/2.40.3...2.40.4) **Merged pull requests:** - Avoid pytest style locustfiles capturing keyboard input [\#3219](https://github.com/locustio/locust/pull/3219) ([cyberw](https://github.com/cyberw)) ## [2.40.3](https://github.com/locustio/locust/tree/2.40.3) (2025-09-11) [Full Changelog](https://github.com/locustio/locust/compare/2.40.2...2.40.3) **Merged pull requests:** - Bump requests minimum version [\#3218](https://github.com/locustio/locust/pull/3218) ([cyberw](https://github.com/cyberw)) - Make ResponseContextManager.raise\_for\_status\(\) consider calls to failure\(\) and success\(\) [\#3217](https://github.com/locustio/locust/pull/3217) ([cyberw](https://github.com/cyberw)) ## [2.40.2](https://github.com/locustio/locust/tree/2.40.2) (2025-09-08) [Full Changelog](https://github.com/locustio/locust/compare/2.40.1...2.40.2) **Fixed bugs:** - Python 3.13: KeyError: 'name' on request\_meta in ResponseContextManager.\_\_exit\_\_ [\#3207](https://github.com/locustio/locust/issues/3207) **Merged pull requests:** - Refactor SocketIOUser to create separate SocketIOClient class [\#3211](https://github.com/locustio/locust/pull/3211) ([cyberw](https://github.com/cyberw)) - Refactor clients.ResponseContextManager and fix gc issue [\#3210](https://github.com/locustio/locust/pull/3210) ([cyberw](https://github.com/cyberw)) ## [2.40.1](https://github.com/locustio/locust/tree/2.40.1) (2025-09-05) [Full Changelog](https://github.com/locustio/locust/compare/2.40.0...2.40.1) **Fixed bugs:** - 2.40.0 with pytest and xdist [\#3202](https://github.com/locustio/locust/issues/3202) **Merged pull requests:** - Move pytest plugin to its own directory, to prevent accidental import [\#3205](https://github.com/locustio/locust/pull/3205) ([cyberw](https://github.com/cyberw)) - Pytest plugin: Delay imports to avoid monkey patching until someone uses the fixtures [\#3204](https://github.com/locustio/locust/pull/3204) ([cyberw](https://github.com/cyberw)) ## [2.40.0](https://github.com/locustio/locust/tree/2.40.0) (2025-09-04) [Full Changelog](https://github.com/locustio/locust/compare/2.39.1...2.40.0) **Fixed bugs:** - HTTP response 0 [\#3199](https://github.com/locustio/locust/issues/3199) **Merged pull requests:** - Avoid exception in HttpUser if requests has lost track of the request it made [\#3201](https://github.com/locustio/locust/pull/3201) ([cyberw](https://github.com/cyberw)) - Support pytests as locustfiles [\#3200](https://github.com/locustio/locust/pull/3200) ([cyberw](https://github.com/cyberw)) - Refactor FastHttpSession to be more like HttpSession [\#3198](https://github.com/locustio/locust/pull/3198) ([cyberw](https://github.com/cyberw)) - Update Dockerfile base to Python 3.13 [\#3193](https://github.com/locustio/locust/pull/3193) ([adaamz](https://github.com/adaamz)) ## [2.39.1](https://github.com/locustio/locust/tree/2.39.1) (2025-08-29) [Full Changelog](https://github.com/locustio/locust/compare/2.39.0...2.39.1) **Merged pull requests:** - Avoid broken gevent version for now [\#3196](https://github.com/locustio/locust/pull/3196) ([cyberw](https://github.com/cyberw)) - Remove duplicated line in pyproject.toml [\#3195](https://github.com/locustio/locust/pull/3195) ([JumboBear](https://github.com/JumboBear)) ## [2.39.0](https://github.com/locustio/locust/tree/2.39.0) (2025-08-19) [Full Changelog](https://github.com/locustio/locust/compare/2.38.1...2.39.0) **Merged pull requests:** - Add SocketIOUser [\#3189](https://github.com/locustio/locust/pull/3189) ([cyberw](https://github.com/cyberw)) - Add MilvusUser and example [\#3168](https://github.com/locustio/locust/pull/3168) ([zhuwenxing](https://github.com/zhuwenxing)) ## [2.38.1](https://github.com/locustio/locust/tree/2.38.1) (2025-08-12) [Full Changelog](https://github.com/locustio/locust/compare/2.38.0...2.38.1) **Closed issues:** - Support for markov chains to describe a user's behavior [\#3156](https://github.com/locustio/locust/issues/3156) - Switch docker base image to one with uv preinstalled [\#3061](https://github.com/locustio/locust/issues/3061) **Merged pull requests:** - FastHttpUser: Dont send zstd in Accept-Encoding header [\#3188](https://github.com/locustio/locust/pull/3188) ([cyberw](https://github.com/cyberw)) - Fix test flakyness and update error message [\#3187](https://github.com/locustio/locust/pull/3187) ([amadeuppereira](https://github.com/amadeuppereira)) ## [2.38.0](https://github.com/locustio/locust/tree/2.38.0) (2025-08-07) [Full Changelog](https://github.com/locustio/locust/compare/2.37.14...2.38.0) **Fixed bugs:** - Failures table sorting is reset to ascending after a few seconds [\#3184](https://github.com/locustio/locust/issues/3184) - argparse.ArgumentError: argument --profile: conflicting option string: --profile \(locust==2.37.14\) [\#3180](https://github.com/locustio/locust/issues/3180) **Merged pull requests:** - Webui: Fix useSortByField [\#3185](https://github.com/locustio/locust/pull/3185) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Optimize unit tests [\#3183](https://github.com/locustio/locust/pull/3183) ([amadeuppereira](https://github.com/amadeuppereira)) - Support for list based custom arguments [\#3181](https://github.com/locustio/locust/pull/3181) ([mickdwyer](https://github.com/mickdwyer)) - Bump form-data from 4.0.0 to 4.0.4 in /locust/webui [\#3179](https://github.com/locustio/locust/pull/3179) ([dependabot[bot]](https://github.com/apps/dependabot)) - Webui: Hide no Host Warning when one is Provided [\#3177](https://github.com/locustio/locust/pull/3177) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add MarkovTaskSet [\#3176](https://github.com/locustio/locust/pull/3176) ([Matthieu-Beauchamp](https://github.com/Matthieu-Beauchamp)) - Add yarn lint precommit [\#3175](https://github.com/locustio/locust/pull/3175) ([cyberw](https://github.com/cyberw)) ## [2.37.14](https://github.com/locustio/locust/tree/2.37.14) (2025-07-16) [Full Changelog](https://github.com/locustio/locust/compare/2.37.13...2.37.14) **Merged pull requests:** - Web UI: Fix Chart Zoom Slider [\#3174](https://github.com/locustio/locust/pull/3174) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI: Improve Locust Cloud Tab [\#3172](https://github.com/locustio/locust/pull/3172) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.37.13](https://github.com/locustio/locust/tree/2.37.13) (2025-07-15) [Full Changelog](https://github.com/locustio/locust/compare/2.37.12...2.37.13) **Fixed bugs:** - Locust can't install on alpine linux anymore [\#3166](https://github.com/locustio/locust/issues/3166) **Closed issues:** - Decouple the JavaScript asset code from the report [\#3064](https://github.com/locustio/locust/issues/3064) **Merged pull requests:** - Remove safe\_name from /stats/requests response [\#3171](https://github.com/locustio/locust/pull/3171) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI: Shrink Bundle Size [\#3169](https://github.com/locustio/locust/pull/3169) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.37.12](https://github.com/locustio/locust/tree/2.37.12) (2025-07-08) [Full Changelog](https://github.com/locustio/locust/compare/2.37.11...2.37.12) **Fixed bugs:** - Error shutting down when using processes [\#3161](https://github.com/locustio/locust/issues/3161) - 1279 Locust instances makes master run at 100% continously [\#3142](https://github.com/locustio/locust/issues/3142) **Merged pull requests:** - Attempt to increase open file limit \(RLIMIT\_NOFILE\) even on master. [\#3162](https://github.com/locustio/locust/pull/3162) ([cyberw](https://github.com/cyberw)) - Bump brace-expansion from 1.1.11 to 1.1.12 in /locust/webui [\#3160](https://github.com/locustio/locust/pull/3160) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [2.37.11](https://github.com/locustio/locust/tree/2.37.11) (2025-06-23) [Full Changelog](https://github.com/locustio/locust/compare/2.37.10...2.37.11) **Fixed bugs:** - FastHttpUser uses incorrect regex to hide home directory [\#3159](https://github.com/locustio/locust/issues/3159) **Closed issues:** - Define a list of paths to simulate an user journey [\#3150](https://github.com/locustio/locust/issues/3150) - export the results as a json file [\#3089](https://github.com/locustio/locust/issues/3089) **Merged pull requests:** - Forward locustfiles to locust cloud [\#3157](https://github.com/locustio/locust/pull/3157) ([amadeuppereira](https://github.com/amadeuppereira)) - Web UI: Always Warn of Invalid Host [\#3155](https://github.com/locustio/locust/pull/3155) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.37.10](https://github.com/locustio/locust/tree/2.37.10) (2025-06-07) [Full Changelog](https://github.com/locustio/locust/compare/2.37.9...2.37.10) **Merged pull requests:** - Revert accidental removal of --json-file option [\#3154](https://github.com/locustio/locust/pull/3154) ([brtkwr](https://github.com/brtkwr)) ## [2.37.9](https://github.com/locustio/locust/tree/2.37.9) (2025-06-05) [Full Changelog](https://github.com/locustio/locust/compare/2.37.8...2.37.9) **Merged pull requests:** - Web UI: Fix host field name missing if host is not required [\#3152](https://github.com/locustio/locust/pull/3152) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.37.8](https://github.com/locustio/locust/tree/2.37.8) (2025-06-05) [Full Changelog](https://github.com/locustio/locust/compare/2.37.7...2.37.8) **Closed issues:** - Support gevent \>= 25.4.1 [\#3143](https://github.com/locustio/locust/issues/3143) **Merged pull requests:** - Bump locust-cloud dependency, allow 25.x versions of gevent [\#3151](https://github.com/locustio/locust/pull/3151) ([cyberw](https://github.com/cyberw)) ## [2.37.7](https://github.com/locustio/locust/tree/2.37.7) (2025-06-03) [Full Changelog](https://github.com/locustio/locust/compare/2.37.6...2.37.7) **Merged pull requests:** - Web Ui: Add host field validation [\#3149](https://github.com/locustio/locust/pull/3149) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.37.6](https://github.com/locustio/locust/tree/2.37.6) (2025-05-28) [Full Changelog](https://github.com/locustio/locust/compare/2.37.5...2.37.6) **Fixed bugs:** - Documentation is Now Missing Table of "All available configuration options" [\#3144](https://github.com/locustio/locust/issues/3144) **Merged pull requests:** - Fix Docs Config Options [\#3145](https://github.com/locustio/locust/pull/3145) ([amadeuppereira](https://github.com/amadeuppereira)) ## [2.37.5](https://github.com/locustio/locust/tree/2.37.5) (2025-05-22) [Full Changelog](https://github.com/locustio/locust/compare/2.37.4...2.37.5) **Fixed bugs:** - Web UI Does Not Switch to Details Page Immediately on Test Start in Current Version [\#3128](https://github.com/locustio/locust/issues/3128) **Merged pull requests:** - Do not require locustfile on specific locust-cloud arguments [\#3141](https://github.com/locustio/locust/pull/3141) ([amadeuppereira](https://github.com/amadeuppereira)) ## [2.37.4](https://github.com/locustio/locust/tree/2.37.4) (2025-05-19) [Full Changelog](https://github.com/locustio/locust/compare/2.37.3...2.37.4) ## [2.37.3](https://github.com/locustio/locust/tree/2.37.3) (2025-05-14) [Full Changelog](https://github.com/locustio/locust/compare/2.37.2...2.37.3) **Merged pull requests:** - Webui: Warn on Missing Host [\#3140](https://github.com/locustio/locust/pull/3140) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.37.2](https://github.com/locustio/locust/tree/2.37.2) (2025-05-13) [Full Changelog](https://github.com/locustio/locust/compare/2.37.1...2.37.2) **Fixed bugs:** - p95 response time increases with the number of unique URLs [\#3134](https://github.com/locustio/locust/issues/3134) - FastResponse.failure\(\) takes 1 positional argument but 2 were given [\#3084](https://github.com/locustio/locust/issues/3084) **Merged pull requests:** - Webui: Block Submitting SwarmForm in Distributed Mode with no Workers [\#3138](https://github.com/locustio/locust/pull/3138) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fixes \#3134 - Improve the performance of the `/stats/requests` endpoint [\#3136](https://github.com/locustio/locust/pull/3136) ([orf](https://github.com/orf)) - Bump vite from 6.3.2 to 6.3.4 in /locust/webui [\#3132](https://github.com/locustio/locust/pull/3132) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [2.37.1](https://github.com/locustio/locust/tree/2.37.1) (2025-05-07) [Full Changelog](https://github.com/locustio/locust/compare/2.37.0...2.37.1) **Fixed bugs:** - --json-file always creates empty file [\#3130](https://github.com/locustio/locust/issues/3130) **Closed issues:** - Forced Dependency Updates \(e.g., python-socketio\) May Cause Version Mismatch with Java Services [\#3129](https://github.com/locustio/locust/issues/3129) **Merged pull requests:** - Fix --json-file \(actually save data to file\) [\#3131](https://github.com/locustio/locust/pull/3131) ([zed](https://github.com/zed)) ## [2.37.0](https://github.com/locustio/locust/tree/2.37.0) (2025-05-05) [Full Changelog](https://github.com/locustio/locust/compare/2.36.2...2.37.0) **Merged pull requests:** - Webui: Fix Failing Tests [\#3126](https://github.com/locustio/locust/pull/3126) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Update uv to 0.7.2 [\#3125](https://github.com/locustio/locust/pull/3125) ([cyberw](https://github.com/cyberw)) - Add command line option to export json results into a file [\#3124](https://github.com/locustio/locust/pull/3124) ([ajt89](https://github.com/ajt89)) - Add locust exporter import \(used in Locust Cloud\) [\#3122](https://github.com/locustio/locust/pull/3122) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - fix error message to be idiomatically correct English [\#3121](https://github.com/locustio/locust/pull/3121) ([davidxia](https://github.com/davidxia)) - Web UI: Use mutations for state buttons [\#3120](https://github.com/locustio/locust/pull/3120) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.36.2](https://github.com/locustio/locust/tree/2.36.2) (2025-04-25) [Full Changelog](https://github.com/locustio/locust/compare/2.36.1...2.36.2) **Merged pull requests:** - Remove circular dependency for locust-cloud [\#3119](https://github.com/locustio/locust/pull/3119) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.36.1](https://github.com/locustio/locust/tree/2.36.1) (2025-04-24) [Full Changelog](https://github.com/locustio/locust/compare/2.36.0...2.36.1) **Merged pull requests:** - Fix setting version for tag and pre-release [\#3118](https://github.com/locustio/locust/pull/3118) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.36.0](https://github.com/locustio/locust/tree/2.36.0) (2025-04-24) [Full Changelog](https://github.com/locustio/locust/compare/2.35.0...2.36.0) **Merged pull requests:** - Bump locust-cloud dependency to 1.20.0 and remove it from docs dependencies [\#3117](https://github.com/locustio/locust/pull/3117) ([cyberw](https://github.com/cyberw)) - Fix yarn publish [\#3116](https://github.com/locustio/locust/pull/3116) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web Ui: Ensure form element has name [\#3115](https://github.com/locustio/locust/pull/3115) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI: Add profile field [\#3113](https://github.com/locustio/locust/pull/3113) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Restrict gevent versions to ensure compatibility [\#3112](https://github.com/locustio/locust/pull/3112) ([amadeuppereira](https://github.com/amadeuppereira)) - Bump vite [\#3111](https://github.com/locustio/locust/pull/3111) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI: Optionally Extend Advanced Options [\#3110](https://github.com/locustio/locust/pull/3110) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Yarn Berry: Update publish command [\#3108](https://github.com/locustio/locust/pull/3108) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI: Fix npm publish failing [\#3107](https://github.com/locustio/locust/pull/3107) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - chore: set yarn to yarn berry [\#3104](https://github.com/locustio/locust/pull/3104) ([schwannden](https://github.com/schwannden)) - Refactoring: Extract locustfile content merger from main function [\#3102](https://github.com/locustio/locust/pull/3102) ([insspb](https://github.com/insspb)) - Refactoring: Extract validate stats configuration from main function [\#3101](https://github.com/locustio/locust/pull/3101) ([insspb](https://github.com/insspb)) - Add locust-cloud as a dependency, update sphinx and some other docs stuff [\#3097](https://github.com/locustio/locust/pull/3097) ([amadeuppereira](https://github.com/amadeuppereira)) ## [2.35.0](https://github.com/locustio/locust/tree/2.35.0) (2025-04-16) [Full Changelog](https://github.com/locustio/locust/compare/2.34.1...2.35.0) **Merged pull requests:** - Bump vite from 6.2.5 to 6.2.6 in /locust/webui [\#3098](https://github.com/locustio/locust/pull/3098) ([dependabot[bot]](https://github.com/apps/dependabot)) - Webui: Add credentials to stop and reset requests [\#3096](https://github.com/locustio/locust/pull/3096) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui: Add history fallback [\#3095](https://github.com/locustio/locust/pull/3095) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI: Add optional base url for locust requests to an external API [\#3094](https://github.com/locustio/locust/pull/3094) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui: adding profile argument and display in html report [\#3093](https://github.com/locustio/locust/pull/3093) ([schwannden](https://github.com/schwannden)) ## [2.34.1](https://github.com/locustio/locust/tree/2.34.1) (2025-04-09) [Full Changelog](https://github.com/locustio/locust/compare/2.34.0...2.34.1) **Merged pull requests:** - Bump vite from 6.2.4 to 6.2.5 in /locust/webui [\#3091](https://github.com/locustio/locust/pull/3091) ([dependabot[bot]](https://github.com/apps/dependabot)) - Drop support for Python 3.9 [\#3090](https://github.com/locustio/locust/pull/3090) ([cyberw](https://github.com/cyberw)) ## [2.34.0](https://github.com/locustio/locust/tree/2.34.0) (2025-04-06) [Full Changelog](https://github.com/locustio/locust/compare/2.33.2...2.34.0) **Merged pull requests:** - Fix missing optional argument definitions in PostKwargs [\#3088](https://github.com/locustio/locust/pull/3088) ([kairi003](https://github.com/kairi003)) - Bump vite from 6.2.1 to 6.2.4 in /locust/webui [\#3087](https://github.com/locustio/locust/pull/3087) ([dependabot[bot]](https://github.com/apps/dependabot)) - Web UI: Offset Graph Legend so There's no Overlap on Mobile / Narrow Screens [\#3086](https://github.com/locustio/locust/pull/3086) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - FastHttpUser: Dont crash if parameters are passed to failure\(\) when someone forgot catch\_response=True [\#3085](https://github.com/locustio/locust/pull/3085) ([cyberw](https://github.com/cyberw)) - Make the Locust UI Responsive [\#3083](https://github.com/locustio/locust/pull/3083) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add OpenAI User and example [\#3081](https://github.com/locustio/locust/pull/3081) ([cyberw](https://github.com/cyberw)) ## [2.33.2](https://github.com/locustio/locust/tree/2.33.2) (2025-03-14) [Full Changelog](https://github.com/locustio/locust/compare/2.33.1...2.33.2) **Fixed bugs:** - There was a heartbeat disconnect during the pressure test [\#3065](https://github.com/locustio/locust/issues/3065) **Closed issues:** - Error Logging in FastHttpUser [\#2937](https://github.com/locustio/locust/issues/2937) **Merged pull requests:** - Bump @babel/runtime from 7.22.15 to 7.26.10 in /locust/webui [\#3080](https://github.com/locustio/locust/pull/3080) ([dependabot[bot]](https://github.com/apps/dependabot)) - Update ruff to 0.10.0 [\#3079](https://github.com/locustio/locust/pull/3079) ([cyberw](https://github.com/cyberw)) - Optimize unit tests [\#3078](https://github.com/locustio/locust/pull/3078) ([cyberw](https://github.com/cyberw)) - Webui: Bump Vite Version for Dependabot [\#3074](https://github.com/locustio/locust/pull/3074) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Update uv to 0.6.5 and optimize docker start time [\#3073](https://github.com/locustio/locust/pull/3073) ([cyberw](https://github.com/cyberw)) ## [2.33.1](https://github.com/locustio/locust/tree/2.33.1) (2025-03-08) [Full Changelog](https://github.com/locustio/locust/compare/2.33.0...2.33.1) **Fixed bugs:** - --iterations with locust==2.33.0 and locust-plugins==4.5.3 [\#3071](https://github.com/locustio/locust/issues/3071) - uv.lock ends up in root of site-packages [\#3053](https://github.com/locustio/locust/issues/3053) **Merged pull requests:** - Fix html file naming crash, simplify code [\#3072](https://github.com/locustio/locust/pull/3072) ([cyberw](https://github.com/cyberw)) ## [2.33.0](https://github.com/locustio/locust/tree/2.33.0) (2025-02-22) [Full Changelog](https://github.com/locustio/locust/compare/2.32.10...2.33.0) **Fixed bugs:** - UnboundLocalError: local variable 'user\_count' referenced before assignment [\#3051](https://github.com/locustio/locust/issues/3051) **Merged pull requests:** - docs: update python-requests documentation links [\#3059](https://github.com/locustio/locust/pull/3059) ([n0h0](https://github.com/n0h0)) - dos: correct venv activation path in docs [\#3058](https://github.com/locustio/locust/pull/3058) ([n0h0](https://github.com/n0h0)) - Use enter to automatically open web UI in default browser [\#3057](https://github.com/locustio/locust/pull/3057) ([cyberw](https://github.com/cyberw)) - Update vite to 6.0.11 [\#3056](https://github.com/locustio/locust/pull/3056) ([cyberw](https://github.com/cyberw)) - Remove uv lock file from build artifacts [\#3055](https://github.com/locustio/locust/pull/3055) ([mquinnfd](https://github.com/mquinnfd)) - Improve error message on missing user\_count or spawn\_rate in swarm payload [\#3052](https://github.com/locustio/locust/pull/3052) ([cyberw](https://github.com/cyberw)) - Enable HTML Report Filename Parsing [\#3049](https://github.com/locustio/locust/pull/3049) ([ktchani](https://github.com/ktchani)) - FastHttpUser: Accept brotli and zstd compression encoding [\#3048](https://github.com/locustio/locust/pull/3048) ([kamilbednarz](https://github.com/kamilbednarz)) - Bump vitest from 2.1.6 to 2.1.9 in /locust/webui [\#3044](https://github.com/locustio/locust/pull/3044) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [2.32.10](https://github.com/locustio/locust/tree/2.32.10) (2025-02-18) [Full Changelog](https://github.com/locustio/locust/compare/2.32.9...2.32.10) **Closed issues:** - Switch from Poetry to uv [\#3033](https://github.com/locustio/locust/issues/3033) **Merged pull requests:** - Add uv lock file to builds [\#3047](https://github.com/locustio/locust/pull/3047) ([mquinnfd](https://github.com/mquinnfd)) - Use uv/hatch instead of Poetry [\#3039](https://github.com/locustio/locust/pull/3039) ([mquinnfd](https://github.com/mquinnfd)) ## [2.32.9](https://github.com/locustio/locust/tree/2.32.9) (2025-02-10) [Full Changelog](https://github.com/locustio/locust/compare/2.32.8...2.32.9) **Fixed bugs:** - Cannot Update Custom options in the Web UI when Default Value is None [\#3011](https://github.com/locustio/locust/issues/3011) **Merged pull requests:** - Update docs for stats.py file [\#3038](https://github.com/locustio/locust/pull/3038) ([gabriel-check24](https://github.com/gabriel-check24)) - Add iter\_lines Method to FastHttpSession Class [\#3024](https://github.com/locustio/locust/pull/3024) ([MasterKey-Pro](https://github.com/MasterKey-Pro)) - Fix issue where empty WebUI property is not parsed correctly [\#3012](https://github.com/locustio/locust/pull/3012) ([timhovius](https://github.com/timhovius)) ## [2.32.8](https://github.com/locustio/locust/tree/2.32.8) (2025-01-30) [Full Changelog](https://github.com/locustio/locust/compare/2.32.7...2.32.8) ## [2.32.7](https://github.com/locustio/locust/tree/2.32.7) (2025-01-30) [Full Changelog](https://github.com/locustio/locust/compare/2.32.6...2.32.7) **Merged pull requests:** - Web UI: Allow Showing Only an Error Message on the Login Page [\#3037](https://github.com/locustio/locust/pull/3037) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Allow Empty Tables when Filtering [\#3036](https://github.com/locustio/locust/pull/3036) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Allow xAxis and Grid to be Configured in Echarts [\#3035](https://github.com/locustio/locust/pull/3035) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Code quality: Fix unused imports and switch on related ruff check [\#3034](https://github.com/locustio/locust/pull/3034) ([insspb](https://github.com/insspb)) - Add tab with locust cloud features [\#3032](https://github.com/locustio/locust/pull/3032) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - fix readme image ref links [\#3031](https://github.com/locustio/locust/pull/3031) ([changchaishi](https://github.com/changchaishi)) ## [2.32.6](https://github.com/locustio/locust/tree/2.32.6) (2025-01-13) [Full Changelog](https://github.com/locustio/locust/compare/2.32.5...2.32.6) **Merged pull requests:** - Update Dockerfile to use Python 3.12 as base [\#3029](https://github.com/locustio/locust/pull/3029) ([vejmoj1](https://github.com/vejmoj1)) - Update tests to check for hostname instead of fqdn [\#3027](https://github.com/locustio/locust/pull/3027) ([ajt89](https://github.com/ajt89)) - Move some argument parsing/validation from main.py to argument\_parser.py and remove deprecated parameter --hatch-rate [\#3026](https://github.com/locustio/locust/pull/3026) ([ftb-skry](https://github.com/ftb-skry)) - pin poetry-core version to \<2.0.0 in pyproject.toml [\#3025](https://github.com/locustio/locust/pull/3025) ([mgor](https://github.com/mgor)) - Optimize run time of some unit tests [\#3020](https://github.com/locustio/locust/pull/3020) ([cyberw](https://github.com/cyberw)) ## [2.32.5](https://github.com/locustio/locust/tree/2.32.5) (2024-12-22) [Full Changelog](https://github.com/locustio/locust/compare/2.32.4...2.32.5) **Merged pull requests:** - Make cpu usage check sleep BEFORE the first check, and make it slightly less frequent [\#3014](https://github.com/locustio/locust/pull/3014) ([cyberw](https://github.com/cyberw)) - FastHttpUser: Fix ssl loading performance issue by avoiding to load certs when they wont be used anyway [\#3013](https://github.com/locustio/locust/pull/3013) ([cyberw](https://github.com/cyberw)) - Treat exceptions in init event handler as fatal [\#3009](https://github.com/locustio/locust/pull/3009) ([cyberw](https://github.com/cyberw)) - Add create store export [\#3004](https://github.com/locustio/locust/pull/3004) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.32.4](https://github.com/locustio/locust/tree/2.32.4) (2024-12-01) [Full Changelog](https://github.com/locustio/locust/compare/2.32.3...2.32.4) **Fixed bugs:** - Number of requests lower than expected in web UI [\#3000](https://github.com/locustio/locust/issues/3000) - Reports download links do not contain web-base-path [\#2998](https://github.com/locustio/locust/issues/2998) - Setuptools CVE-2024-6345 [\#2995](https://github.com/locustio/locust/issues/2995) - When using exclude-tags to exclude more than two tags, this setting will not take effect [\#2994](https://github.com/locustio/locust/issues/2994) **Merged pull requests:** - Allow showing auth info on blank page [\#3002](https://github.com/locustio/locust/pull/3002) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Number of requests lower than expected in web UI [\#3001](https://github.com/locustio/locust/pull/3001) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui links should be relative [\#2999](https://github.com/locustio/locust/pull/2999) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Dependency and node version bump [\#2997](https://github.com/locustio/locust/pull/2997) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fix example in docs [\#2993](https://github.com/locustio/locust/pull/2993) ([daniloakamine](https://github.com/daniloakamine)) - Add Input Type to Login Form [\#2992](https://github.com/locustio/locust/pull/2992) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Update configuration.rst to show minimalistic config example [\#2990](https://github.com/locustio/locust/pull/2990) ([vkuehn](https://github.com/vkuehn)) - Fix README Images for PyPi [\#2989](https://github.com/locustio/locust/pull/2989) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.32.3](https://github.com/locustio/locust/tree/2.32.3) (2024-11-17) [Full Changelog](https://github.com/locustio/locust/compare/2.32.2...2.32.3) **Fixed bugs:** - Setuptools CVE-2022-40897 [\#2986](https://github.com/locustio/locust/issues/2986) - master crash with different version worker [\#2975](https://github.com/locustio/locust/issues/2975) **Merged pull requests:** - Ensure we never use old version of setuptools [\#2988](https://github.com/locustio/locust/pull/2988) ([cyberw](https://github.com/cyberw)) - README Themed Screenshots [\#2985](https://github.com/locustio/locust/pull/2985) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - When specifying locustfile fia url, output start of response text when it wasnt valid python [\#2983](https://github.com/locustio/locust/pull/2983) ([cyberw](https://github.com/cyberw)) - Use debug log level for first 5s of waiting for workers to be ready. [\#2982](https://github.com/locustio/locust/pull/2982) ([cyberw](https://github.com/cyberw)) - Add option for Extra Options to be Required [\#2981](https://github.com/locustio/locust/pull/2981) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Update ruff mypy [\#2978](https://github.com/locustio/locust/pull/2978) ([tdadela](https://github.com/tdadela)) - Fix crash with older worker version requesting locustfile from master [\#2976](https://github.com/locustio/locust/pull/2976) ([cyberw](https://github.com/cyberw)) - Use f-strings instead of old style string interpolation [\#2974](https://github.com/locustio/locust/pull/2974) ([tdadela](https://github.com/tdadela)) ## [2.32.2](https://github.com/locustio/locust/tree/2.32.2) (2024-11-08) [Full Changelog](https://github.com/locustio/locust/compare/2.32.1...2.32.2) **Fixed bugs:** - Requests not ramping up after switching to using pydantic in django project [\#2960](https://github.com/locustio/locust/issues/2960) - The locust chart shows that data is still being recorded after the timed run time expires [\#2910](https://github.com/locustio/locust/issues/2910) **Closed issues:** - Downloading report should provide a meaningful human name [\#2931](https://github.com/locustio/locust/issues/2931) - Hard coded path make it impossible to host the UI on a path \(instead of the domain root\) [\#2909](https://github.com/locustio/locust/issues/2909) **Merged pull requests:** - Fix Incorrectly Updating Stat History [\#2972](https://github.com/locustio/locust/pull/2972) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Add Markdown Support for Auth Page [\#2969](https://github.com/locustio/locust/pull/2969) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fix Web Base Path Env Variable [\#2967](https://github.com/locustio/locust/pull/2967) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Locust Configurable Web Base Path [\#2966](https://github.com/locustio/locust/pull/2966) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fix Auth Args Type [\#2965](https://github.com/locustio/locust/pull/2965) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Add Auth Info to Auth Page [\#2963](https://github.com/locustio/locust/pull/2963) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fix doc link [\#2961](https://github.com/locustio/locust/pull/2961) ([tjandy98](https://github.com/tjandy98)) - Report name [\#2947](https://github.com/locustio/locust/pull/2947) ([obriat](https://github.com/obriat)) ## [2.32.1](https://github.com/locustio/locust/tree/2.32.1) (2024-10-29) [Full Changelog](https://github.com/locustio/locust/compare/2.32.0...2.32.1) **Closed issues:** - Add option to enable different statistics in the chart menu [\#2946](https://github.com/locustio/locust/issues/2946) **Merged pull requests:** - Webui Echarts Redraw Request Lines if Changed [\#2953](https://github.com/locustio/locust/pull/2953) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Add Custom Form to Auth Page [\#2952](https://github.com/locustio/locust/pull/2952) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Override Markdown HTML Link with MUI Link [\#2951](https://github.com/locustio/locust/pull/2951) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Fix Echarts Axis Formatting [\#2950](https://github.com/locustio/locust/pull/2950) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Echarts Time Axis Should be Localized [\#2949](https://github.com/locustio/locust/pull/2949) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add Google Analytics to docs.locust.io [\#2948](https://github.com/locustio/locust/pull/2948) ([heyman](https://github.com/heyman)) - LocustBadStatusCode without url param in fasthttp [\#2944](https://github.com/locustio/locust/pull/2944) ([swaalt](https://github.com/swaalt)) - Web UI Remove Default Value for Select if Value is Provided [\#2943](https://github.com/locustio/locust/pull/2943) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI Auth Add Password Visibility Toggle [\#2941](https://github.com/locustio/locust/pull/2941) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.32.0](https://github.com/locustio/locust/tree/2.32.0) (2024-10-15) [Full Changelog](https://github.com/locustio/locust/compare/2.31.8...2.32.0) **Fixed bugs:** - logfile is erroniously written when there are many workers. [\#2927](https://github.com/locustio/locust/issues/2927) - Form field for users, spawn rate, and run time still visible in UI although CustomShape defined without use\_common\_options. [\#2924](https://github.com/locustio/locust/issues/2924) - --html with --process 4 then get ValueError: StatsEntry.use\_response\_times\_cache must be set to True [\#2908](https://github.com/locustio/locust/issues/2908) - IPV6 check doesn't work as expected on AWS EKS [\#2787](https://github.com/locustio/locust/issues/2787) **Merged pull requests:** - Log deprecation warning for Python 3.9 [\#2940](https://github.com/locustio/locust/pull/2940) ([cyberw](https://github.com/cyberw)) - Run tests on python 3.13 too [\#2939](https://github.com/locustio/locust/pull/2939) ([cyberw](https://github.com/cyberw)) - Web UI - Fix Line Chart [\#2935](https://github.com/locustio/locust/pull/2935) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Modern UI - Fix Hide Common Options [\#2934](https://github.com/locustio/locust/pull/2934) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Allow alerts and errors on new and edit form [\#2932](https://github.com/locustio/locust/pull/2932) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add error message to swarm form [\#2930](https://github.com/locustio/locust/pull/2930) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Disable --csv for workers when using --processes. [\#2929](https://github.com/locustio/locust/pull/2929) ([cyberw](https://github.com/cyberw)) - Decide if ipv6 can work [\#2923](https://github.com/locustio/locust/pull/2923) ([nc-marco](https://github.com/nc-marco)) - Webui Add Form Alert [\#2922](https://github.com/locustio/locust/pull/2922) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add faq item: Basic auth \(Authorization header\) does not work after redirection [\#2921](https://github.com/locustio/locust/pull/2921) ([obriat](https://github.com/obriat)) - Add CSRF example [\#2920](https://github.com/locustio/locust/pull/2920) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI Add Exports for Library [\#2919](https://github.com/locustio/locust/pull/2919) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - lower log level for unnecessary --autostart argument [\#2918](https://github.com/locustio/locust/pull/2918) ([cyberw](https://github.com/cyberw)) ## [2.31.8](https://github.com/locustio/locust/tree/2.31.8) (2024-09-28) [Full Changelog](https://github.com/locustio/locust/compare/2.31.7...2.31.8) **Merged pull requests:** - Log locust-cloud version if it is installed [\#2916](https://github.com/locustio/locust/pull/2916) ([cyberw](https://github.com/cyberw)) - Web UI Auth submit should submit a POST request [\#2915](https://github.com/locustio/locust/pull/2915) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Url in template arg [\#2914](https://github.com/locustio/locust/pull/2914) ([fletelli42](https://github.com/fletelli42)) - Fix RTD versioning with a deep git clone [\#2913](https://github.com/locustio/locust/pull/2913) ([mquinnfd](https://github.com/mquinnfd)) ## [2.31.7](https://github.com/locustio/locust/tree/2.31.7) (2024-09-25) [Full Changelog](https://github.com/locustio/locust/compare/2.31.4.dev9994...2.31.7) **Merged pull requests:** - Fix Dependabot Complaints [\#2912](https://github.com/locustio/locust/pull/2912) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Improve Web UI Logging [\#2911](https://github.com/locustio/locust/pull/2911) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Pin python versions to avoid gh caching issue + always Install Dependencies, even when it looks like there was a cache hit [\#2907](https://github.com/locustio/locust/pull/2907) ([cyberw](https://github.com/cyberw)) - Fix Login Manager Error Message [\#2905](https://github.com/locustio/locust/pull/2905) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Log locust version earlier [\#2904](https://github.com/locustio/locust/pull/2904) ([cyberw](https://github.com/cyberw)) - Add Mongodb test example [\#2903](https://github.com/locustio/locust/pull/2903) ([guel-codes](https://github.com/guel-codes)) ## [2.31.4.dev9994](https://github.com/locustio/locust/tree/2.31.4.dev9994) (2024-09-16) [Full Changelog](https://github.com/locustio/locust/compare/2.31.4.dev9993...2.31.4.dev9994) ## [2.31.4.dev9993](https://github.com/locustio/locust/tree/2.31.4.dev9993) (2024-09-16) [Full Changelog](https://github.com/locustio/locust/compare/2.31.4.dev9992...2.31.4.dev9993) ## [2.31.4.dev9992](https://github.com/locustio/locust/tree/2.31.4.dev9992) (2024-09-16) [Full Changelog](https://github.com/locustio/locust/compare/2.31.4.dev9991...2.31.4.dev9992) ## [2.31.4.dev9991](https://github.com/locustio/locust/tree/2.31.4.dev9991) (2024-09-16) [Full Changelog](https://github.com/locustio/locust/compare/2.31.4.dev999...2.31.4.dev9991) ## [2.31.4.dev999](https://github.com/locustio/locust/tree/2.31.4.dev999) (2024-09-16) [Full Changelog](https://github.com/locustio/locust/compare/2.31.6...2.31.4.dev999) ## [2.31.6](https://github.com/locustio/locust/tree/2.31.6) (2024-09-15) [Full Changelog](https://github.com/locustio/locust/compare/2.31.5...2.31.6) **Fixed bugs:** - RPS vs Total Running Users [\#2895](https://github.com/locustio/locust/issues/2895) - Overwriting weight by config-users may lead to crash [\#2852](https://github.com/locustio/locust/issues/2852) - FastHttpSession requests typing for the json argument should support lists [\#2842](https://github.com/locustio/locust/issues/2842) - Dockerfile warning [\#2811](https://github.com/locustio/locust/issues/2811) **Closed issues:** - Cleaning up the build process [\#2857](https://github.com/locustio/locust/issues/2857) - Simplify GitHub Actions using install-poetry [\#2822](https://github.com/locustio/locust/issues/2822) **Merged pull requests:** - Add Error Message for Accessing Login Manager without --web-login [\#2902](https://github.com/locustio/locust/pull/2902) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Update Webui README [\#2901](https://github.com/locustio/locust/pull/2901) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add worker\_count = 1 to LocalRunner for parity with MasterRunner [\#2900](https://github.com/locustio/locust/pull/2900) ([tarkatronic](https://github.com/tarkatronic)) - Remove redundant None in Any | None annotations [\#2892](https://github.com/locustio/locust/pull/2892) ([tdadela](https://github.com/tdadela)) - Fix \_kl\_generator by filtering nonpositive User weights [\#2891](https://github.com/locustio/locust/pull/2891) ([tdadela](https://github.com/tdadela)) - Update README.md [\#2889](https://github.com/locustio/locust/pull/2889) ([JonanOribe](https://github.com/JonanOribe)) - Filename from URL Should Strip Query Params [\#2887](https://github.com/locustio/locust/pull/2887) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Simplify Generator annotations - PEP 696 [\#2886](https://github.com/locustio/locust/pull/2886) ([tdadela](https://github.com/tdadela)) - Fix FastHttpSession.request json typing [\#2885](https://github.com/locustio/locust/pull/2885) ([tdadela](https://github.com/tdadela)) ## [2.31.5](https://github.com/locustio/locust/tree/2.31.5) (2024-08-30) [Full Changelog](https://github.com/locustio/locust/compare/2.31.4...2.31.5) **Fixed bugs:** - Pressure testing is over, but RPS and Users still have data [\#2870](https://github.com/locustio/locust/issues/2870) **Merged pull requests:** - Ensure we don't accidentally hide errors while importing in locust-cloud or locust-plugins [\#2881](https://github.com/locustio/locust/pull/2881) ([cyberw](https://github.com/cyberw)) - Add publishing dependency on build package step [\#2880](https://github.com/locustio/locust/pull/2880) ([mquinnfd](https://github.com/mquinnfd)) - Build pipeline tweaks - docker tagging [\#2879](https://github.com/locustio/locust/pull/2879) ([mquinnfd](https://github.com/mquinnfd)) - Webui Remove chart initial data fetch [\#2878](https://github.com/locustio/locust/pull/2878) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Document use with uvx and remove openssl version from --version output [\#2877](https://github.com/locustio/locust/pull/2877) ([cyberw](https://github.com/cyberw)) - Web UI Remove Scroll to Zoom [\#2876](https://github.com/locustio/locust/pull/2876) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Cleaning and improvements in the Build Pipeline [\#2873](https://github.com/locustio/locust/pull/2873) ([mquinnfd](https://github.com/mquinnfd)) - WebUI: Correct types for form select [\#2872](https://github.com/locustio/locust/pull/2872) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.31.4](https://github.com/locustio/locust/tree/2.31.4) (2024-08-26) [Full Changelog](https://github.com/locustio/locust/compare/2.31.3...2.31.4) **Merged pull requests:** - Webui Allow changing select input size [\#2871](https://github.com/locustio/locust/pull/2871) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Replace Logo SVG [\#2867](https://github.com/locustio/locust/pull/2867) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add favicon that looks good in light color theme [\#2866](https://github.com/locustio/locust/pull/2866) ([heyman](https://github.com/heyman)) - Webui Add build lib command to package.json [\#2865](https://github.com/locustio/locust/pull/2865) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI Github Action Publish steps must Build lib [\#2864](https://github.com/locustio/locust/pull/2864) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fix npm publish issue [\#2863](https://github.com/locustio/locust/pull/2863) ([cyberw](https://github.com/cyberw)) - GH actions: Update names of publish steps. Don't run prerelease steps when no prerelease is actually going to be published [\#2862](https://github.com/locustio/locust/pull/2862) ([cyberw](https://github.com/cyberw)) - Webui Fix Version Tag in NPM Prerelease Step [\#2861](https://github.com/locustio/locust/pull/2861) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Fix NPM Publish Step [\#2860](https://github.com/locustio/locust/pull/2860) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Web UI as a Library NPM Release [\#2858](https://github.com/locustio/locust/pull/2858) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add PostgresUser to examples [\#2836](https://github.com/locustio/locust/pull/2836) ([guel-codes](https://github.com/guel-codes)) ## [2.31.3](https://github.com/locustio/locust/tree/2.31.3) (2024-08-15) [Full Changelog](https://github.com/locustio/locust/compare/2.31.2...2.31.3) **Fixed bugs:** - Brew installed locust missing UI assets [\#2831](https://github.com/locustio/locust/issues/2831) - response avg time is NaN [\#2829](https://github.com/locustio/locust/issues/2829) - Windows Action Runs Wrong Version of Locust [\#2796](https://github.com/locustio/locust/issues/2796) **Merged pull requests:** - Web UI Remove Echarts startValue [\#2855](https://github.com/locustio/locust/pull/2855) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Update GitHub action versions [\#2853](https://github.com/locustio/locust/pull/2853) ([cyberw](https://github.com/cyberw)) - Improve docs for --class-picker/--config-users and give better error messages if json is bad [\#2851](https://github.com/locustio/locust/pull/2851) ([cyberw](https://github.com/cyberw)) - Add missing margin between Logo and Host in Navbar [\#2850](https://github.com/locustio/locust/pull/2850) ([heyman](https://github.com/heyman)) - Web UI Should use Built-In Echarts Time Axis [\#2847](https://github.com/locustio/locust/pull/2847) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Notification Improvements [\#2846](https://github.com/locustio/locust/pull/2846) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Use new logo in web UI + some minor improvements [\#2844](https://github.com/locustio/locust/pull/2844) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Webui Add Scatterplot Support [\#2840](https://github.com/locustio/locust/pull/2840) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.31.2](https://github.com/locustio/locust/tree/2.31.2) (2024-08-06) [Full Changelog](https://github.com/locustio/locust/compare/2.31.1...2.31.2) **Merged pull requests:** - Prebuild UI in PyPi publish steps so that even source distributions contain web UI code [\#2839](https://github.com/locustio/locust/pull/2839) ([mquinnfd](https://github.com/mquinnfd)) - Add Tests for Web UI Line Chart [\#2838](https://github.com/locustio/locust/pull/2838) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Documentation: Configure html\_baseurl and jinja on RTD [\#2837](https://github.com/locustio/locust/pull/2837) ([plaindocs](https://github.com/plaindocs)) ## [2.31.1](https://github.com/locustio/locust/tree/2.31.1) (2024-08-05) [Full Changelog](https://github.com/locustio/locust/compare/2.31.0...2.31.1) **Merged pull requests:** - Fix issue with downloading HTML report, update package.json for webui build [\#2834](https://github.com/locustio/locust/pull/2834) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.31.0](https://github.com/locustio/locust/tree/2.31.0) (2024-08-05) [Full Changelog](https://github.com/locustio/locust/compare/2.30.0...2.31.0) **Merged pull requests:** - Fix docker build for release [\#2830](https://github.com/locustio/locust/pull/2830) ([cyberw](https://github.com/cyberw)) - Github Actions: Use node 20.x \(fix PyPI Release and pre-Release Steps\) [\#2828](https://github.com/locustio/locust/pull/2828) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Improve Echarts and Expose Line and Axis Configuration [\#2826](https://github.com/locustio/locust/pull/2826) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Bump Node [\#2825](https://github.com/locustio/locust/pull/2825) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Integrations for Locust Cloud [\#2824](https://github.com/locustio/locust/pull/2824) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Fix HTML Report Stats Table [\#2817](https://github.com/locustio/locust/pull/2817) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Locust UI Charts Should Change Color Based on Theme [\#2815](https://github.com/locustio/locust/pull/2815) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Locust UI as a Module [\#2804](https://github.com/locustio/locust/pull/2804) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Log a message if total fixed\_count is higher than number of users to spawn [\#2793](https://github.com/locustio/locust/pull/2793) ([cyberw](https://github.com/cyberw)) - Simplify fixed\_count Users generation in UsersDispatcher.\_user\_gen [\#2783](https://github.com/locustio/locust/pull/2783) ([tdadela](https://github.com/tdadela)) - URL Directory, and Multi-File Support for Locustfile Distribution [\#2766](https://github.com/locustio/locust/pull/2766) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.30.0](https://github.com/locustio/locust/tree/2.30.0) (2024-07-31) [Full Changelog](https://github.com/locustio/locust/compare/2.29.1...2.30.0) **Fixed bugs:** - Locust / pypy fails with "AttributeError: module 'gc' has no attribute 'freeze'" error. [\#2818](https://github.com/locustio/locust/issues/2818) - Worker sometimes fails to send heartbeat after upgrade to urllib3\>=1.26.16 [\#2812](https://github.com/locustio/locust/issues/2812) - Web UI lacking asset [\#2781](https://github.com/locustio/locust/issues/2781) **Closed issues:** - Charts Update Is Delayed [\#2771](https://github.com/locustio/locust/issues/2771) - Use `itertools.cycle` in `SequentialTaskSet` [\#2740](https://github.com/locustio/locust/issues/2740) - `SequentialTaskSet` handles task weights in an inconsistent way [\#2739](https://github.com/locustio/locust/issues/2739) **Merged pull requests:** - Update poetry windows tests [\#2821](https://github.com/locustio/locust/pull/2821) ([mquinnfd](https://github.com/mquinnfd)) - Fix pypy gc.freeze\(\) AttributeError [\#2819](https://github.com/locustio/locust/pull/2819) ([jimoleary](https://github.com/jimoleary)) - Fix Dockerfile style warning [\#2814](https://github.com/locustio/locust/pull/2814) ([mehrdadbn9](https://github.com/mehrdadbn9)) - Avoid deadlock in gevent/urllib3 connection pool \(fixes occasional worker heartbeat timeouts\) [\#2813](https://github.com/locustio/locust/pull/2813) ([tdadela](https://github.com/tdadela)) - Replace total avg response time with 50 percentile \(avg was broken\) [\#2806](https://github.com/locustio/locust/pull/2806) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add example of a bottlenecked server and use that test to make a new graph for the docs [\#2805](https://github.com/locustio/locust/pull/2805) ([cyberw](https://github.com/cyberw)) - Fix tests on windows [\#2803](https://github.com/locustio/locust/pull/2803) ([mquinnfd](https://github.com/mquinnfd)) - Provide warning for local installs where yarn is not present [\#2801](https://github.com/locustio/locust/pull/2801) ([mquinnfd](https://github.com/mquinnfd)) - Fix Extend Webui Example [\#2800](https://github.com/locustio/locust/pull/2800) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Correctly set version from Poetry in published builds [\#2791](https://github.com/locustio/locust/pull/2791) ([mquinnfd](https://github.com/mquinnfd)) - Fix StatsEntry docstring [\#2784](https://github.com/locustio/locust/pull/2784) ([tdadela](https://github.com/tdadela)) - dispatch benchmark test improvements [\#2780](https://github.com/locustio/locust/pull/2780) ([tdadela](https://github.com/tdadela)) - Typing: strict optional in dispatch.py [\#2779](https://github.com/locustio/locust/pull/2779) ([tdadela](https://github.com/tdadela)) - new events for heartbeat and usage monitor [\#2777](https://github.com/locustio/locust/pull/2777) ([mgor](https://github.com/mgor)) - FastHttpSession requests typing [\#2775](https://github.com/locustio/locust/pull/2775) ([tdadela](https://github.com/tdadela)) - Remove Line Chart Default Zoom [\#2774](https://github.com/locustio/locust/pull/2774) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - FastHttpSession: Enable passing json as a positional argument for post\(\) and stop converting response times to int [\#2772](https://github.com/locustio/locust/pull/2772) ([tdadela](https://github.com/tdadela)) - SequentialTaskSet: Allow weighted tasks and dict in .tasks [\#2742](https://github.com/locustio/locust/pull/2742) ([bakhtos](https://github.com/bakhtos)) - Implement Poetry build system \(mainly so we don't have to commit dynamically generated front end bundles to git\) [\#2725](https://github.com/locustio/locust/pull/2725) ([mquinnfd](https://github.com/mquinnfd)) ## [2.29.1](https://github.com/locustio/locust/tree/2.29.1) (2024-06-25) [Full Changelog](https://github.com/locustio/locust/compare/2.29.0...2.29.1) **Fixed bugs:** - locust/webui/dist/index.html script errors. [\#2753](https://github.com/locustio/locust/issues/2753) **Merged pull requests:** - Option to Skip Monkey Patching with LOCUST\_SKIP\_MONKEY\_PATCH [\#2765](https://github.com/locustio/locust/pull/2765) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - HttpSession: Improve error message when someone forgot to pass catch\_response=True + small optimization [\#2762](https://github.com/locustio/locust/pull/2762) ([cyberw](https://github.com/cyberw)) - Add JavaScript to MIME types for Windows Operating Systems [\#2759](https://github.com/locustio/locust/pull/2759) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add proxy support for FastHttpUser [\#2758](https://github.com/locustio/locust/pull/2758) ([NicoAdrian](https://github.com/NicoAdrian)) - Httpsession requests typing [\#2699](https://github.com/locustio/locust/pull/2699) ([tdadela](https://github.com/tdadela)) ## [2.29.0](https://github.com/locustio/locust/tree/2.29.0) (2024-06-07) [Full Changelog](https://github.com/locustio/locust/compare/2.28.0...2.29.0) **Fixed bugs:** - The time of the downloaded html report is not correct [\#2691](https://github.com/locustio/locust/issues/2691) - Event spawning\_complete fires every time a user is created [\#2671](https://github.com/locustio/locust/issues/2671) - Delay at startup and high cpu usage on Windows in Python 3.12 [\#2555](https://github.com/locustio/locust/issues/2555) **Closed issues:** - Log a warning if getting locustfile from master takes more than 60s [\#2748](https://github.com/locustio/locust/issues/2748) - Show the reset button even after stopping a test [\#2723](https://github.com/locustio/locust/issues/2723) - Add date to charts in web UI [\#2678](https://github.com/locustio/locust/issues/2678) **Merged pull requests:** - Send logs from workers to master and improve log viewer tab in the Web UI [\#2750](https://github.com/locustio/locust/pull/2750) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Add Logging to download\_locustfile\_from\_master [\#2749](https://github.com/locustio/locust/pull/2749) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Modify timestamp generation to remove deprecation warning [\#2738](https://github.com/locustio/locust/pull/2738) ([JavierUhagon](https://github.com/JavierUhagon)) - Docs: Fix API TOC [\#2737](https://github.com/locustio/locust/pull/2737) ([plaindocs](https://github.com/plaindocs)) - Docs: Fix sphinx and theme upgrade [\#2736](https://github.com/locustio/locust/pull/2736) ([plaindocs](https://github.com/plaindocs)) - Docs: Fix theme [\#2735](https://github.com/locustio/locust/pull/2735) ([plaindocs](https://github.com/plaindocs)) - Docs: Import wiki to docs [\#2734](https://github.com/locustio/locust/pull/2734) ([plaindocs](https://github.com/plaindocs)) - Mention installing Locust in Building the Docs [\#2733](https://github.com/locustio/locust/pull/2733) ([plaindocs](https://github.com/plaindocs)) - Docs: Upgrade Sphinx to latest version \(7.3.7\) [\#2732](https://github.com/locustio/locust/pull/2732) ([plaindocs](https://github.com/plaindocs)) - Add date and zoom to charts in web UI [\#2731](https://github.com/locustio/locust/pull/2731) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Use requests 2.32.2 or higher for Python 3.12 [\#2730](https://github.com/locustio/locust/pull/2730) ([cyberw](https://github.com/cyberw)) - The time of the downloaded HTML report is not correct [\#2729](https://github.com/locustio/locust/pull/2729) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Ensure spawning\_complete only happens once on workers [\#2728](https://github.com/locustio/locust/pull/2728) ([cyberw](https://github.com/cyberw)) - Improve confusing log messages if someone accidentally accesses the Web UI over HTTPS [\#2727](https://github.com/locustio/locust/pull/2727) ([cyberw](https://github.com/cyberw)) - Show Reset Button when Test is Stopped [\#2726](https://github.com/locustio/locust/pull/2726) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) ## [2.28.0](https://github.com/locustio/locust/tree/2.28.0) (2024-05-23) [Full Changelog](https://github.com/locustio/locust/compare/2.27.0...2.28.0) **Fixed bugs:** - The Charts presentation in the report downloaded by locust is problematic [\#2706](https://github.com/locustio/locust/issues/2706) - Locust insists on using IPv6 despite being in an IPv4 stack. [\#2689](https://github.com/locustio/locust/issues/2689) - When there is an error in the FAILURES of the front-end page when there is a worker, there is no escape. [\#2674](https://github.com/locustio/locust/issues/2674) **Closed issues:** - Pin the headers and aggregated row to the top and bottom of the window [\#2688](https://github.com/locustio/locust/issues/2688) - Remove legacy UI [\#2673](https://github.com/locustio/locust/issues/2673) - TaskSet's `_task_queue` should be a `collections.deque`? [\#2653](https://github.com/locustio/locust/issues/2653) **Merged pull requests:** - Pin the headers to the top of the window [\#2717](https://github.com/locustio/locust/pull/2717) ([JavierUhagon](https://github.com/JavierUhagon)) - Dont enable ipv6 for zmq if no ipv6 stack exists [\#2715](https://github.com/locustio/locust/pull/2715) ([cyberw](https://github.com/cyberw)) - Give better error message if User subclass doesnt call base constructor [\#2713](https://github.com/locustio/locust/pull/2713) ([cyberw](https://github.com/cyberw)) - Stop quoting error messages an extra time in distributed mode [\#2712](https://github.com/locustio/locust/pull/2712) ([cyberw](https://github.com/cyberw)) - Lower log levels for exceptions in flask [\#2711](https://github.com/locustio/locust/pull/2711) ([cyberw](https://github.com/cyberw)) - Stop HTML escaping errors for /stats/requests endpoint [\#2710](https://github.com/locustio/locust/pull/2710) ([cyberw](https://github.com/cyberw)) - Update Stats History on HTML Report [\#2709](https://github.com/locustio/locust/pull/2709) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - Logging: Only print hostname instead of FQDN [\#2705](https://github.com/locustio/locust/pull/2705) ([cyberw](https://github.com/cyberw)) - Remove legacy UI [\#2703](https://github.com/locustio/locust/pull/2703) ([andrewbaldwin44](https://github.com/andrewbaldwin44)) - WebUI: update users, spawn\_rate, host and run\_time in `parsed_options` \(for LoadShapes that might access it\) [\#2656](https://github.com/locustio/locust/pull/2656) ([raulparada](https://github.com/raulparada)) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ================================================ FILE: Dockerfile ================================================ # This is a local-use Docker image which illustrates the end-to-end build process for Locust # Stage 1: Build web front end FROM node:22.0.0-alpine AS webui-builder ADD locust/webui locust/webui ADD package.json . # long yarn timeout necessary in certain network environments RUN yarn webui:install --production --network-timeout 60000 RUN yarn webui:build # Stage 2: Build Locust package (make sure any changes here are also reflected in Dockerfile.ci) FROM python:3.13-slim AS base FROM base AS builder RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates git # there are no wheels for some packages (geventhttpclient?) for arm64/aarch64, so we need some build dependencies there RUN if [ -n "$(arch | grep 'arm64\|aarch64')" ]; then apt install -y --no-install-recommends gcc python3-dev; fi RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" ENV SKIP_PRE_BUILD="true" COPY . /build WORKDIR /build # clear locally built assets, dist remains part of the docker context for CI purposes RUN rm -rf dist # bring in the prebuilt front-end before package installation COPY --from=webui-builder locust/webui/dist locust/webui/dist # Build the Python project ENV UV_PROJECT_ENVIRONMENT="/opt/venv" ADD https://astral.sh/uv/0.7.2/install.sh /uv-installer.sh RUN sh /uv-installer.sh && rm /uv-installer.sh ENV PATH="/root/.local/bin/:$PATH" RUN uv build && \ pip install dist/*.whl # Stage 3: Runtime image FROM base COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" # turn off python output buffering ENV PYTHONUNBUFFERED=1 RUN useradd --create-home locust # ensure correct permissions RUN chown -R locust /opt/venv # perform initial bytecode compilation (brings down total startup time from ~0.9s to ~0.6s) RUN locust --version USER locust WORKDIR /home/locust EXPOSE 8089 5557 ENTRYPOINT ["locust"] ================================================ FILE: Dockerfile.ci ================================================ # This is the image pushed to Dockerhub, containing the built and tested Locust package # Stage 1: Install Locust package FROM python:3.13-slim AS base FROM base AS builder RUN apt-get update && apt-get install -y git # there are no wheels for some packages (geventhttpclient?) for arm64/aarch64, so we need some build dependencies there RUN if [ -n "$(arch | grep 'arm64\|aarch64')" ]; then apt install -y --no-install-recommends gcc python3-dev; fi RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" # Install the built Python dist COPY ./dist dist RUN pip install dist/*.whl # Stage 2: Runtime image FROM base COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" # turn off python output buffering ENV PYTHONUNBUFFERED=1 RUN useradd --create-home locust # ensure correct permissions RUN chown -R locust /opt/venv # perform initial bytecode compilation (brings down total startup time from ~0.9s to ~0.6s) RUN locust --version USER locust WORKDIR /home/locust EXPOSE 8089 5557 ENTRYPOINT ["locust"] ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2009-2025, Carl Byström, Jonatan Heyman, Lars Holmberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ test: pytest -vv locust/test .PHONY: build build: check-uv check-yarn uv build install: check-uv uv sync .SILENT: .PHONY: check-uv check-uv: command -v uv >/dev/null 2>&1 || { echo >&2 "Locust requires the uv binary to be available in this shell to build the Python package.\nSee: https://docs.locust.io/en/stable/developing-locust.html#install-locust-for-development"; exit 1; } .SILENT: .PHONY: check-yarn check-yarn: command -v yarn >/dev/null 2>&1 || { echo >&2 "Locust requires the yarn binary to be available in this shell to build the web front-end.\nSee: https://docs.locust.io/en/stable/developing-locust.html#making-changes-to-locust-s-web-ui"; exit 1; } frontend_build: yarn webui:install && yarn webui:build release: build twine upload dist/* setup_docs_dependencies: uv sync --all-groups build_docs: setup_docs_dependencies sphinx-build -b html docs/ docs/_build/ # This command can be used to serve the built documentation at http://localhost for # easier offline viewing .SILENT: .PHONY: serve_docs serve_docs: echo "Serving docs at http://localhost:80" python -m http.server 80 -d docs/_build changelog: @echo "Not supported any more. Run ./generate_changelog.py instead!" ================================================ FILE: README.md ================================================ # Locust [![PyPI](https://img.shields.io/pypi/v/locust.svg)](https://pypi.org/project/locust/) [![Supported Python Versions](https://img.shields.io/pypi/pyversions/locust?color=brightgreen)](https://pypi.org/project/locust/) [![Downloads](https://static.pepy.tech/personalized-badge/locust?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/locust) [![GitHub contributors](https://img.shields.io/github/contributors/locustio/locust.svg)](https://github.com/locustio/locust/graphs/contributors) [![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine) Locust is an open source performance/load testing tool for HTTP and other protocols. Its developer-friendly approach lets you define your tests in regular Python code. Locust tests can be run from command line or using its web-based UI. Throughput, response times and errors can be viewed in real time and/or exported for later analysis. You can import regular Python libraries into your tests, and with Locust's pluggable architecture it is infinitely expandable. Unlike when using most other tools, your test design will never be limited by a GUI or domain-specific language. To get started right away, head over to the [documentation](http://docs.locust.io/en/stable/installation.html). ## Features #### Write user test scenarios in plain old Python If you want your users to loop, perform some conditional behaviour or do some calculations, you just use the regular programming constructs provided by Python. Locust runs every user inside its own greenlet (a lightweight process/coroutine). This enables you to write your tests like normal (blocking) Python code instead of having to use callbacks or some other mechanism. Because your scenarios are “just python” you can use your regular IDE, and version control your tests as regular code (as opposed to some other tools that use XML or binary formats) ```python from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(1, 2) def on_start(self): self.client.post("/login", json={"username":"foo", "password":"bar"}) @task def hello_world(self): self.client.get("/hello") self.client.get("/world") @task(3) def view_item(self): for item_id in range(10): self.client.get(f"/item?id={item_id}", name="/item") ``` #### Distributed & Scalable - supports hundreds of thousands of users Locust makes it easy to run load tests distributed over multiple machines. It is event-based (using [gevent](http://www.gevent.org/)), which makes it possible for a single process to handle many thousands concurrent users. While there may be other tools that are capable of doing more requests per second on a given hardware, the low overhead of each Locust user makes it very suitable for testing highly concurrent workloads. #### Web-based UI Locust has a user friendly web interface that shows the progress of your test in real-time. You can even change the load while the test is running. It can also be run without the UI, making it easy to use for CI/CD testing. Locust UI charts Locust UI stats Locust UI workers Locust UI start test #### Can test any system Even though Locust primarily works with web sites/services, it can be used to test almost any system or protocol. Just [write a client](https://docs.locust.io/en/latest/testing-other-systems.html#testing-other-systems) for what you want to test, or [explore some created by the community](https://github.com/SvenskaSpel/locust-plugins#users). ## Hackable Locust's code base is intentionally kept small and doesn't solve everything out of the box. Instead, we try to make it easy to adapt to any situation you may come across, using regular Python code. There is nothing stopping you from: * [Send real time reporting data to TimescaleDB and visualize it in Grafana](https://github.com/SvenskaSpel/locust-plugins/blob/master/locust_plugins/dashboards/README.md) * [Wrap calls to handle the peculiarities of your REST API](https://github.com/SvenskaSpel/locust-plugins/blob/8af21862d8129a5c3b17559677fe92192e312d8f/examples/rest_ex.py#L87) * [Use a totally custom load shape/profile](https://docs.locust.io/en/latest/custom-load-shape.html#custom-load-shape) * [...](https://github.com/locustio/locust/wiki/Extensions) ## Links * Documentation: [docs.locust.io](https://docs.locust.io) * Support/Questions: [StackOverflow](https://stackoverflow.com/questions/tagged/locust) * Github Discussions: [Github Discussions](https://github.com/orgs/locustio/discussions) * Chat/discussion: [Slack](https://locustio.slack.com) [(signup)](https://communityinviter.com/apps/locustio/locust) ## Authors * Maintainer: [Lars Holmberg](https://github.com/cyberw) * UI: [Andrew Baldwin](https://github.com/andrewbaldwin44) * Original creator: [Jonatan Heyman](https://github.com/heyman) * Massive thanks to [all of our contributors](https://github.com/locustio/locust/graphs/contributors) ## License Open source licensed under the MIT license (see _LICENSE_ file for details). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Only latest version is actively supported, but issues reported for earlier minor will be considered if they are serious. ## Reporting a Vulnerability Reported using regular GitHub issues. If there is some reason an issue cannot be made public before a fix has been made, contact @cyberw directly. ## Use Locust safely Locust is not intended to be deployed on a public-facing server. By default the web UI is only exposed on localhost, so normally this is not a problem. Do not give someone access to the web UI unless you trust them with everything else that is on that machine. ## Use Locust nicely Do not load test public web sites/services that you do not own. ================================================ FILE: Vagrantfile ================================================ Vagrant.configure("2") do |config| config.vm.box = "ubuntu/xenial32" config.vm.network :forwarded_port, guest: 8089, host: 8089 config.vm.provision :shell, :path => "examples/vagrant/vagrant.sh" end ================================================ FILE: benchmarks/dispatch.py ================================================ """ This file contains a benchmark to validate the performance of Locust itself. More precisely, the performance of the `UsersDispatcher` class which is responsible for calculating the distribution of users on each worker. This benchmark is to be used by people working on Locust's development. """ from locust import User from locust.dispatch import UsersDispatcher from locust.runners import WorkerNode import argparse import gc import itertools import statistics import time from prettytable import PrettyTable NUMBER_OF_USER_CLASSES: int = 1000 USER_CLASSES: list[type[User]] = [] WEIGHTS = list(range(1, NUMBER_OF_USER_CLASSES + 1)) for i, x in enumerate(WEIGHTS): exec(f"class User{i}(User): weight = {x}") # Equivalent to: # # class User0(User): # weight = 5 # # class User1(User): # weight = 55 # . # . # . exec("USER_CLASSES = [" + ",".join(f"User{i}" for i in range(len(WEIGHTS))) + "]") # Equivalent to: # # USER_CLASSES = [ # User0, # User1, # . # . # . # ] if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-f", "--full-benchmark", action="store_true", help="run benchmark on full test matrix") parser.add_argument( "-i", "--include-fixed-users", action="store_true", help="add test cases when 50 percent of users use User.fixed_count instead of User.weight", ) parser.add_argument("-r", "--repeat", default=1, type=int, help="number of test cases with the same parameters") parser.add_argument("-s", "--save-output", action="store_true", help="save test results to files") args = parser.parse_args() now = time.time() worker_count_cases = [10, 100, 1000] user_count_cases = [10_000, 100_000, 1_000_000] number_of_user_classes_cases = [1, 30, 1000] spawn_rate_cases = [100, 10_000] fixed_count_cases = [False, True] if args.include_fixed_users else [False] # [0% fixed_count users, 50% fixed_count users] if args.mixed_user_types else [0% fixed_count users] repeat_cases = list(range(1, args.repeat + 1)) if not args.full_benchmark: worker_count_cases = [max(worker_count_cases)] user_count_cases = [max(user_count_cases)] number_of_user_classes_cases = [max(number_of_user_classes_cases)] spawn_rate_cases = [max(spawn_rate_cases)] case_count = ( len(worker_count_cases) * len(user_count_cases) * len(number_of_user_classes_cases) * len(spawn_rate_cases) * len(fixed_count_cases) * len(repeat_cases) ) results = {} try: for case_index, ( worker_count, user_count, number_of_user_classes, spawn_rate, fixed_users, iteration, ) in enumerate( itertools.product( worker_count_cases, user_count_cases, number_of_user_classes_cases, spawn_rate_cases, fixed_count_cases, repeat_cases, ) ): workers = [WorkerNode(str(i)) for i in range(worker_count)] if fixed_users: sum_fixed_weight = 0 for j in range(0, number_of_user_classes, 2): sum_fixed_weight += USER_CLASSES[j].weight for j in range(0, number_of_user_classes, 2): # set fixed_weights for 50% of users USER_CLASSES[j].fixed_count = max(1, USER_CLASSES[j].weight // sum_fixed_weight) # type: ignore # assigned .weight is int ts = time.process_time() users_dispatcher = UsersDispatcher( worker_nodes=workers, user_classes=USER_CLASSES[:number_of_user_classes], ) instantiate_duration = time.process_time() - ts # Ramp-up gc.disable() ts = time.process_time() users_dispatcher.new_dispatch(target_user_count=user_count, spawn_rate=spawn_rate) new_dispatch_ramp_up_duration = time.process_time() - ts gc.enable() assert len(users_dispatcher.dispatch_iteration_durations) == 0 users_dispatcher._wait_between_dispatch = 0 all_dispatched_users_ramp_up = list(users_dispatcher) dispatch_iteration_durations_ramp_up = users_dispatcher.dispatch_iteration_durations[:] # Ramp-down gc.disable() ts = time.process_time() users_dispatcher.new_dispatch(target_user_count=0, spawn_rate=spawn_rate) new_dispatch_ramp_down_duration = time.process_time() - ts gc.enable() assert len(users_dispatcher.dispatch_iteration_durations) == 0 users_dispatcher._wait_between_dispatch = 0 all_dispatched_users_ramp_down = list(users_dispatcher) dispatch_iteration_durations_ramp_down = users_dispatcher.dispatch_iteration_durations[:] if fixed_users: for j in range(0, number_of_user_classes, 2): USER_CLASSES[j].fixed_count = None cpu_ramp_up = "{:3.3f}/{:3.3f}/{:3.3f}".format( # noqa: UP032 1000 * statistics.mean(dispatch_iteration_durations_ramp_up), 1000 * min(dispatch_iteration_durations_ramp_up), 1000 * max(dispatch_iteration_durations_ramp_up), ) # noqa: UP032 cpu_ramp_down = "{:3.3f}/{:3.3f}/{:3.3f}".format( # noqa: UP032 1000 * statistics.mean(dispatch_iteration_durations_ramp_down), 1000 * min(dispatch_iteration_durations_ramp_down), 1000 * max(dispatch_iteration_durations_ramp_down), ) print( "{:04.0f}/{:04.0f} - {:,} workers - {:,} users - {} user classes - {:,} users/s - instantiate: {:.3f}ms - new_dispatch (ramp-up/ramp-down): {:.3f}ms/{:.3f}ms - cpu_ramp_up: {}ms - cpu_ramp_down: {}ms".format( # noqa: UP032 case_index + 1, case_count, worker_count, user_count, number_of_user_classes, spawn_rate, instantiate_duration * 1000, new_dispatch_ramp_up_duration * 1000, new_dispatch_ramp_down_duration * 1000, cpu_ramp_up, cpu_ramp_down, ) ) results[(worker_count, user_count, number_of_user_classes, spawn_rate, fixed_users, iteration)] = ( cpu_ramp_up, cpu_ramp_down, ) finally: table = PrettyTable() table.field_names = [ "Workers", "Users", "User Classes", "Spawn Rate", "Fixed Users", "Iteration", "Ramp-Up (avg/min/max) (ms)", "Ramp-Down (avg/min/max) (ms)", ] table.align["Workers"] = "l" table.align["Users"] = "l" table.align["User Classes"] = "l" table.align["Spawn Rate"] = "l" table.align["Fixed Users"] = "l" table.align["Iteration"] = "c" table.align["Ramp-Up (avg/min/max) (ms)"] = "c" table.align["Ramp-Down (avg/min/max) (ms)"] = "c" table.add_rows( [ [ f"{worker_count:,}", f"{user_count:,}", number_of_user_classes, f"{spawn_rate:,}", "50%" if fixed_users else "0%", iteration, cpu_ramp_up, cpu_ramp_down, ] for (worker_count, user_count, number_of_user_classes, spawn_rate, fixed_users, iteration), ( cpu_ramp_up, cpu_ramp_down, ) in results.items() ] ) print() print(table) if args.save_output: with open(f"results-dispatch-benchmarks-{int(now)}.txt", "w") as file: file.write(table.get_string()) with open(f"results-dispatch-benchmarks-{int(now)}.json", "w") as file: file.write(table.get_json_string()) ================================================ FILE: docs/_static/theme-overrides.css ================================================ /* Make it possible to have multiline table cells by removing white-space:nowrap */ .wy-table-responsive table td, .wy-table-responsive table th { white-space: normal; } .wy-table-responsive { margin-bottom: 24px; max-width: 100%; overflow: visible; } ================================================ FILE: docs/_templates/footer.html ================================================ {% extends "!footer.html" %} {% block extrafooter %} {{ super }} {% endblock %} ================================================ FILE: docs/api.rst ================================================ ### API ### User class ============ .. autoclass:: locust.User :members: wait_time, tasks, weight, fixed_count, abstract, on_start, on_stop, wait, context, environment HttpUser class ================ .. autoclass:: locust.HttpUser :members: wait_time, tasks, client, abstract HttpSession class ================= .. autoclass:: locust.clients.HttpSession :members: __init__, request, get, post, delete, put, head, options, patch FastHttpUser class ================== .. autoclass:: locust.contrib.fasthttp.FastHttpUser :members: wait_time, tasks, client, abstract, rest :noindex: MqttUser class ================== .. autoclass:: locust.contrib.mqtt.MqttUser :members: __init__, host, port, transport, ws_path, tls_context, client_cls, client_id, username, password, protocol .. _socketio: SocketIOUser class ================== .. autoclass:: locust.contrib.socketio.SocketIOUser :members: connect, call, emit, send, on_message, client, options :noindex: FastHttpSession class ===================== .. autoclass:: locust.contrib.fasthttp.FastHttpSession :members: __init__, request, get, post, delete, put, head, options, patch, iter_lines PostgresUser class ================== .. autoclass:: locust.contrib.postgres.PostgresUser :members: wait_time, tasks, client, abstract :noindex: MongoDBUser class ================= .. autoclass:: locust.contrib.mongodb.MongoDBUser :members: wait_time, tasks, client, abstract :noindex: MilvusUser class ================ .. autoclass:: locust.contrib.milvus.MilvusUser :members: wait_time, tasks, client, abstract :noindex: QdrantUser class ================ .. autoclass:: locust.contrib.qdrant.QdrantUser :members: wait_time, tasks, client, abstract :noindex: DNSUser class ========== .. autoclass:: locust.contrib.dns.DNSUser :members: client :noindex: TaskSet class ============= .. autoclass:: locust.TaskSet :members: user, parent, wait_time, client, tasks, interrupt, schedule_task, on_start, on_stop, wait task decorator ============== .. autofunction:: locust.task tag decorator ============== .. autofunction:: locust.tag SequentialTaskSet class ======================= .. autoclass:: locust.SequentialTaskSet :members: user, parent, wait_time, client, tasks, interrupt, schedule_task, on_start, on_stop .. _wait_time_functions: Built in wait_time functions ============================ .. automodule:: locust.wait_time :members: between, constant, constant_pacing, constant_throughput Response class ============== This class actually resides in the `requests `_ library, since that's what Locust is using to make HTTP requests, but it's included in the API docs for locust since it's so central when writing locust load tests. You can also look at the :py:class:`Response ` class at the `requests documentation `_. .. autoclass:: requests.Response :inherited-members: :noindex: ResponseContextManager class ============================ .. autoclass:: locust.clients.ResponseContextManager :members: success, failure .. _exceptions: Exceptions ========== .. autoexception:: locust.exception.InterruptTaskSet .. autoexception:: locust.exception.RescheduleTask .. autoexception:: locust.exception.RescheduleTaskImmediately Environment class ================= .. autoclass:: locust.env.Environment :members: .. _events: Event hooks =========== Locust provides event hooks that can be used to extend Locust in various ways. The following event hooks are available under :py:attr:`Environment.events `, and there's also a reference to these events under ``locust.events`` that can be used at the module level of locust scripts (since the Environment instance hasn't been created when the locustfile is imported). .. autoclass:: locust.event.Events :members: .. note:: It's highly recommended that you add a wildcard keyword argument in your event listeners to prevent your code from breaking if new arguments are added in a future version. EventHook class --------------- The event hooks are instances of the **locust.events.EventHook** class: .. autoclass:: locust.event.EventHook :members: Runner classes ===================== .. autoclass:: locust.runners.Runner :members: start, stop, quit, user_count .. autoclass:: locust.runners.LocalRunner .. autoclass:: locust.runners.MasterRunner :members: register_message, send_message .. autoclass:: locust.runners.WorkerRunner :members: register_message, send_message, client_id, worker_index Web UI class ============ .. autoclass:: locust.web.WebUI :members: Other ===== .. autoclass:: locust.shape.LoadTestShape :members: .. autoclass:: locust.stats.RequestStats :members: get .. autoclass:: locust.stats.StatsEntry .. autofunction:: locust.debug.run_single_user ================================================ FILE: docs/changelog.rst ================================================ #################### Changelog Highlights #################### For full details of changes, please see https://github.com/locustio/locust/releases or https://github.com/locustio/locust/blob/master/CHANGELOG.md 2.43.3 ====== * Update dependencies and some documentation, no functional changes 2.43.2 ====== * Upgrade various dependencies, no functional changes https://github.com/locustio/locust/pull/3339, https://github.com/locustio/locust/pull/3343, https://github.com/locustio/locust/pull/3342, https://github.com/locustio/locust/pull/3341, https://github.com/locustio/locust/pull/3340 2.43.1 ====== * Upgrade various Web UI dependencies, no other changes 2.43.0 ====== * Capture stacktrace on KeyboardInterrupt on greenlets https://github.com/locustio/locust/pull/3306 * Solving iter_lines problem https://github.com/locustio/locust/pull/3311 * Allow users to stop test run by raising StopTest, use it on missing host https://github.com/locustio/locust/pull/3313 * Remove references to locust.cloud now that it is shutting down https://github.com/locustio/locust/pull/3314 * Support requests>=2.32.5, reimplement the fix previously there for only loading ssl certificates once https://github.com/locustio/locust/pull/3316 2.42.6 ====== * GH Actions: Bump actions/checkout from 5 to 6 https://github.com/locustio/locust/pull/3287 * Fix Toml Parser Being Called on .conf Files https://github.com/locustio/locust/pull/3293 * Log duplicate client_ready messages as debug instead of info level https://github.com/locustio/locust/pull/3296 * Various minor code modernization https://github.com/locustio/locust/pull/3299, https://github.com/locustio/locust/pull/3301 * Don't import pytest unless it is really needed, to speed up startup https://github.com/locustio/locust/pull/3296 2.42.5 ====== * Log when otel is enabled https://github.com/locustio/locust/pull/3284 2.42.4 ====== * Update pytest dependency range https://github.com/locustio/locust/pull/3263 * Allow multiple dropdown in Web UI https://github.com/locustio/locust/pull/3261 * Add OpenTelemetry support https://github.com/locustio/locust/pull/3275, https://github.com/locustio/locust/pull/3276, https://github.com/locustio/locust/pull/3278, https://github.com/locustio/locust/pull/3279 2.42.3 ====== * Fix single line .conf files incorrectly being treated as toml https://github.com/locustio/locust/pull/3257 * Update various GitHub actions used in the build https://github.com/locustio/locust/pull/3266, https://github.com/locustio/locust/pull/3259 2.42.2 ====== * Remove setuptools from dependencies https://github.com/locustio/locust/pull/3253 * Fix an issue with .conf-files https://github.com/locustio/locust/pull/3256 2.42.1 ====== * Add VS Code Extension and k8s Operator to documentation https://github.com/locustio/locust/pull/3251 2.42.0 ====== * Introduce DNSUser https://github.com/locustio/locust/pull/3243 * Dependencies: allow gevent >25.8.1, avoid python-requests >2.32.5 2.41.6 ====== * Officially support Python 3.14 https://github.com/locustio/locust/pull/3235 2.41.5 ====== * Fix FastHttpUser handling of 308 response code * Fix Unsafe Template Arg https://github.com/locustio/locust/pull/3232 2.41.4 ====== (skipped because of failed publishing) 2.41.3 ====== * Make workers correctly ignore the --run-time flag https://github.com/locustio/locust/pull/3230 2.41.2 ====== * Fix Alignment of View Column Selector https://github.com/locustio/locust/pull/3226 * Pytest plugin: Workaround issue with potential duplicate --host argument definition https://github.com/locustio/locust/pull/3228 2.41.1 ====== * Fix incorrect url for feedback form 2.41.0 ====== * Suggest possibly misspelled command line arguments (Did you mean ...) https://github.com/locustio/locust/pull/3224 * Add Feedback Form https://github.com/locustio/locust/pull/3223 * Add MqttUser https://github.com/locustio/locust/pull/3216 2.40.5 ====== * Avoid wrapping FastResponse in response context manager when not needed https://github.com/locustio/locust/pull/3222 2.40.4 ====== * Avoid pytest style locustfiles capturing keyboard input https://github.com/locustio/locust/pull/3219 2.40.3 ====== * Make ResponseContextManager.raise_for_status() consider calls to failure() and success() https://github.com/locustio/locust/pull/3217 2.40.2 ====== * Refactor clients.ResponseContextManager and fix GC issue https://github.com/locustio/locust/pull/3210 * Refactor SocketIOUser to create separate SocketIOClient class https://github.com/locustio/locust/pull/3211 2.40.1 ====== * Pytest plugin: Delay imports to avoid monkey patching until someone actually uses the fixtures https://github.com/locustio/locust/pull/3204 https://github.com/locustio/locust/pull/3205 2.40.0 ====== * Add support for pytests as locustfiles https://github.com/locustio/locust/pull/3200 * Refactor FastHttpSession slightly https://github.com/locustio/locust/pull/3198 * Update Dockerfile base to Python 3.13 https://github.com/locustio/locust/pull/3193 2.39.1 ====== * Avoid broken gevent version for now by @cyberw in https://github.com/locustio/locust/pull/3196 2.39.0 ====== * Add MilvusUser https://github.com/locustio/locust/pull/3168 * Add SocketIOUser https://github.com/locustio/locust/pull/3189 2.38.1 ====== * FastHttpUser: Dont send zstd in Accept-Encoding header https://github.com/locustio/locust/pull/3188 2.38.0 ====== * Add MarkovTaskSet https://github.com/locustio/locust/pull/3176 * Support for list based custom arguments https://github.com/locustio/locust/pull/3181 * Webui fixes https://github.com/locustio/locust/pull/3177 & https://github.com/locustio/locust/pull/3185 2.37.14 ======= * Web UI: Fix Chart Zoom Slider https://github.com/locustio/locust/pull/3174 2.37.13 ======= * Web UI: Shrink Bundle Size by https://github.com/locustio/locust/pull/3169 * Remove safe_name from /stats/requests response https://github.com/locustio/locust/pull/3171 2.37.12 ======= * Attempt to increase open file limit (RLIMIT_NOFILE) even on master https://github.com/locustio/locust/pull/3162 2.37.11 ======= * Web UI: Always Warn of Invalid Host https://github.com/locustio/locust/pull/3155 * locust-cloud: bump minimum version and support multiple locustfiles https://github.com/locustio/locust/pull/3157 / https://github.com/locustcloud/locust-cloud/pull/38 2.37.10 ======= * Revert accidental removal of --json-file option https://github.com/locustio/locust/pull/3154 2.37.9 ====== * Web UI: Fix host field name missing if host is not required https://github.com/locustio/locust/pull/3152 2.37.8 ====== * Allow 25.x versions of gevent https://github.com/locustio/locust/pull/3151 2.37.7 ====== * Web Ui: Add host field validation https://github.com/locustio/locust/pull/3149 2.37.6 ====== * Doc updates, including a fix for config options https://github.com/locustio/locust/pull/3145 * Bumped minimum ConfigArgParse dependency to 1.7.1 2.37.5 ====== * Locust Cloud: Stop requiring a locustfile when doing --login or --delete https://github.com/locustio/locust/pull/3141 2.37.4 ====== * Bump minimum version of locust-cloud 2.37.3 ====== * Webui: Warn on Missing Host https://github.com/locustio/locust/pull/3140 2.37.2 ====== * Improve the performance of the `/stats/requests` endpoint when there are >500 unique request names https://github.com/locustio/locust/pull/3136 * Webui: Block starting test in distributed mode until workers have connected https://github.com/locustio/locust/pull/3138 2.37.1 ====== * Fix --json-file https://github.com/locustio/locust/pull/3131 2.37.0 ====== * Update uv to 0.7.2 * Add --json-file by https://github.com/locustio/locust/pull/3124 * Minor fixes 2.36.3 ====== * Bump minimum locust-cloud version + some minor fixes 2.36.2 ====== * Remove circular dependency between locust and locust-cloud https://github.com/locustio/locust/pull/3119 2.36.1 ====== * Ensure correct version of gevent on Python 3.12 2.36.0 ====== * Add locust-cloud as a dependency, so you can now run it using ``locust --cloud`` https://github.com/locustio/locust/pull/3097 * Various minor UI fixes & changes to build process https://github.com/locustio/locust/pull/3104, https://github.com/locustio/locust/pull/3107, https://github.com/locustio/locust/pull/3116 * Refactorings in main() https://github.com/locustio/locust/pull/3101, https://github.com/locustio/locust/pull/3102 * Narrow gevent version requirements to avoid incompatible new version for now https://github.com/locustio/locust/pull/3112 2.35.0 ====== * Add profile argument and display in html report https://github.com/locustio/locust/pull/3093 * Various UI fixes and dependency updates 2.34.1 ====== * Drop support for Python 3.9 https://github.com/locustio/locust/pull/3090 2.34.0 ====== * Add experimental OpenAIUser and example https://github.com/locustio/locust/pull/3081 * Make the Locust UI Responsive and work for smaller screens https://github.com/locustio/locust/pull/3083, https://github.com/locustio/locust/pull/3086 * Tiny fixes & version bumps 2.33.2 ====== * Optimize docker startup time https://github.com/locustio/locust/pull/3073 * Various dependency updates 2.33.1 ====== * Fix html report file naming crash https://github.com/locustio/locust/pull/3072 2.33.0 ====== * Press enter to automatically open web UI in browser https://github.com/locustio/locust/pull/3057 * Enable HTML Report Filename Parsing https://github.com/locustio/locust/pull/3049 * Various minor fixes and dependency updates 2.32.10 ======= * Use uv/hatch instead of Poetry https://github.com/locustio/locust/pull/3039 2.32.9 ====== * Fix issue where empty WebUI property is not parsed correctly https://github.com/locustio/locust/pull/3012 * Add iter_lines method to FastHttpSession https://github.com/locustio/locust/pull/3024 2.32.8 ====== * Fix a single broken link in the UI 2.32.7 ====== * Fix readme image ref links by @changchaishi in https://github.com/locustio/locust/pull/3031 * Add tab showing `Locust Cloud `_ features https://github.com/locustio/locust/pull/3032 * Minor UI enhancements https://github.com/locustio/locust/pull/3035, https://github.com/locustio/locust/pull/3036, https://github.com/locustio/locust/pull/3037 * Code quality: Fix unused imports and switch on related ruff check https://github.com/locustio/locust/pull/3034 2.32.6 ====== * Update Dockerfile to use Python 3.12 https://github.com/locustio/locust/pull/3029 * Remove deprecated --hatch-rate parameter https://github.com/locustio/locust/pull/3026 2.32.5 ====== * Treat exceptions in init event handler as fatal https://github.com/locustio/locust/pull/3009 * FastHttpUser: Fix SSL certificate loading performance issue https://github.com/locustio/locust/pull/3013 * Delay CPU usage checks to make it less likely to generate false positives during process startup https://github.com/locustio/locust/pull/3014 2.32.4 ====== * Minor doc updates https://github.com/locustio/locust/pull/2990, https://github.com/locustio/locust/pull/2989, https://github.com/locustio/locust/pull/2993 * UI fixes and dependency updates https://github.com/locustio/locust/pull/2992 https://github.com/locustio/locust/pull/2997, https://github.com/locustio/locust/pull/3002, https://github.com/locustio/locust/pull/2999 * Fix missing last requests in web UI https://github.com/locustio/locust/pull/3001 2.32.3 ====== * Add option for Extra Options to be Required https://github.com/locustio/locust/pull/2981 * Use debug log level for first 5s of waiting for workers to be ready https://github.com/locustio/locust/pull/2982 * Ensure we never use old version of setuptools https://github.com/locustio/locust/pull/2988 2.32.2 ====== * Better html report file names https://github.com/locustio/locust/pull/2947 * Fix Incorrectly Updating Stat History https://github.com/locustio/locust/pull/2972 * Various WebUI fixes (most only relevant for https://locust.cloud) 2.32.1 ====== * Various WebUI fixes (most only relevant for https://locust.cloud) * LocustBadStatusCode without url param in fasthttp https://github.com/locustio/locust/pull/2944 2.32.0 ====== * Explicitly support Python 3.13 https://github.com/locustio/locust/pull/2939 * Log deprecation warning for Python 3.9 https://github.com/locustio/locust/pull/2940 * Decide if ipv6 can work (especially relevant for EKS) https://github.com/locustio/locust/pull/2923 * Various minor fixes 2.31.8 ====== * Minor fixes, nothing worth mentioning. 2.31.7 ====== * Log locust version earlier https://github.com/locustio/locust/pull/2904 * Improve Web UI Logging https://github.com/locustio/locust/pull/2911 2.31.6 ====== * Various documentation and type hint fixes. * Fix issue when using --config-users to set weight to 0 https://github.com/locustio/locust/pull/2891 * Add worker_count = 1 to LocalRunner for parity with MasterRunner https://github.com/locustio/locust/pull/2900/files * Tiny WebUI fixes https://github.com/locustio/locust/pull/2901, https://github.com/locustio/locust/pull/2902 2.31.5 ====== * WebUI: Correct types for form select https://github.com/locustio/locust/pull/2872 * Web UI Remove Scroll to Zoom https://github.com/locustio/locust/pull/2876 * Webui Remove chart initial data fetch https://github.com/locustio/locust/pull/2878 * Improved Build Pipeline https://github.com/locustio/locust/pull/2873, https://github.com/locustio/locust/pull/2879, https://github.com/locustio/locust/pull/2880 2.31.4 ====== * Publish UI NPM package to simplify use from custom UIs (https://locust.cloud) * Some tiny UI fixes 2.31.3 ====== * Use new logo in web UI + some minor improvements https://github.com/locustio/locust/pull/2844, https://github.com/locustio/locust/pull/2840, https://github.com/locustio/locust/pull/2846, https://github.com/locustio/locust/pull/2850, https://github.com/locustio/locust/pull/2847, https://github.com/locustio/locust/pull/2855 * Update GitHub action versions https://github.com/locustio/locust/pull/2853 2.31.2 ====== * Prebuild UI in PyPi publish steps so that even source distributions contain web UI code https://github.com/locustio/locust/pull/2839 2.31.1 ====== * Fix issue with downloading HTML report https://github.com/locustio/locust/pull/2834 2.31.0 ====== * Fix HTML Report Stats Table https://github.com/locustio/locust/pull/2817 * URL Directory, and Multi-File Support for Locustfile Distribution https://github.com/locustio/locust/pull/2766 * Various UI improvements https://github.com/locustio/locust/pull/2815, https://github.com/locustio/locust/pull/2804, https://github.com/locustio/locust/pull/2824, https://github.com/locustio/locust/pull/2825, https://github.com/locustio/locust/pull/2826, https://github.com/locustio/locust/pull/2828 * Fix docker image build https://github.com/locustio/locust/pull/2830 2.30.0 ====== * FastHttpSession: Enable passing json as a positional argument for post() and stop converting response times to int https://github.com/locustio/locust/pull/2772 * New events for heartbeat and usage monitor https://github.com/locustio/locust/pull/2777 * SequentialTaskSet: Allow weighted tasks and dict in .tasks (experimental) https://github.com/locustio/locust/pull/2742 * Implement Poetry build system (mainly so we don't have to commit the built frontend resources to git) https://github.com/locustio/locust/pull/2725 * UI: Replace total avg response time with 50 percentile (avg was broken) https://github.com/locustio/locust/pull/2806 * Avoid deadlock in gevent/urllib3 connection pool (fixes occasional worker heartbeat timeouts) https://github.com/locustio/locust/pull/2813 * This release got no docker image due to a build error 2.29.1 ====== * Add option to Skip Monkey Patching with LOCUST_SKIP_MONKEY_PATCH https://github.com/locustio/locust/pull/2765 * HttpSession requests typing https://github.com/locustio/locust/pull/2699 * Add proxy support for FastHttpUser https://github.com/locustio/locust/pull/2758 2.29.0 ====== * Ensure spawning_complete only happens once on workers https://github.com/locustio/locust/pull/2728 * Correct time in the downloaded HTML report https://github.com/locustio/locust/pull/2729 * Add date and zoom to charts in web UI https://github.com/locustio/locust/pull/2731 * Send logs from workers to master and improve log viewer tab in the Web UI https://github.com/locustio/locust/pull/2750 * Docs: Upgrade Sphinx and theme, Fix API TOC, import wiki to docs, and mention installing Locust in Building the Docs 2.28.0 ====== * Remove legacy UI https://github.com/locustio/locust/pull/2703 * Stop HTML escaping errors for /stats/requests endpoint https://github.com/locustio/locust/pull/2710 * Various minor UI & logging improvements 2.27.0 ====== * Simplify how locustfiles are found (using -f). Don’t automatically append .py https://github.com/locustio/locust/pull/2655 * Use more efficient algorithm to calculate user distribution, and allow float weights https://github.com/locustio/locust/pull/2686 * Various minor fixes 2.26.0 ====== * Drop support for Python 3.8 * Update geventhttpclient and adjust FastHttpUser max_retries / max_redirects (https://github.com/locustio/locust/pull/2676) * Pin gevenhttpclient version (https://github.com/locustio/locust/pull/2682) 2.25.0 ====== * Add functionality to run listener functions for `custom_messages` concurrently (https://github.com/locustio/locust/pull/2650) * Update User Classes in Distributed Mode (https://github.com/locustio/locust/pull/2666) * Log deprecation warning for --legacy-ui (https://github.com/locustio/locust/pull/2670) * Fix UserClasses weight distribution with gcd (https://github.com/locustio/locust/pull/2663) 2.24.1 ====== * Some documentation updates & minor fixes to UI * Fixes to FastHttpUser content streaming (https://github.com/locustio/locust/pull/2642, https://github.com/locustio/locust/pull/2643) 2.24.0 ====== * Pluggable dispatcher logic https://github.com/locustio/locust/pull/2606 * pyproject.toml support for Locust configuration file https://github.com/locustio/locust/pull/2612 * Minor fixes 2.23.1 ====== * Fixes for locustfile download https://github.com/locustio/locust/pull/2599 * UI fixes https://github.com/locustio/locust/pull/2600 https://github.com/locustio/locust/pull/2601 2.23.0 ====== * UI updates (https://github.com/locustio/locust/pull/2589, https://github.com/locustio/locust/pull/2590, https://github.com/locustio/locust/pull/2596) * Locustfile distribution from master to worker https://github.com/locustio/locust/pull/2583 * Allow getting locust files from http urls https://github.com/locustio/locust/pull/2595 * Use exec_module() when loading locustfile instead of the deprecated load_module() https://github.com/locustio/locust/pull/2576 2.22.0 ====== * Use Modern UI by default, remove --modern-ui and add --legacy-ui parameters https://github.com/locustio/locust/pull/2569 2.21.0 ====== * Switch from flake8 + black to ruff for linting and formatting of code * Update shape class' runner when Web UI picker is used by https://github.com/locustio/locust/pull/2534 * Web UI Modern Auth https://github.com/locustio/locust/pull/2538 * Customization Feature for Percentile Display on Statistics Page https://github.com/locustio/locust/pull/2550 * Allow User weight adjustment (and task selection) in UI when running with --class-picker, or on command line with --config-users argument https://github.com/locustio/locust/pull/2559 * Optimize memory usage when using --processes https://github.com/locustio/locust/pull/2564 2.20.1 ====== * run_single_user improvements https://github.com/locustio/locust/pull/2519 * Support IPv6 for zmq connection between master and worker https://github.com/locustio/locust/pull/2521 * Modern UI: Update Vite to 4.5.1 https://github.com/locustio/locust/pull/2530 * Other tiny fixes 2.20.0 ====== * Add event.measure context manager for simpler firing of request event (experimental) https://github.com/locustio/locust/pull/2511 * Various improvements to modern UI https://github.com/locustio/locust/pull/2491 * Various tiny fixes 2.19.1 ====== * Create any directories as part of the CSV Prefix https://github.com/locustio/locust/pull/2481 * Dont suppress StopUser or GreenletExit in on_stop https://github.com/locustio/locust/pull/2486 * FastHttpUser: Detect response text encoding when no information is present in headers https://github.com/locustio/locust/pull/2485 2.19.0 ====== * Add --processes parameter to automatically fork subprocesses for workers https://github.com/locustio/locust/pull/2472 * Automatically shut down workers if master goes missing for too long https://github.com/locustio/locust/pull/2474 * Update minimum version of various dependencies https://github.com/locustio/locust/pull/2476 2.18.4 ====== * Various fixes to Modern UI * Ensure to wait a second before next call to LoadTestShape's tick() https://github.com/locustio/locust/pull/2465 2.18.3 ====== * Modern UI: Add sorting to columns on statistics page and downloaded report https://github.com/locustio/locust/pull/2453 2.18.2 ====== * FastHttpUser: encoding return str when response is empty https://github.com/locustio/locust/pull/2451 2.18.1 ====== * Add Log Viewer to Modern UI https://github.com/locustio/locust/pull/2440 2.18.0 ====== * Add a modern web UI based on React, MaterialUI and Vite (activated using --modern-ui) https://github.com/locustio/locust/pull/2405 * Stop supporting Python 3.7 https://github.com/locustio/locust/pull/2421 * Fix too long first wait time for constant_pacing (and constant_throughput) https://github.com/locustio/locust/pull/2428 2.17.0 ====== * Support user abstract load shape base classes https://github.com/locustio/locust/pull/2393 * Allow LoadShapes to reuse run-time, spawn-rate and users parameters https://github.com/locustio/locust/pull/2395 * Improve performance for statistics handling https://github.com/locustio/locust/pull/2410 * Test and explicitly support Python 3.12 https://github.com/locustio/locust/pull/2411 2.16.1 ====== * Deprecate LOCUST_PLAYWRIGHT env var https://github.com/locustio/locust/pull/2378 * Import locust_plugins if available to give access to its custom command line arguments https://github.com/locustio/locust/pull/2379 2.16.0 ====== * Add worker_connect event https://github.com/locustio/locust/pull/2344 * Allow selecting user classes using LOCUST_USER_CLASSES env var https://github.com/locustio/locust/pull/2355 * Web UI dropdown for custom args with choices https://github.com/locustio/locust/pull/2372 * Various minor fixes 2.15.1 ====== * Add PERCENTILES_TO_CHART param in stats.py to make the Response Time Chart configurable https://github.com/locustio/locust/pull/2313 2.15.0 ====== * Add is_secret option for custom args to be shown in the web UI masked https://github.com/locustio/locust/pull/2284 * Breaking change: Remove deprecated request_success and request_failure event handlers (unified request handler was introduced in 1.5) https://github.com/locustio/locust/pull/2306 2.14.2 ====== * Re-add py.typed marker file to package (it was missing in 2.14.1) https://github.com/locustio/locust/pull/2282 2.14.1 ====== * Add --json to send stats to stdout as json by @AndersSpringborg in https://github.com/locustio/locust/pull/2269 2.14.0 ====== * Add rest method to FastHttpUser to facilitate easy REST/JSON API testing https://github.com/locustio/locust/pull/2274 2.13.2 ====== * Fix: Ask worker to reconnect if master gets a broken RPC message by @marcinh in https://github.com/locustio/locust/pull/2271 2.13.1 ====== * Document har2locust (auto generation of locustfiles from browser recordings) https://github.com/locustio/locust/pull/2259 * Dont reset connection to worker if master receives a corrupted zmq message by @marcinh in https://github.com/locustio/locust/pull/2266 * Other minor fixes 2.13.0 ====== * Add the ability to set default_headers on FastHttpUser https://github.com/locustio/locust/pull/2231 * Web UI: URL link on the host name for easy navigation by @JonanOribe in https://github.com/locustio/locust/pull/2228 * Add support for time strings for --stop timeout (e.g. "5m30s") @cyberw in https://github.com/locustio/locust/pull/2239 2.12.1 ====== * Allow setting run time from the web UI & http api by @ajt89 in https://github.com/locustio/locust/pull/2202 * Various fixes 2.12.0 ====== * LoadTestShapes with custom user classes https://github.com/locustio/locust/pull/2181 * Minor fixes and bumped some dependencies 2.11.1 ====== * Fix issue when editing user count while running a test using --class-picker https://github.com/locustio/locust/pull/2171 * Various minor logging fixes 2.11.0 ====== * Allow passing multiple Locustfiles, allow selecting User and Shape class from the WebUI https://github.com/locustio/locust/pull/2137 * Add 'worker_index' to WorkerRunner https://github.com/locustio/locust/pull/2155 * Fix: Ensure new test starts with specified number of users after previous test has been stopped https://github.com/locustio/locust/pull/2152 2.10.2 ====== * Fix for Flask 2.2.0 breaking changes https://github.com/locustio/locust/pull/2148 2.10.1 ====== * Increase CONNECT_RETRY_COUNT to avoid workers giving up too soon if master is not up yet by https://github.com/locustio/locust/pull/2125 2.10.0 ====== * Add ack for worker connection https://github.com/locustio/locust/pull/2077 (note that 2.10 workers will not work with a 2.9 master) * add support for custom SSLContext when using FastHttpUser https://github.com/locustio/locust/pull/2113 * More robust handling of ZMQ/RPC errors https://github.com/locustio/locust/pull/2120 / https://github.com/locustio/locust/pull/2096 * Full Changelog https://github.com/locustio/locust/compare/2.9.0...2.10.0 2.9.0 ===== * FastHttpUser improvements (including a rename of parameter "url" to "path") https://github.com/locustio/locust/pull/2083 * Modernized build https://github.com/locustio/locust/pull/2070 * Drop support for Python 3.6 https://github.com/locustio/locust/pull/2080 * Add table linkage in UI https://github.com/locustio/locust/pull/2082 * Uniform style of stats/report ascii tables https://github.com/locustio/locust/pull/2084 * Remove explicit version requirement for jinja2 https://github.com/locustio/locust/pull/2090 * Rebalance users even when using fixed_count https://github.com/locustio/locust/pull/2093 * Avoid using incompatible pyzmq 23 https://github.com/locustio/locust/pull/2100 2.8.6 ===== * Support sharing connection pools between users https://github.com/locustio/locust/pull/2059 * Add cpu_warning event, so listeners can do some action when CPU usage is too high https://github.com/locustio/locust/pull/2067 2.8.5 ===== * Fix dependency: Dont use latest Jinja2 because it has breaking changes 2.8.4 ===== * New event: `test_stopping`, triggered just before stopping the test https://github.com/locustio/locust/pull/2033 * New event: `quit`, to enable getting the locust process exit code https://github.com/locustio/locust/pull/2049 * Fix users sometimes not being stopped correctly https://github.com/locustio/locust/pull/2041 2.8.3 ===== * Ensure users are distributed evently across hosts during ramp up https://github.com/locustio/locust/pull/2025 2.8.2 ===== * Fix issue with permissions in docker image 2.8.1 ===== * Further optimize docker image (60MB compressed) 2.8.0 ===== * Shrink docker image significantly (95MB compressed size for x64 instead of 358MB) by basing the image on python3-slim instead of python3 * Fix empty tasks section in UI and static report bug (really) https://github.com/locustio/locust/pull/2001 2.7.3 ===== * Fix 'Tasks' section remains empty in web ui https://github.com/locustio/locust/pull/1997 2.7.2 ===== * Fix an issue introduced in 2.7.1 that caused Locust to shut down when the UI stop was clicked https://github.com/locustio/locust/pull/1996 2.7.1 ===== * fix --html report in web mode https://github.com/locustio/locust/pull/1992 2.7.0 ===== * Add run_single_user and documentation on how to debug Users/locustfiles https://github.com/locustio/locust/pull/1985 * Fix "socket operation on non-socket" at shutdown, by reverting #1935 https://github.com/locustio/locust/pull/1991 * Fixing issue with incorrect "All users spawned" log messages https://github.com/locustio/locust/pull/1977 2.6.1 ===== * Documentation fixes only. 2.6.0 ===== * Pass --tags and --exclude-tags to workers. (https://github.com/locustio/locust/pull/1976) * Clean up some logging messages (https://github.com/locustio/locust/pull/1973) * Ensure heartbeat\_worker doesn't try to re-establish connection to workers when quit has been called (https://github.com/locustio/locust/pull/1972) * fixed\_count: ability to spawn a specific number of users \(as opposed to just using weights\) (https://github.com/locustio/locust/pull/1964) 2.5.1 ===== * Ignore empty host field in web ui (Fix running the web UI with class defined hosts) (https://github.com/locustio/locust/pull/1956) * Throw exception when calling response.success()/.failure() if with-block has not been entered (https://github.com/locustio/locust/pull/1955) * Stop declaring "fake" class level variables in Environment, User and StatsEntry (https://github.com/locustio/locust/pull/1948) 2.5.0 ===== * Change request event 'url' parameter to contain full URL (technically a breaking change, but very few users will have had time to start using this) (https://github.com/locustio/locust/issues/1927) * Suppress warnings for patch version mismatch between master and worker (https://github.com/locustio/locust/issues/1926) 2.4.3 ===== * Fix crash on windows (https://github.com/locustio/locust/issues/1924) 2.4.2 ===== * Add --expect-workers-max-wait parameter (https://github.com/locustio/locust/pull/1922) * Track worker memory usage (https://github.com/locustio/locust/pull/1917) * Other small fixes 2.4.1 ===== * Fix stat printing when using shapes (https://github.com/locustio/locust/pull/1907) 2.4.0 ===== * Add start_time and url parameters to request event. (https://github.com/locustio/locust/pull/1900) * Support (and test) Python 3.10 (https://github.com/locustio/locust/pull/1901) * Make User.run/TaskSet.run final and raise an exception if someone marks it as a task (https://github.com/locustio/locust/pull/1895) * Release docker image for arm64. (https://github.com/locustio/locust/pull/1889) * Automated change log generation is broken. Will fix this later, but until then you can look here: https://github.com/locustio/locust/compare/2.2.3...2.4.0 2.3.0 ===== * Accidentally increased version to 2.4 directly so there is no 2.3... 2.2.3 ===== * Fix issue with custom arguments in config file (when not running headless) (https://github.com/locustio/locust/pull/1888) * Automated change log generation is broken. Will fix this later, but until then you can look here: https://github.com/locustio/locust/compare/2.2.2...2.2.3 2.2.2 ===== * Fix version in Docker builds * Automated change log generation is broken. Will fix this later, but until then you can look here: https://github.com/locustio/locust/compare/2.2.1...2.2.2 2.2.1 ===== * Automated change log generation is broken. Will fix this later, but until then you can look here: https://github.com/locustio/locust/compare/2.2.0...2.2.1 2.2.0 ===== * Display locustfile and tasks ratio information on index.html * Add --autostart and --autoquit parameters (https://github.com/locustio/locust/pull/1864) * Add constant\_throughput wait time \(the inverse of constant\_pacing\) * Alternative way to rename requests (particularly useful when using an SDK that wraps `requests`) (https://github.com/locustio/locust/pull/1858) * Add --equal-weights flag (https://github.com/locustio/locust/pull/1842) * HttpUser: Unpack known exceptions * Various charting fixes * Add FastHttpUser directly under locust package * Auto-generate Locust's version number using setuptools\_scm and git tags * Show custom arguments in web ui and forward them to worker (https://github.com/locustio/locust/pull/1841) 2.1.0 ===== * Fix docker builds (2.0 never got pushed to Docker Hub) * Bump dependency on pyzmq to fix out of memory issue on Windows * Use 1 as default for user count and spawn rate in web UI start form * Various documentation updates 2.0.0 ===== User ramp up/down and User type selection is now controlled by the master instead of autonomously by the workers ---------------------------------------------------------------------------------------------------------------- This has allowed us to fix some issues with incorrect/skewed User type selection and undesired stepping of ramp up. The issues were especially visible when running many workers and/or using LoadShape:s. This change also allows redistribution of Users if a worker disconnects during a test. This is a major change internally in Locust so please let us know if you encounter any problems (particularly regarding ramp up pace, User distribution, CPU usage on master, etc) Other potentially breaking API changes -------------------------------------- * Change the default User weight to 1 instead of 10 (the old default made no sense) * Fire test_start and test_stop events on workers too (previously they were only fired on master/standalone instances) * Workers now send their version number to master. Master will warn about version differences, and pre 2.0-versions will not be allowed to connect at all (because they would not work anyway) * Update Flask dependency to 2.0 Significant merged PR:s (and prerelease version they were introduced in) ------------------------------------------------------------------------ * Allow workers to bypass version check by sending -1 as version (2.0.0) https://github.com/locustio/locust/pull/1830 * Improve logging messages and clean up code after dispatch refactoring (2.0.0b4) https://github.com/locustio/locust/pull/1826 * Remove `user_classes_count` from heartbeat payload (2.0.0b4) https://github.com/locustio/locust/pull/1825 * Add option to set concurrency of FastHttpUser/Session (2.0.0b3) https://github.com/locustio/locust/pull/1812/ * Fire test_start and test_stop events on worker nodes (2.0.0b3) https://github.com/locustio/locust/pull/1777/ * Auto shrink request stats table to fit terminal (2.0.0b2) https://github.com/locustio/locust/pull/1811 * Refactoring of the dispatch logic to improve performance (2.0.0b2) https://github.com/locustio/locust/pull/1809 * Check version of workers when they connect. Warn if there is a mismatch, refuse 1.x workers to connect (2.0.0b1) https://github.com/locustio/locust/pull/1805 * Change the default User weight to 1 instead of 10 (2.0.0b1) https://github.com/locustio/locust/pull/1803 * Upgrade to Flask 2 (2.0.0b1) https://github.com/locustio/locust/pull/1764 * Move User selection responsibility from worker to master in order to fix unbalanced distribution of users and uneven ramp-up (2.0.0b0) https://github.com/locustio/locust/pull/1621 Some of these are not really that significant and may be removed from this list at a later time, once 2.0 has stabilised. 1.6.0 ===== * Allow cross process communication using custom messages https://github.com/locustio/locust/pull/1782 * Fix: status "stopped" instead of "spawning", tick\(\) method of LoadShape called only once https://github.com/locustio/locust/pull/1769 1.5.3 ===== * Fix an issue with custom Users calling request_success/_failure.fire() not being added to statistics https://github.com/locustio/locust/pull/1761 1.5.2 ===== * Pin version of flask to 1.1.2, fixing https://github.com/locustio/locust/issues/1759 * Fix issue with GRPC compatibility and add GRPC example to documentation https://github.com/locustio/locust/pull/1755 * Use time.perf_counter() to calculate elapsed times everywhere, should only matter for Windows https://github.com/locustio/locust/pull/1758 1.5.1 ===== * Fixed an issue with 1.5.0 where an extra parameter (start_time) was passed to request event https://github.com/locustio/locust/pull/1754 1.5.0 ===== * Unify request_success/request_failure into a single event called request (the old ones are deprecated but still work) https://github.com/locustio/locust/issues/1724 * Add the response object and context as parameters to the request event. context is used to forward information to the request event handler (can be used for things like username, tags etc) 1.4.4 ===== * Ensure runner.quit finishes even when users are broken https://github.com/locustio/locust/pull/1728 * Make runner / user count available to LoadTestShape https://github.com/locustio/locust/pull/1719 * Other small fixes 1.4.3 ===== * Fix bug that broke the tooltips for charts in the Web UI 1.4.2 ===== * Multiple improvements for charting including tooltips etc * Added --html option to save HTML report https://github.com/locustio/locust/pull/1637 * Lots of other small fixes 1.4.1 ===== * Fix 100% cpu usage when running in docker/non-tty terminal https://github.com/locustio/locust/issues/1629 1.4.0 ===== * You can now control user count from terminal while the test is running https://github.com/locustio/locust/pull/1612 * Infinite run time is now the default for command line runs https://github.com/locustio/locust/pull/1625 * wait_time now defaults to zero https://github.com/locustio/locust/pull/1626 1.3.2 ===== * List Python 3.9 as supported in the package/on PyPi * Fix XSS vulnerability in the web UI (sounds important but really isn't, as Locust UI is not meant to be exposed to outside users) 1.3.1 ===== * Bump minimum required gevent version to 20.9.0 (latest), as the previous ones had sneaky binary incompatibilities with the latest version of greenlet ("RuntimeWarning: greenlet.greenlet size changed, may indicate binary incompatibility. Expected 144 from C header, got 152 from PyObject") 1.3.0 ===== * Breaking change: Remove step-load feature (now that we have LoadTestShape it is no longer needed) * More type hints to enable better code completion and linting of locustfiles Bug fixes: * LoadTestShape.get\_run\_time is not relative to start of test https://github.com/locustio/locust/issues/1557 * Refactor and fix delayed user stopping in combination with on\_stop https://github.com/locustio/locust/pull/1560 * runner.quit gets blocked by slow on stop https://github.com/locustio/locust/issues/1552 * Remove legacy code that was only needed for py2 * Lots more 1.2.3 ===== * Bug fix (TypeError: code() takes at least 14 arguments (13 given) (Werkzeug version issue) https://github.com/locustio/locust/issues/1545) * Bug fix (Locust stuck in "Shape worker starting" when restarting a test from the webUI https://github.com/locustio/locust/issues/1540) * Various linting fixes that *should* have no functional impact 1.2.2 ===== * Bug fix (LoadTestShape in headless mode https://github.com/locustio/locust/pull/1539) 1.2.1 ===== * Bug fix (StatsEntry.use_response_times_cache must be set to True, https://github.com/locustio/locust/issues/1531) 1.2 === * Rename hatch rate to spawn rate (the --hatch-rate parameter is only deprecated, but the hatch_complete event has been renamed spawning_complete) * Ability to generate any custom load shape with LoadTestShape class * Allow ramping down of users * Ability to use save custom percentiles * Improve command line stats output * Bug fixes (excessive precision of metrics in losust csv stats, negative response time when system clock has changed, issue with non-string failure messages, some typos etc) * Documentation improvements 1.1.1 ===== * --run-time flag is not respected if there is an exception in a test_stop listener * FastHttpUser: Handle stream ended at an unexpected time and UnicodeDecodeError. Show bad/error status codes on failures page. * Improve logging when locust master port is busy 1.1 === * The official Docker image is now based on the ``python:3.8`` image instead of ``python:3.8-alpine``. This should make it easier to install other python packages when extending the locust docker image. * Allow Users to stop the runner by calling self.environment.runner.quit() (without deadlocking sometimes) * Cut to only 5% free space on the top of the graphs * Use csv module to generate csv data (solves issues with sample names that need escaping in csv) * Various documentation improvements 1.0.3 ===== * Ability to control the exit code of the Locust process by setting :py:attr:`Environment.process_exit_code ` * FastHttpLocust: Change dependency to use original geventhttpclient (now that releases can be made there) instead of geventhttpclient-wheels * Fix search on readthedocs 1.0.2 ===== * Check for low open files limit (ulimit) and try to automatically increase it from within the locust process. * Other various bug fixes as improvements .. _changelog-1-0: 1.0, 1.0.1 ========== This version contains some breaking changes. Locust class renamed to User ---------------------------- We've renamed the ``Locust`` and ``HttpLocust`` classes to ``User`` and ``HttpUser``. The ``locust`` attribute on :py:class:`TaskSet ` instances has been renamed to :py:attr:`user `. The parameter for setting number of users has also been changed, from ``-c`` / ``--clients`` to ``-u`` / ``--users``. Ability to declare @task directly under the ``User`` class ---------------------------------------------------------- It's now possible to declare tasks directly under a User class like this: .. code-block:: python class WebUser(User): @task def some_task(self): pass In tasks declared under a User class (e.g. ``some_task`` in the example above), ``self`` refers to the User instance, as one would expect. For tasks defined under a :py:class:`TaskSet ` class, ``self`` would refer to the ``TaskSet`` instance. The ``task_set`` attribute on the ``User`` class (previously ``Locust`` class) has been removed. To declare a ``User`` class with a single ``TaskSet`` one would now use the :py:attr:`tasks ` attribute instead: .. code-block:: python class MyTaskSet(TaskSet): ... class WebUser(User): tasks = [MyTaskSet] Task tagging ------------ A new :ref:`tag feature ` has been added that makes it possible to include/exclude tasks during a test run. Tasks can be tagged using the :py:func:`@tag ` decorator: .. code-block:: python class WebUser(User): @task @tag("tag1", "tag2") def my_task(self): ... And tasks can then be specified/excluded using the ``--tags``/``-T`` and ``--exclude-tags``/``-E`` command line arguments. Environment variables changed ----------------------------- The following changes has been made to the configuration environment variables * ``LOCUST_MASTER`` has been renamed to ``LOCUST_MODE_MASTER`` (in order to make it less likely to get variable name collisions when running Locust in Kubernetes/K8s which automatically adds environment variables depending on service/pod names). * ``LOCUST_SLAVE`` has been renamed to ``LOCUST_MODE_WORKER``. * ``LOCUST_MASTER_PORT`` has been renamed to ``LOCUST_MASTER_NODE_PORT``. * ``LOCUST_MASTER_HOST`` has been renamed to ``LOCUST_MASTER_NODE_HOST``. * ``CSVFILEBASE`` has been renamed to ``LOCUST_CSV``. See the :ref:`configuration` documentation for a full list of available :ref:`environment variables `. Other breaking changes ---------------------- * The master/slave terminology has been changed to master/worker. Therefore the command line arguments ``--slave`` and ``--expect-slaves`` has been renamed to ``--worker`` and ``--expect-workers``. * The option for running Locust without the Web UI has been renamed from ``--no-web`` to ``--headless``. * Removed ``Locust.setup``, ``Locust.teardown``, ``TaskSet.setup`` and ``TaskSet.teardown`` hooks. If you want to run code at the start or end of a test, you should instead use the :py:attr:`test_start ` and :py:attr:`test_stop ` events: .. code-block:: python from locust import events @events.test_start.add_listener def on_test_start(**kw): print("test is starting") @events.test_stop.add_listener def on_test_start(**kw): print("test is stopping") * ``TaskSequence`` and ``@seq_task`` has been replaced with :ref:`SequentialTaskSet `. * A ``User count`` column has been added to the history stats CSV file. The column order and column names has been changed. * The official docker image no longer uses a shell script with a bunch of special environment variables to configure how how locust is started. Instead, the ``locust`` command is now set as ``ENTRYPOINT`` of the docker image. See :ref:`running-in-docker` for more info. * Command line option ``--csv-base-name`` has been removed, since it was just an alias for ``--csv``. * The way Locust handles logging has been changed. We no longer wrap stdout (and stderr) to automatically make print statements go into the log. ``print()`` statements now only goes to stdout. To add custom entries to the log, one should now use the Python logging module: .. code-block:: python import logging logging.info("custom logging message) For more info see :ref:`logging` Web UI improvements ------------------- * It's now possible to protect the Web UI with Basic Auth using the ``--web-auth`` command line argument. * The Web UI can now be served over HTTPS by specifying a TLS certificate and key with the ``--tls-cert`` and ``--tls-key`` command line arguments. * If the number of users and hatch rate are specified on command line, it's now used to pre-populate the input fields in the Web UI. Other fixes and improvements ---------------------------- * Added ``--config`` command line option for specifying a :ref:`configuration file ` path * The code base has been refactored to make it possible to run :ref:`Locust as a python lib `. * It's now possible to call ``response.failure()`` or ``response.success()`` multiple times when using the ``catch_response=True`` in the HTTP clients. Only the last call to ``success``/``failure`` will count. * The ``--help`` output has been improved by grouping related options together. 0.14.6 ====== * Fix bug when running with latest Gevent version, and pinned the latest version 0.14.0 ====== * Drop Python 2 and Python 3.5 support! * Continuously measure CPU usage and emit a warning if we get a five second average above 90% * Show CPU usage of slave nodes in the Web UI * Fixed issue when running Locust distributed and new slave nodes connected during the hatching/ramp-up phase (https://github.com/locustio/locust/issues/1168) 0.13.5 ====== Various minor fixes, mainly regarding FastHttpLocust. 0.13.4 ====== Identical to previous version, but now built & deployed to Pypi using Travis. 0.13.3 ====== * Unable to properly connect multiple slaves - https://github.com/locustio/locust/issues/1176 * Zero exit code on exception - https://github.com/locustio/locust/issues/1172 * `--stop-timeout` is not respected when changing number of running Users in distributed mode - https://github.com/locustio/locust/issues/1162 0.13.2 ====== * Fixed bug that broke the Web UI's response time graph 0.13.1 ====== * Fixed crash bug on Python 3.8.0 * Various other bug fixes and improvements. 0.13.0 ====== * New API for specifying wait time - https://github.com/locustio/locust/pull/1118 Example of the new API:: from locust import HttpLocust, between class User(HttpLocust): # wait between 5 and 30 seconds wait_time = between(5, 30) There are three built in :ref:`wait time functions `: :py:func:`between `, :py:func:`constant ` and :py:func:`constant_pacing `. * FastHttpLocust: Accept self signed SSL certificates, ignore host checks. Improved response code handling * Add current working dir to sys.path - https://github.com/locustio/locust/pull/484 * Web UI improvements: Added 90th percentile to table, failure per seconds as a series in the chart * Ability to specify host in web ui * Added response_length to request_failure event - https://github.com/locustio/locust/pull/1144 * Added p99.9 and p99.99 to request stats distribution csv - https://github.com/locustio/locust/pull/1125 * Various other bug fixes and improvements. 0.12.2 ====== * Added `--skip-log-setup` to disable Locust's default logging setup. * Added `--stop-timeout` to allow tasks to finish running their iteration before stopping * Added 99.9 and 99.99 percentile response times to csv output * Allow custom clients to set request response time to None. Those requests will be excluded when calculating median, average, min, max and percentile response times. * Renamed the last row in statistics table from "Total" to "Aggregated" (since the values aren't a sum of the individual table rows). * Some visual improvements to the web UI. * Fixed issue with simulating fewer number of locust users than the number of slave/worker nodes. * Fixed bugs in the web UI related to the fact that the stats table is truncated at 500 entries. * Various other bug fixes and improvements. 0.12.1 ====== * Added new :code:`FastHttpLocust` class that uses a faster HTTP client, which should be 5-6 times faster than the normal :code:`HttpLocust` class. For more info see the documentation on :ref:`increasing performance `. * Added ability to set the exit code of the locust process when exceptions has occurred within the user code, using the :code:`--exit-code-on-error` parameter. * Added TCP keep alive to master/slave communication sockets to avoid broken connections in some environments. * Dropped support for Python 3.4 * Numerous other bug fixes and improvements. 0.10.0 ====== * Python 3.7 support * Added a status page to the web UI when running Locust distributed showing the status of slave nodes and detect down slaves using heartbeats * Numerous bugfixes/documentation updates (see detailed changelog) 0.9.0 ===== * Added detailed changelog (https://github.com/locustio/locust/blob/master/CHANGELOG.md) * Numerous bugfixes (see detailed changelog) * Added sequential task support - https://github.com/locustio/locust/pull/827 * Added support for user-defined wait_function - https://github.com/locustio/locust/pull/785 * By default, Locust no longer resets the statistics when the hatching is complete. Therefore :code:`--no-reset-stats` has been deprecated (since it's now the default behavior), and instead a new :code:`--reset-stats` option has been added. * Dropped support for Python 3.3 * Updated documentation 0.8.1 ===== * Updated pyzmq version, and changed so that we don't pin a specific version. This makes it easier to install Locust on Windows. 0.8 === * Python 3 support * Dropped support for Python 2.6 * Added :code:`--no-reset-stats` option for controlling if the statistics should be reset once the hatching is complete * Added charts to the web UI for requests per second, average response time, and number of simulated users. * Updated the design of the web UI. * Added ability to write a CSV file for results via command line flag * Added the URL of the host that is currently being tested to the web UI. * We now also apply gevent's monkey patching of threads. This fixes an issue when using Locust to test Cassandra (https://github.com/locustio/locust/issues/569). * Various bug fixes and improvements 0.7.5 ===== * Use version 1.1.1 of gevent. Fixes an install issue on certain versions of python. 0.7.4 ===== * Use a newer version of requests, which fixed an issue for users with older versions of requests getting ConnectionErrors (https://github.com/locustio/locust/issues/273). * Various fixes to documentation. 0.7.3 ===== * Fixed bug where POST requests (and other methods as well) got incorrectly reported as GET requests, if the request resulted in a redirect. * Added ability to download exceptions in CSV format. Download links has also been moved to its own tab in the web UI. 0.7.2 ===== * Locust now returns an exit code of 1 when any failed requests were reported. * When making an HTTP request to an endpoint that responds with a redirect, the original URL that was requested is now used as the name for that entry in the statistics (unless an explicit override is specified through the *name* argument). Previously, the last URL in the redirect chain was used to label the request(s) in the statistics. * Fixed bug which caused only the time of the last request in a redirect chain to be included in the reported time. * Fixed bug which caused the download time of the request body not to be included in the reported response time. * Fixed bug that occurred on some linux dists that were tampering with the python-requests system package (removing dependencies which requests is bundling). This bug only occurred when installing Locust in the python system packages, and not when using virtualenv. * Various minor fixes and improvements. 0.7.1 ===== * Exceptions that occurs within TaskSets are now caught by default. * Fixed bug which caused Min response time to always be 0 after all locusts had been hatched and the statistics had been reset. * Minor UI improvements in the web interface. * Handle messages from "zombie" slaves by ignoring the message and making a log entry in the master process. 0.7 === HTTP client functionality moved to HttpLocust --------------------------------------------- Previously, the Locust class instantiated a :py:class:`HttpSession ` under the client attribute that was used to make HTTP requests. This functionality has now been moved into the :py:class:`HttpLocust ` class, in an effort to make it more obvious how one can use Locust to :doc:`load test non-HTTP systems `. To make existing locust scripts compatible with the new version you should make your locust classes inherit from HttpLocust instead of the base Locust class. msgpack for serializing master/slave data ----------------------------------------- Locust now uses `msgpack `_ for serializing data that is sent between a master node and its slaves. This addresses a possible attack that can be used to execute code remote, if one has access to the internal locust ports that are used for master-slave communication. The reason for this exploit was due to the fact that pickle was used. .. warning:: Anyone who uses an older version should make sure that their Locust machines are not publicly accessible on port 5557 and 5558. Also, one should never run Locust as root. Anyone who uses the :py:class:`report_to_master ` and :py:class:`slave_report ` events, needs to make sure that any data that is attached to the slave reports is serializable by msgpack. requests updated to version 2.2 ------------------------------- Locust updated `requests `_ to the latest major release. .. note:: Requests 1.0 introduced some major API changes (and 2.0 just a few). Please check if you are using any internal features and check the documentation: `Migrating to 1.x `_ and `Migrationg to 2.x `_ gevent updated to version 1.0 ------------------------------- gevent 1.0 has now been released and Locust has been updated accordingly. Big refactoring of request statistics code ------------------------------------------ Refactored :py:class:`RequestStats`. * Created :py:class:`StatsEntry` which represents a single stats entry (URL). Previously the :py:class:`RequestStats` was actually doing two different things: * It was holding track of the aggregated stats from all requests * It was holding the stats for single stats entries. Now RequestStats should be instantiated and holds the global stats, as well as a dict of StatsEntry instances which holds the stats for single stats entries (URLs) Removed support for avg_wait ---------------------------- Previously one could specify avg_wait to :py:class:`TaskSet` and :py:class:`Locust` that Locust would try to strive to. However this can be sufficiently accomplished by using min_wait and max_wait for most use-cases. Therefore we've decided to remove the avg_wait as its use-case is not clear or just too narrow to be in the Locust core. Removed support for ramping ---------------------------- Previously one could tell Locust, using the --ramp option, to try to find a stable client count that the target host could handle, but it's been broken and undocumented for quite a while so we've decided to remove it from the locust core and perhaps have it reappear as a plugin in the future. Locust Event hooks now takes keyword argument --------------------------------------------- When :doc:`extending-locust` by listening to :ref:`events`, the listener functions should now expect the arguments to be passed in as keyword arguments. It's also highly recommended to add an extra wildcard keyword arguments to listener functions, since they're then less likely to break if extra arguments are added to that event in some future version. For example:: from locust import events def on_request(request_type, name, response_time, response_length, **kw): print "Got request!" locust.events.request_success += on_request The *method* and *path* arguments to :py:obj:`request_success ` and :py:obj:`request_failure ` are now called *request_type* and *name*, since it's less HTTP specific. Other changes ------------- * You can now specify the port on which to run the web host * Various code cleanups * Updated gevent/zmq libraries * Switched to unittest2 discovery * Added option --only-summary to only output the summary to the console, thus disabling the periodic stats output. * Locust will now make sure to spawn all the specified locusts in distributed mode, not just a multiple of the number of slaves. * Fixed the broken Vagrant example. * Fixed the broken events example (events.py). * Fixed issue where the request column was not sortable in the web-ui. * Minor styling of the statistics table in the web-ui. * Added options to specify host and ports in distributed mode using --master-host, --master-port for the slaves, --master-bind-host, --master-bind-port for the master. * Removed previously deprecated and obsolete classes WebLocust and SubLocust. * Fixed so that also failed requests count, when specifying a maximum number of requests on the command line 0.6.2 ===== * Made Locust compatible with gevent 1.0rc2. This allows user to step around a problem with running Locust under some versions of CentOS, that can be fixed by upgrading gevent to 1.0. * Added :py:attr:`parent ` attribute to TaskSet class that refers to the parent TaskSet, or Locust, instance. Contributed by Aaron Daubman. 0.6.1 ===== * Fixed bug that was causing problems when setting a maximum number of requests using the **-n** or **--num-request** command line parameter. 0.6 === .. warning:: This version comes with non backward compatible changes to the API. Anyone who is currently using existing locust scripts and want to upgrade to 0.6 should read through these changes. :py:class:`SubLocust ` replaced by :py:class:`TaskSet ` and :py:class:`Locust ` class behavior changed ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- :py:class:`Locust ` classes does no longer control task scheduling and execution. Therefore, you no longer define tasks within Locust classes, instead the Locust class has a :py:attr:`task_set ` attribute which should point to a :py:class:`TaskSet ` class. Tasks should now be defined in TaskSet classes, in the same way that was previously done in Locust and SubLocust classes. TaskSets can be nested just like SubLocust classes could. So the following code for 0.5.1:: class User(Locust): min_wait = 10000 max_wait = 120000 @task(10) def index(self): self.client.get("/") @task(2) class AboutPage(SubLocust): min_wait = 10000 max_wait = 120000 def on_init(self): self.client.get("/about/") @task def team_page(self): self.client.get("/about/team/") @task def press_page(self): self.client.get("/about/press/") @task def stop(self): self.interrupt() Should now be written like:: class BrowsePage(TaskSet): @task(10) def index(self): self.client.get("/") @task(2) class AboutPage(TaskSet): def on_init(self): self.client.get("/about/") @task def team_page(self): self.client.get("/about/team/") @task def press_page(self): self.client.get("/about/press/") @task def stop(self): self.interrupt() class User(Locust): min_wait = 10000 max_wait = 120000 task_set = BrowsePage Each TaskSet instance gets a :py:attr:`locust ` attribute, which refers to the Locust class. Locust now uses Requests ------------------------ Locust's own HttpBrowser class (which was typically accessed through *self.client* from within a locust class) has been replaced by a thin wrapper around the requests library (http://python-requests.org). This comes with a number of advantages. Users can now take advantage of a well documented, well written, fully fledged library for making HTTP requests. However, it also comes with some small API changes which will require users to update their existing load testing scripts. Gzip encoding turned on by default ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The HTTP client now sends headers for accepting gzip encoding by default. The **--gzip** command line argument has been removed and if someone want to disable the *Accept-Encoding* that the HTTP client uses, or any other HTTP headers you can do:: class MyWebUser(Locust): def on_start(self): self.client.headers = {"Accept-Encoding":""} Improved HTTP client ^^^^^^^^^^^^^^^^^^^^ Because of the switch to using python-requests in the HTTP client, the API for the client has also gotten a few changes. * Additionally to the :py:meth:`get `, :py:meth:`post `, :py:meth:`put `, :py:meth:`delete ` and :py:meth:`head ` methods, the :py:class:`HttpSession ` class now also has :py:meth:`patch ` and :py:meth:`options ` methods. * All arguments to the HTTP request methods, except for **url** and **data** should now be specified as keyword arguments. For example, previously one could specify headers using:: client.get("/path", {"User-Agent":"locust"}) # this will no longer work And should now be specified like:: client.get("/path", headers={"User-Agent":"locust"}) * In general the whole HTTP client is now more powerful since it leverages on python-requests. Features that we're now able to use in Locust includes file upload, SSL, connection keep-alive, and more. See the `python-requests documentation `_ for more details. * The new :py:class:`HttpSession ` class' methods now return python-request :py:class:`Response ` objects. This means that accessing the content of the response is no longer made using the **data** attribute, but instead the **content** attribute. The HTTP response code is now accessed through the **status_code** attribute, instead of the **code** attribute. HttpSession methods' catch_response argument improved and allow_http_error argument removed ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * When doing HTTP requests using the **catch_response** argument, the context manager that is returned now provides two functions, :py:meth:`success ` and :py:meth:`failure ` that can be used to manually control what the request should be reported as in Locust's statistics. .. autoclass:: locust.clients.ResponseContextManager :members: success, failure :noindex: * The **allow_http_error** argument of the HTTP client's methods has been removed. Instead one can use the **catch_response** argument to get a context manager, which can be used together with a with statement. The following code in the previous Locust version:: client.get("/does/not/exist", allow_http_error=True) Can instead now be written like:: with client.get("/does/not/exist", catch_response=True) as response: response.success() Other improvements and bug fixes -------------------------------- * Scheduled task callables can now take keyword arguments and not only normal function arguments. * SubLocust classes that are scheduled using :func:`locust.core.Locust.schedule_task` can now take arguments and keyword arguments (available in *self.args* and *self.kwargs*). * Fixed bug where the average content size would be zero when doing requests against a server that didn't set the content-length header (i.e. server that uses *Transfer-Encoding: chunked*) Smaller API Changes ------------------- * The *require_once* decorator has been removed. It was an old legacy function that no longer fit into the current way of writing Locust tests, where tasks are either methods under a Locust class or SubLocust classes containing task methods. * Changed signature of :func:`locust.core.Locust.schedule_task`. Previously all extra arguments that was given to the method was passed on to the task when it was called. It no longer accepts extra arguments. Instead, it takes an *args* argument (list) and a *kwargs* argument (dict) which are be passed to the task when it's called. * Arguments for :py:class:`request_success ` event hook has been changed. Previously it took an HTTP Response instance as argument, but this has been changed to take the content-length of the response instead. This makes it easier to write custom clients for Locust. 0.5.1 ===== * Fixed bug which caused --logfile and --loglevel command line parameters to not be respected when running locust without zeromq. 0.5 === API changes ----------- * Web interface is now turned on by default. The **--web** command line option has been replaced by --no-web. * :func:`locust.events.request_success` and :func:`locust.events.request_failure` now gets the HTTP method as the first argument. Improvements and bug fixes -------------------------- * Removed **--show-task-ratio-confluence** and added a **--show-task-ratio-json** option instead. The **--show-task-ratio-json** will output JSON data containing the task execution ratio for the locust "brain". * The HTTP method used when a client requests a URL is now displayed in the web UI * Some fixes and improvements in the stats exporting: * A file name is now set (using content-disposition header) when downloading stats. * The order of the column headers for request stats was wrong. * Thanks Benjamin W. Smith, Jussi Kuosa and Samuele Pedroni! 0.4 === API changes ----------- * WebLocust class has been deprecated and is now called just Locust. The class that was previously called Locust is now called LocustBase. * The *catch_http_error* argument to HttpClient.get() and HttpClient.post() has been renamed to *allow_http_error*. Improvements and bug fixes -------------------------- * Locust now uses python's logging module for all logging * Added the ability to change the number of spawned users when a test is running, without having to restart the test. * Experimental support for automatically ramping up and down the number of locust to find a maximum number of concurrent users (based on some parameters like response times and acceptable failure rate). * Added support for failing requests based on the response data, even if the HTTP response was OK. * Improved master node performance in order to not get bottlenecked when using enough slaves (>100) * Minor improvements in web interface. * Fixed missing template dir in MANIFEST file causing locust installed with "setup.py install" not to work. ================================================ FILE: docs/conf.py ================================================ # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. from locust.argument_parser import get_parser import os import subprocess # Add fixes for RTD deprecation # https://about.readthedocs.com/blog/2024/07/addons-by-default/ # Define the canonical URL if you are using a custom domain on Read the Docs html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") # Tell Jinja2 templates the build is running on Read the Docs if os.environ.get("READTHEDOCS", "") == "True": if "html_context" not in globals(): html_context = {} html_context["READTHEDOCS"] = True # Run command `locust --help` and store output in cli-help-output.txt which is included in the docs def save_locust_help_output(): cli_help_output_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), "cli-help-output.txt") print(f"Running `locust --help` command and storing output in {cli_help_output_file}") help_output = subprocess.check_output(["locust", "--help"], text=True) with open(cli_help_output_file, "w") as f: f.write(help_output) save_locust_help_output() # Generate RST table with help/descriptions for all available environment variables def save_locust_env_variables(): env_options_output_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), "config-options.rst") print(f"Generating RST table for Locust environment variables and storing in {env_options_output_file}") parser = get_parser() table_data = [] for action in parser._actions: if action.env_var and action.help != "==SUPPRESS==": table_data.append( ( ", ".join([f"``{c}``" for c in action.option_strings]), f"``{action.env_var}``", ", ".join([f"``{c}``" for c in parser.get_possible_config_keys(action) if not c.startswith("--")]), action.help.replace("\n", " "), ) ) colsizes = [max(len(r[i]) for r in table_data) for i in range(len(table_data[0]))] formatter = " ".join("{:<%d}" % c for c in colsizes) rows = [formatter.format(*row) for row in table_data] edge = formatter.format(*["=" * c for c in colsizes]) divider = formatter.format(*["-" * c for c in colsizes]) headline = formatter.format(*["Command line", "Environment", "Config file", "Description"]) output = "\n".join( [ edge, headline, divider, "\n".join(rows), edge, ] ) with open(env_options_output_file, "w") as f: f.write(output) save_locust_env_variables() # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. from locust import __version__ # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx-prompt", "sphinx_substitution_extensions", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_search.extension", "sphinx_rtd_theme", "sphinxcontrib.googleanalytics", "sphinx.ext.extlinks", ] extlinks = { "gh": ("https://github.com/locustio/locust/blob/master/%s", "%s"), } # autoclass options # autoclass_content = "both" autodoc_typehints = "none" # I would have liked to use 'description' but unfortunately it too is very verbose # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = {".rst": "restructuredtext"} # The master toctree document. master_doc = "index" # General substitutions. project = "Locust" copyright = "2009-2025, Carl Byström, Jonatan Heyman, Lars Holmberg" # Intersphinx config intersphinx_mapping = { "requests": ("https://requests.readthedocs.io/en/latest/", None), "socketio": ("https://python-socketio.readthedocs.io/en/stable/", None), "dnspython": ("https://dnspython.readthedocs.io/en/stable/", None), } # The full version, including alpha/beta/rc tags. release = __version__ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = "%B %d, %Y" # List of documents that shouldn't be included in the build. # unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # Sphinx will recurse into subversion configuration folders and try to read # any document file within. These should be ignored. # Note: exclude_dirnames is new in Sphinx 0.5 exclude_dirnames = [] # Options for HTML output # ----------------------- html_show_sourcelink = False html_file_suffix = ".html" html_theme = "sphinx_rtd_theme" # Custom CSS overrides html_static_path = ["_static"] html_css_files = ["_static/theme-overrides.css", "_static/css/rtd_sphinx_search.min.css"] # Google Analytics ID googleanalytics_id = "G-MCG99XYF9M" googleanalytics_enabled = True # HTML theme # html_theme = "default" # html_theme_options = { # "rightsidebar": "true", # "codebgcolor": "#fafcfa", # "bodyfont": "Arial", # } # The name of the Pygments (syntax highlighting) style to use. # pygments_style = 'trac' rst_prolog = f""" .. |version| replace:: {__version__} """ ================================================ FILE: docs/configuration.rst ================================================ .. _configuration: ============= Configuration ============= Command Line Options ==================== Locust is configured mainly through command line arguments. .. code-block:: console $ locust --help .. literalinclude:: cli-help-output.txt :language: console .. _environment-variables: Environment Variables ===================== Options can also be set through environment variables. They are typically the same as the command line argument but capitalized and prefixed with ``LOCUST_``: On Linux/macOS: .. code-block:: $ LOCUST_LOCUSTFILE=custom_locustfile.py locust On Windows: .. code-block:: > set LOCUST_LOCUSTFILE=custom_locustfile.py > locust .. _configuration-file: Configuration File ================== Options can also be set in a configuration file in the `config or TOML file format `_. Locust will look for ``~/.locust.conf``, ``./locust.conf`` and ``./pyproject.toml`` by default. You can specify an additional file using the ``--config`` flag. .. code-block:: console $ locust --config custom_config.conf Here's an example: locust.conf -------------- .. code-block:: ini locustfile = locust_files/my_locust_file.py headless = true master = true expect-workers = 5 host = https://target-system users = 100 spawn-rate = 10 run-time = 10m tags = [Critical, Normal] Have a look later in this page for `All available configuration options`_ pyproject.toml -------------- When using a TOML file, configuration options should be defined within the ``[tool.locust]`` section. .. code-block:: toml [tool.locust] locustfile = "locust_files/my_locust_file.py" headless = true master = true expect-workers = 5 host = "https://target-system" users = 100 spawn-rate = 10 run-time = "10m" tags = ["Critical", "Normal"] .. note:: Configuration values are read (and overridden) in the following order: .. code-block:: console ./pyproject.toml -> ./locust.conf -> (file specified using --config) -> env vars -> cmd args All available configuration options =================================== Here's a table of all the available configuration options, and their corresponding Environment and config file keys: .. include:: config-options.rst Running without the web UI ========================== See :ref:`running-without-web-ui` Using multiple Locustfiles at once ================================== ``-f/--locustfile`` accepts multiple, comma-separated locustfiles. Example: With the following file structure: .. code-block:: ├── locustfiles/ │ ├── locustfile1.py │ ├── locustfile2.py │ └── more_files/ │ ├── locustfile3.py │ ├── _ignoreme.py .. code-block:: console $ locust -f locustfiles/locustfile1.py,locustfiles/locustfile2.py,locustfiles/more_files/locustfile3.py Locust will use ``locustfile1.py``, ``locustfile2.py`` & ``more_files/locustfile3.py`` Additionally, ``-f/--locustfile`` accepts directories as an option. Locust will recursively search specified directories for ``*.py`` files, ignoring files that start with "_". Example: .. code-block:: console $ locust -f locustfiles Locust will use ``locustfile1.py``, ``locustfile2.py`` & ``more_files/locustfile3.py`` You can also use ``-f/--locustfile`` for web urls. This will download the file and use it as any normal locustfile. Example: .. code-block:: console $ locust -f https://raw.githubusercontent.com/locustio/locust/master/examples/basic.py .. _class-picker: Pick User classes, Shapes and tasks from the UI =============================================== You can select which Shape class and which User classes to run in the WebUI when running locust with the ``--class-picker`` flag. No selection uses all the available User classes. For example, with a file structure like this: .. code-block:: ├── src/ │ ├── some_file.py ├── locustfiles/ │ ├── locustfile1.py │ ├── locustfile2.py │ └── more_files/ │ ├── locustfile3.py │ ├── _ignoreme.py │ └── shape_classes/ │ ├── DoubleWaveShape.py │ ├── StagesShape.py .. code-block:: console $ locust -f locustfiles --class-picker The Web UI will display: .. image:: images/userclass_picker_example.png The class picker additionally allows for disabling individual User tasks, changing the weight or fixed count, and configuring the host. It is even possible to add custom attributes that you wish to be configurable for each User. Simply add a ``json`` classmethod to your user: .. code-block:: python class Example(HttpUser): @task def example_task(self): self.client.get(f"/example/{self.some_custom_arg}") @classmethod def json(self): return { "host": self.host, "some_custom_arg": "example" } Configure Users from command line ================================= You can update User class attributes from the command line too, using the ``--config-users`` argument: .. code-block:: console $ locust --config-users '{"user_class_name": "Example", "fixed_count": 1, "some_custom_attribute": false}' To configure multiple users you pass multiple arguments to ``--config-users``, or use a JSON Array. You can also pass a path to a JSON file. .. code-block:: console $ locust --config-users '{"user_class_name": "Example", "fixed_count": 1}' '{"user_class_name": "ExampleTwo", "fixed_count": 2}' $ locust --config-users '[{"user_class_name": "Example", "fixed_count": 1}, {"user_class_name": "ExampleTwo", "fixed_count": 2}]' $ locust --config-users my_user_config.json When using this way to configure your users, you can set any attribute. .. note:: ``--config-users`` is a somewhat experimental feature and the json format may change even between minor Locust revisions. Configuring Profiles ==================== Profiles are an advanced feature that allow for grouping and filtering testruns. A profile may be set using the ``--profile`` argument or by inputting a value in the advanced options from the web ui. It may be useful to see a list of existing profiles as options in the form. If you have such a list, you may set it in your locustfile: .. code-block:: python from locust import events @events.init.add_listener def on_locust_init(environment, **kwargs): environment.web_ui.template_args["all_profiles"] = ["one", "two", "three"] Custom arguments ================ See :ref:`custom-arguments` Customization of statistics settings ==================================== Default configuration for Locust statistics is set in constants of stats.py file. It can be tuned to specific requirements by overriding these values. To do this, import locust.stats module and override required settings: .. code-block:: python import locust.stats locust.stats.CONSOLE_STATS_INTERVAL_SEC = 15 It can be done directly in Locust file or extracted to separate file for common usage by all Locust files. The list of statistics parameters that can be modified is: +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | Parameter name | Purpose | Default value | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | STATS_NAME_WIDTH | Width of column for request name in console output | terminal size or 80 | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | STATS_TYPE_WIDTH | Width of column for request type in console output | 8 | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | CSV_STATS_INTERVAL_SEC | Interval for how frequently the CSV file is written if this option is configured | 1 | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | CONSOLE_STATS_INTERVAL_SEC | Interval for how frequently results are written to console / chart UI | 2 | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | HISTORY_STATS_INTERVAL_SEC | Interval for how frequently results are written to history | 5 | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW | Window size/resolution - in seconds - when calculating the current response | 10 | | | time percentile | | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | PERCENTILES_TO_REPORT | List of response time percentiles to be calculated & reported | [0.50, 0.66, 0.75, 0.80, 0.90, 0.95, 0.98, 0.99, 0.999, 0.9999, 1.0] | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | PERCENTILES_TO_STATISTICS | List of response time percentiles in the screen of statistics for UI | [0.95, 0.99] | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ | PERCENTILES_TO_CHART | List of response time percentiles in the screen of chart for UI | [0.5, 0.95] | +-----------------------------------------+----------------------------------------------------------------------------------+----------------------------------------------------------------------+ .. _customizing-response-time-bucketing: Customizing response time bucketing ==================================== Response times are grouped into histogram buckets before being stored in the ``response_times`` dict. By default, Locust rounds to approximately 2 significant digits (e.g. 147 becomes 150, 3432 becomes 3400). This keeps the dict small, which matters in distributed mode where the dict is serialized from workers to master. You can replace the bucketing function to change this behaviour: .. code-block:: python import locust.stats from math import floor, log10 def my_bucket_function(response_time: int | float) -> int: """Example: bucket to 3 significant figures.""" if response_time == 0: return 0 return int(round(response_time, -int(floor(log10(abs(response_time)))) + 2)) locust.stats.bucket_response_time = my_bucket_function The replacement function receives a single numeric argument (the response time in milliseconds) and must return a numeric value to use as the dict key. Keep in mind that more unique keys means more data transferred in distributed mode. Customization of additional static variables ============================================ This table lists the constants that are set within locust and may be overridden. +-------------------------------------------+--------------------------------------------------------------------------------------+ | Parameter name | Purpose | +-------------------------------------------+--------------------------------------------------------------------------------------+ | locust.runners.WORKER_LOG_REPORT_INTERVAL | Interval for how frequently worker logs are reported to master. Can be disabled | | | by setting to a negative number | +-------------------------------------------+--------------------------------------------------------------------------------------+ | locust.web.HOST_IS_REQUIRED | Makes the host field for the webui required | +-------------------------------------------+--------------------------------------------------------------------------------------+ ================================================ FILE: docs/custom-load-shape.rst ================================================ .. _custom-load-shape: ================== Custom load shapes ================== Sometimes a completely custom shaped load test is required that cannot be achieved by simply setting or changing the user count and spawn rate. For example, you might want to generate a load spike or ramp up and down at custom times. By using a `LoadTestShape` class you have full control over the user count and spawn rate at all times. Define a class inheriting the LoadTestShape class in your locust file. If this type of class is found then it will be automatically used by Locust. In this class you define a `tick()` method that returns a tuple with the desired user count and spawn rate (or `None` to stop the test). Locust will call the `tick()` method approximately once per second. In the class you also have access to the `get_run_time()` method, for checking how long the test has run for. Example ------- This shape class will increase user count in blocks of 100 and then stop the load test after 10 minutes: .. code-block:: python class MyCustomShape(LoadTestShape): time_limit = 600 spawn_rate = 20 def tick(self): run_time = self.get_run_time() if run_time < self.time_limit: # User count rounded to nearest hundred. user_count = round(run_time, -2) return (user_count, self.spawn_rate) return None This functionality is further demonstrated in the `examples on github `_ including: - Generating a double wave shape - Time based stages like K6 - Step load pattern like Visual Studio One further method may be helpful for your custom load shapes: `get_current_user_count()`, which returns the total number of active users. This method can be used to prevent advancing to subsequent steps until the desired number of users has been reached. This is especially useful if the initialization process for each user is slow or erratic in how long it takes. If this sounds like your use case, see the `example on github `_. Note that if you want to defined your own custom base shape, you need to define the `abstract` attribute to `True` to avoid it being picked as Shape when imported: .. code-block:: python class MyBaseShape(LoadTestShape): abstract = True def tick(self): # Something reusable but needing inheritance return None Combining Users with different load profiles -------------------------------------------- If you use the Web UI, you can add the :ref:`---class-picker ` parameter to select which shape to use. But it often more flexible to have your User definitions in one file and your LoadTestShape in a separate one. For example, if you a high/low load Shape class defined in low_load.py and high_load.py respectively: .. code-block:: console $ locust -f locustfile.py,low_load.py $ locust -f locustfile.py,high_load.py Restricting which user types to spawn in each tick -------------------------------------------------- Adding the element ``user_classes`` to the return value gives you more detailed control: .. code-block:: python class StagesShapeWithCustomUsers(LoadTestShape): stages = [ {"duration": 10, "users": 10, "spawn_rate": 10, "user_classes": [UserA]}, {"duration": 30, "users": 50, "spawn_rate": 10, "user_classes": [UserA, UserB]}, {"duration": 60, "users": 100, "spawn_rate": 10, "user_classes": [UserB]}, {"duration": 120, "users": 100, "spawn_rate": 10, "user_classes": [UserA,UserB]}, ] def tick(self): run_time = self.get_run_time() for stage in self.stages: if run_time < stage["duration"]: try: tick_data = (stage["users"], stage["spawn_rate"], stage["user_classes"]) except: tick_data = (stage["users"], stage["spawn_rate"]) return tick_data return None This shape would create create in the first 10 seconds 10 User of ``UserA``. In the next twenty seconds 40 of type ``UserA / UserB`` and this continues until the stages end. .. _use-common-options: Reusing common options in custom shapes --------------------------------------- When using shapes, the the *Users*, *Spawn Rate* and *Run Time* options will be hidden from the UI, and if you specify them on command line Locust will log a warning. This is because those options dont directly apply to shapes, and specifying them might be a mistake. If you really want to combine a shape with these options, set the ``use_common_options`` attribute and access them from ``self.runner.environment.parsed_options``: .. code-block:: python class MyCustomShape(LoadTestShape): use_common_options = True def tick(self): run_time = self.get_run_time() if run_time < self.runner.environment.parsed_options.run_time: # User count rounded to nearest hundred, just like in previous example user_count = round(run_time, -2) return (user_count, self.runner.environment.parsed_options.spawn_rate) return None ================================================ FILE: docs/developing-locust.rst ================================================ .. _developing-locust: ================================= Developing and Documenting Locust ================================= You want to contribute to Locust? Great! Here is a list of `open bugs/feature requests `_. Install Locust for development ============================== Fork Locust on `GitHub `_ and then .. code-block:: sh # clone the repo: $ git clone git://github.com//locust.git # install the `uv` build system, https://docs.astral.sh/uv/getting-started/installation/ # [optional] create a virtual environment and activate it $ uv venv $ . .venv/bin/activate # perform an editable install of the "locust" package along with the dev and test packages: $ uv sync Now the ``uv --directory locust run locust`` command will run *your* code (with no need for reinstalling after making changes). If you have installed the project to a virtual environment, you can simply call `locust`. To contribute your changes, push to a branch in your repo and then `open a PR on github `_. If you install `pre-commit `_, linting and format checks/fixes will be automatically performed before each commit. Before you open a pull request, make sure all the tests work. And if you are adding a feature, make sure it is documented (in ``docs/*.rst``). If you're in a hurry or don't have access to a development environment, you can simply use `Codespaces `_, the github cloud development environment. On your fork page, just click on *Code* then on *Create codespace on *, and voila, your ready to code and test. Testing your changes ==================== We use `hatch `_ to automate tests across multiple Python versions. All tests: .. code-block:: console $ hatch test ... py39: commands[1]> pytest locust/test ... You can also run these tests against a specific Python version .. code-block:: console $ hatch test -py=3.10 ... py39: commands[1]> pytest locust/test ... To only run a specific suite or specific test you can call `pytest `_ directly. All tests: .. code-block:: console $ pytest locust/test Individual test: .. code-block:: console $ pytest locust/test/test_main.py::DistributedIntegrationTests::test_distributed_tags Debugging ========= See: :ref:`running-in-debugger`. Formatting and linting ====================== Locust uses `ruff `_ for formatting and linting. The build will fail if code does not adhere to it. If you run vscode it will automatically run every time you save a file, but if your editor doesn't support it you can run it manually: .. code-block:: console $ ruff --fix $ ruff format You can validate the whole project using hatch: .. code-block:: console $ hatch run lint:format ruff: commands[0]> ruff check . ruff: commands[1]> ruff format --check 104 files already formatted ruff: OK (1.41=setup[1.39]+cmd[0.01,0.01] seconds) congratulations :) (1.47 seconds) Build documentation =================== The documentation source is in the `docs/ `_ directory. To build the documentation you'll need to `Install Locust for development`_ then #. Install the documentation requirements: .. code-block:: console $ uv sync --all-groups #. Build the documentation locally: .. code-block:: console $ make build_docs View your generated documentation by opening ``docs/_build/index.html`` or running `make serve_docs` Making changes to Locust's Web UI ================================= The Web UI is built using React and Typescript Setup ----- Node ```` Install node using nvm to easily switch between node version - Copy and run the install line from `nvm `_ (starts with curl/wget ...) - Verify nvm was installed correctly .. code-block:: console $ nvm --version - Install the proper Node version according to engines in the ``locust/webui/package.json`` .. code-block:: console $ nvm install {version} $ nvm alias default {version} Yarn ```` - Install Yarn from their official website (avoid installing through Node if possible) - Verify yarn was installed correctly .. code-block:: console $ yarn --version - Next, install all dependencies .. code-block:: console $ cd locust/webui $ yarn Developing ---------- To develop while running a locust instance, run ``yarn watch``. This will output the static files to the ``dist`` directory. Vite will automatically detect any changed files and re-build as needed. Simply refresh the page to view the changes In certain situations (usually when styling), you may want to develop the frontend without running a locust instance. Running ``yarn dev`` will start the Vite dev server and allow for viewing your changes. To compile the webui, run ``yarn build`` The frontend can additionally be built using make: .. code-block:: console $ make frontend_build Linting ------- Run ``yarn lint`` to detect lint failures in the frontend project. Running ``yarn lint --fix`` will resolve any issues that are automatically resolvable. Your IDE can additionally be configured with ESLint to resolve these issues on save. Formatting ---------- Run ``yarn format`` to fix any formatting issues in the frontend project. Once again your IDE can be configured to automatically format on save. Typechecking ------------ We use Typescript in the frontend project. Run ``yarn type-check`` to find any issues. ================================================ FILE: docs/extending-locust.rst ================================================ .. _extending_locust: =========== Event hooks =========== Locust comes with a number of event hooks that can be used to extend Locust in different ways. For example, here's how to set up an event listener that will trigger after a request is completed:: from locust import events @events.request.add_listener def my_request_handler(request_type, name, response_time, response_length, response, context, exception, start_time, url, **kwargs): if exception: print(f"Request to {name} failed with exception {exception}") else: print(f"Successfully made a request to: {name}") print(f"The response was {response.text}") .. note:: In the above example the wildcard keyword argument (\**kwargs) will be empty, because we're handling all arguments, but it prevents the code from breaking if new arguments are added in some future version of Locust. Also, it is entirely possible to implement a client that does not supply all parameters for this event. For example, non-HTTP protocols might not even have the a concept of `url` or `response` object. Remove any such missing field from your listener function definition or use default arguments. When running locust in distributed mode, it may be useful to do some setup on worker nodes before running your tests. You can check to ensure you aren't running on the master node by checking the type of the node's :py:attr:`runner `:: from locust import events from locust.runners import MasterRunner @events.test_start.add_listener def on_test_start(environment, **kwargs): if not isinstance(environment.runner, MasterRunner): print("Beginning test setup") else: print("Started test from Master node") @events.test_stop.add_listener def on_test_stop(environment, **kwargs): if not isinstance(environment.runner, MasterRunner): print("Cleaning up test data") else: print("Stopped test from Master node") You can also use events `to add custom command line arguments `_. To see a full list of available events see :ref:`events`. .. _request_context: Request context =============== The :py:attr:`request event ` has a context parameter that enable you to pass data `about` the request (things like username, tags etc). It can be set directly in the call to the request method or at the User level, by overriding the User.context() method. Context from request method:: class MyUser(HttpUser): @task def t(self): self.client.post("/login", json={"username": "foo"}) self.client.get("/other_request", context={"username": "foo"}) @events.request.add_listener def on_request(context, **kwargs): if context: print(context["username"]) Context from User instance:: class MyUser(HttpUser): def context(self): return {"username": self.username} @task def t(self): self.username = "foo" self.client.post("/login", json={"username": self.username}) @events.request.add_listener def on_request(context, **kwargs): print(context["username"]) Context from a value in the response, using :ref:`catch_response `:: with self.client.get("/", catch_response=True) as resp: resp.request_meta["context"]["requestId"] = resp.json()["requestId"] .. note:: Request context doesn't change how Locust's regular statistics are calculated. Adding Web Routes ================== Locust uses Flask to serve the web UI and therefore it is easy to add web end-points to the web UI. By listening to the :py:attr:`init ` event, we can retrieve a reference to the Flask app instance and use that to set up a new route:: from locust import events @events.init.add_listener def on_locust_init(environment, **kw): @environment.web_ui.app.route("/added_page") def my_added_page(): return "Another page" You should now be able to start locust and browse to http://127.0.0.1:8089/added_page. Note that it doesn't get automatically added as a new tab - you'll need to enter the URL directly. Extending Web UI ================ As an alternative to adding simple web routes, you can use `Flask Blueprints `_ and `templates `_ to not only add routes but also extend the web UI to allow you to show custom data along side the built-in Locust stats. This is more advanced but can greatly enhance the utility and customizability of the web UI. Working examples of extending the web UI can be found in the `examples directory `_ of the Locust source code. * ``extend_modern_web_ui.py``: Display a table with content-length for each call. * ``web_ui_cache_stats.py``: Display Varnish Hit/Miss stats for each call. This could easily be extended to other CDN or cache proxies and gather other cache statistics such as cache age, control, ... .. image:: images/extend_modern_web_ui_cache_stats.png Adding Authentication to the Web UI =================================== Locust uses `Flask-Login `_ to handle authentication when the ``--web-login`` flag is present. The ``login_manager`` is exposed on ``environment.web_ui.app``, allowing the flexibility for you to implement any kind of auth that you would like! To use username / password authentication, simply provide a ``username_password_callback`` to the ``environment.web_ui.auth_args``. You are responsible for defining the route for the callback and implementing the authentication. Authentication providers can additionally be configured to allow authentication from 3rd parties such as GitHub or an SSO provider. Simply provide a list of desired ``auth_providers``. You may specify the ``label`` and ``icon`` for display on the button. The ``callback_url`` will be the url that the button directs to. You will be responsible for defining the callback route as well as the authentication with the 3rd party. Whether you are using username / password authentication, an auth provider, or both, a ``user_loader`` needs to be proivded to the ``login_manager``. The ``user_loader`` should return ``None`` to deny authentication or return a User object when authentication to the app should be granted. To display errors on the login page, such as an incorrect username / password combination, you may store the ``auth_error`` on the session object: ``session["auth_error"] = "Incorrect username or password"``. If you have non-erroneous information you would like to display to the user, you can opt instead to set ``auth_info`` on the session object: ``session["auth_info"] = "Successfully created new user!"`` A full example can be seen `in the auth example `_. In certain situations you may wish to further extend the fields present in the auth form. To achieve this, pass a ``custom_form`` dict to the ``environment.web_ui.auth_args``. In this case, the fields will be represented by a list of ``inputs``, the callback url is configured by the ``custom_form.callback_url``, and the submit button may optionally be configured using the ``custom_form.submit_button_text``. The fields in the auth form may be a text, select, checkbox, or secret password field. You may additionally override the HTML input type for specific field validation (e.g. type=email). For a full example see `configuring the custom_form in the auth example `_. Run a background greenlet ========================= Because a locust file is "just code", there is nothing preventing you from spawning your own greenlet to run in parallel with your actual load/Users. For example, you can monitor the fail ratio of your test and stop the run if it goes above some threshold: .. code-block:: python import gevent from locust import events from locust.runners import STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP, MasterRunner, LocalRunner def checker(environment): while not environment.runner.state in [STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP]: time.sleep(1) if environment.runner.stats.total.fail_ratio > 0.2: print(f"fail ratio was {environment.runner.stats.total.fail_ratio}, quitting") environment.runner.quit() return @events.init.add_listener def on_locust_init(environment, **_kwargs): # dont run this on workers, we only care about the aggregated numbers if isinstance(environment.runner, MasterRunner) or isinstance(environment.runner, LocalRunner): gevent.spawn(checker, environment) .. _parametrizing-locustfiles: Parametrizing locustfiles ========================= There are two main ways to parametrize your locustfile. Basic environment variables --------------------------- Like with any program, you can use environment variables: On linux/mac: .. code-block:: bash MY_FUNKY_VAR=42 locust ... On windows: .. code-block:: bash SET MY_FUNKY_VAR=42 locust ... ... and then access them in your locustfile. .. code-block:: python import os print(os.environ['MY_FUNKY_VAR']) .. _custom-arguments: Custom arguments ---------------- You can add your own command line arguments to Locust, using the :py:attr:`init_command_line_parser ` Event. Custom arguments are also presented and editable in the web UI. If `choices` are specified for the argument, they will be presented as a dropdown in the web UI. If `is_multiple` is set to True together with `choices`, the argument will be presented as a multi-select dropdown in the web UI. .. literalinclude:: ../examples/add_command_line_argument.py :language: python When running Locust :ref:`distributed `, custom arguments are automatically forwarded to workers when the run is started (but not before then, so you cannot rely on forwarded arguments *before* the test has actually started). Test data management ==================== There are a number of ways to get test data into your tests (after all, your test is just a Python program and it can do whatever Python can). Locust's events give you fine-grained control over *when* to fetch/release test data. You can find a `detailed example here `_. More examples ============= See `locust-plugins `_ ================================================ FILE: docs/extensions.rst ================================================ .. _extensions: ====================== Third party extensions ====================== Support for load testing other protocols, reporting etc ------------------------------------------------------- - `locust-plugins `__ - request logging & graphing - new protocols like selenium/webdriver, http users that load html page resources - readers (ways to get test data into your tests) - wait time (custom wait time functions) - checks (adds command line parameters to set locust exit code based on requests/s, error percentage and average response times) Automatically translate a browser recording (HAR-file) to a locustfile ---------------------------------------------------------------------- - `har2locust `__ Workers written in other languages than Python ---------------------------------------------- A Locust master and a Locust worker communicate by exchanging `msgpack `__ messages, which is supported by many languages. So, you can write your User tasks in any languages you like. For convenience, some libraries do the job as a worker runner. They run your User tasks, and report to master regularly. - `Boomer `__ - Go - `Locust4j `__ - Java ================================================ FILE: docs/faq.rst ================================================ .. _faq: === FAQ === How do I… Resolve errors that occur during load (error 5xx, Connection aborted, Connection reset by peer, etc) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Check your server logs. If it works at low load then it is almost certainly not a Locust issue, but an issue with the system you are testing. Clear cookies, to make the next task iteration seem like a new user to my system under test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Call ``self.client.cookies.clear()`` at the end of your task. Control headers or other things about my HTTP requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Locust’s client in ``HttpUser`` inherits from `requests `__ and the vast majority of parameters and methods for requests should just work with Locust. Check out the docs and Stack Overflow answers for requests first and then ask on Stack Overflow for additional Locust specific help if necessary. Basic auth (Authorization header) does not work after redirection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `requests `__ has a security mecanism that drops the authorization header on domain change. It could occure when testing a SSO, which is typically on a different domain and use mulitple redirections (30x). Since ``allow_redirects=True`` is the default ``request`` behavior you'll have to turn it off, handle manually the redirections and inject again the authorization header, ex: .. code-block:: python response = self.client.post( allow_redirects=False, url=...) while "location" in response.headers: response = self.client.get( allow_redirects=False, url=response.headers['location'], headers={ 'Authorization': 'XXX' } ) Create a Locust file based on a recorded browser session ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Try using `har2locust `__ How to run a Docker container of Locust in Windows Subsystem for Linux (Windows 10 users)? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you use WSL on a Windows computer, then you need one extra step than the `“docker run …” command `__: copy your locusttest1.py to a folder in a Windows path on your WSL and mount that folder instead of your normal WSL folder: :: $ mkdir /c/Users/[YOUR_Windows_USER]/Documents/Locust/ $ cp ~/path/to/locusttest1.py /c/Users/[YOUR_Windows_USER]/Documents/Locust/ $ docker run -p 8089:8089 -v /c/Users/[YOUR_Windows_USER]/Documents/Locust/:/mnt/locust locustio/locust:1.3.1 -f /mnt/locust/locusttest1.py How to run locust on custom endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Prefix the the endpoint to all ``@app.route`` definitions in ``locust/web.py`` file & also change as follows (where ``/locust`` is new endpoint) ``app = Flask(__name__, static_url_path='/locust')`` Change the entries of static content location in file ``locust/templates/index.html``. Eg: ```` Locust web UI doesn’t show my tasks running, says 0 RPS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Locust only knows what you’re doing when you tell it. There are `Event Hooks `__ that you use to tell Locust what’s going on in your code. If you use Locust’s ``HttpUser`` and then use ``self.client`` to make http calls, the proper events are normally fired for you automatically, making less work for you unless you want to override the default events. If you use the plain ``User`` or use ``HttpUser`` and you’re not using ``self.client`` to make http calls, Locust will not fire events for you. You will have to fire events yourself. See `the Locust docs `__ for examples. HTML Report is filled up with failed requests for long running tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ https://github.com/locustio/locust/issues/2328 Other questions and issues ~~~~~~~~~~~~~~~~~~~~~~~~~~ `Check the list of issues (a lot of questions/misunderstandings are filed as issues) `__ Add things that you ran into and solved here! Anyone with a GitHub account can contribute! If you have questions about Locust that are not answered here, please check `StackOverflow `__, or post your question there. This wiki is not for asking questions but for answering them :) ================================================ FILE: docs/further-reading.rst ================================================ =============================== Further reading / knowledgebase =============================== - :ref:`extensions`. - An list of Locust `articles, blog posts, reviews, and walkthroughs `_. - :ref:`Common questions and answers about Locust `. You'll also find a lot of answers on `stackoverflow `_, for example: - `BadStatusLine Errors `_ - `Passing HTTP Headers `_ - `POST file upload `_ - `How often does Locust perform dns queries? `_ In other words, just google whatever problem or error message you're getting and you are likely to find good answers out there. If you think Locust is missing some obvious feature (like stopping after a certain number of iterations/requests, setting goals/thresholds for when to fail a test run, support for additional User types like Kafka, Selenium/WebDriver etc), chances are it is implemented in `locust-plugins `_. ================================================ FILE: docs/history.rst ================================================ :orphan: .. _history: =============================== The history of Locust =============================== Locust was created because we were fed up with existing solutions. None of them are solving the right problem and to me, they are missing the point. We've tried both Apache JMeter and Tsung. Both tools are quite OK to use; we've used the former many times benchmarking stuff at work. JMeter comes with a UI, which you might think for a second is a good thing. But you soon realize it's a PITA to "code" your testing scenarios through some point-and-click interface. Secondly, JMeter is thread-bound. This means for every user you want to simulate, you need a separate thread. Needless to say, benchmarking thousands of users on a single machine just isn't feasible. Tsung, on the other hand, does not have these thread issues as it's written in Erlang. It can make use of the light-weight processes offered by BEAM itself and happily scale up. But when it comes to defining the test scenarios, Tsung is as limited as JMeter. It offers an XML-based DSL to define how a user should behave when testing. I guess you can imagine the horror of "coding" this. Displaying any sorts of graphs or reports when completed requires you to post-process the log files generated from the test. Only then can you get an understanding of how the test went. Anyway, we've tried to address these issues when creating Locust. Hopefully none of the above pain points should exist. I guess you could say we're really just trying to scratch our own itch here. We hope others will find it as useful as we do. - `Jonatan Heyman `_ (`@jonatanheyman `_ on Twitter) ================================================ FILE: docs/increase-performance.rst ================================================ .. _increase-performance: ============================================================== Increase performance with a more efficient HTTP client ============================================================== Locust's default HTTP client uses `python-requests `_. It provides a nice API that many python developers are familiar with, and is very well-maintained. But if you're planning to run tests with very high throughput and have limited hardware for running Locust, it is sometimes not efficient enough. Because of this, Locust also comes with :py:class:`FastHttpUser ` which uses `geventhttpclient `_ instead. It provides a very similar API and uses significantly less CPU time, sometimes increasing the maximum number of requests per second on a given hardware by as much as 5x-6x. It is impossible to say how many requests Locust can do on your particular hardware, using your particular test plan, so you'll need to test it out. Check Locust's console output, it will log a warning if it is limited by CPU. In a *best case* scenario (doing small requests inside a ``while True``-loop) a single Locust process (limited to one CPU core) can do around **16000 requests per second using FastHttpUser, and 4000 using HttpUser** (tested on a 2021 M1 MacBook Pro and Python 3.11) The relative improvement may be even bigger with bigger request payloads, but it may also be smaller if your test is doing CPU intensive things not related to requests. Of course, in reality, you should run :ref:`one locust process per CPU core `. .. note:: As long as your load generator CPU is not overloaded, FastHttpUser's response times should be almost identical to those of HttpUser. It does not make individual requests faster. How to use FastHttpUser =========================== Just subclass FastHttpUser instead of HttpUser:: from locust import task, FastHttpUser class MyUser(FastHttpUser): @task def index(self): response = self.client.get("/") Concurrency =========== A single FastHttpUser/geventhttpclient session can run concurrent requests, you just have to launch greenlets for each request:: @task def t(self): def concurrent_request(url): self.client.get(url) pool = gevent.pool.Pool() urls = ["/url1", "/url2", "/url3"] for url in urls: pool.spawn(concurrent_request, url) pool.join() .. note:: FastHttpUser/geventhttpclient is very similar to HttpUser/python-requests, but sometimes there are subtle differences. This is particularly true if you work with the client library's internals, e.g. when manually managing cookies. .. _rest: REST ==== FastHttpUser provides a ``rest`` method for testing REST/JSON HTTP interfaces. It is a wrapper for ``self.client.request`` that: * Parses the JSON response to a dict called ``js`` in the response object. Marks the request as failed if the response was not valid JSON. * Defaults ``Content-Type`` and ``Accept`` headers to ``application/json`` * Sets ``catch_response=True`` (so always use a :ref:`with-block `) * Catches any unhandled exceptions thrown inside your with-block, marking the sample as failed (instead of exiting the task immediately without even firing the request event) .. code-block:: python from locust import task, FastHttpUser class MyUser(FastHttpUser): @task def t(self): with self.rest("POST", "/", json={"foo": 1}) as resp: if resp.js is None: pass # no need to do anything, already marked as failed elif "bar" not in resp.js: resp.failure(f"'bar' missing from response {resp.text}") elif resp.js["bar"] != 42: resp.failure(f"'bar' had an unexpected value: {resp.js['bar']}") For a complete example, see `rest.py `_. That also shows how you can use inheritance to provide behaviors specific to your REST API that are common to multiple requests/testplans. .. note:: This feature is new and details of its interface/implementation may change in new versions of Locust. Connection Handling =================== By default, a User will reuse the same TCP/HTTP connection (unless it breaks somehow). To more realistically simulate new browsers connecting to your application this connection can be manually closed. .. code-block:: python @task def t(self): self.client.client.clientpool.close() # self.client.client is not a typo self.client.get("/") # Here a new connection will be created API === FastHttpUser class -------------------- .. autoclass:: locust.contrib.fasthttp.FastHttpUser :members: network_timeout, connection_timeout, max_redirects, max_retries, insecure, proxy_host, proxy_port, concurrency, client_pool, rest, rest_ FastHttpSession class --------------------- .. autoclass:: locust.contrib.fasthttp.FastHttpSession :members: request, get, post, delete, put, head, options, patch .. autoclass:: locust.contrib.fasthttp.FastResponse :members: content, text, json, headers ================================================ FILE: docs/increasing-request-rate.rst ================================================ .. _increaserr: =========================== Increasing the request rate =========================== If you're not getting the desired/expected throughput there are a number of things you can do. Concurrency ----------- Increase the number of Users. To fully utilize your target system you may need a lot of concurrent requests. Note that spawn rate/ramp up does not change peak load, it only changes how fast you get there. High `wait times `_ and sleeps *do* impact throughput, so that may make it necessary to launch even more Users. You can find a whole blog post on this topic `here `__. Load generation performance --------------------------- If Locust prints a warning about high CPU usage (``WARNING/root: CPU usage above 90%! ...``) try the following: - Run Locust `distributed `__ to utilize multiple cores & multiple machines - Try switching to `FastHttpUser `__ to reduce CPU usage - Check to see that there are no strange/infinite loops in your code Also, if you are using a custom client (not HttpUser or FastHttpUser), make sure any client library you are using is `gevent-friendly `__ otherwise it will block the entire Python process (essentially limiting you to one user per worker) If you're doing really high throughput or using a lot of bandwidth, you may also want to check out your network utilization and other OS level metrics. Actual issues with the system under test ---------------------------------------- If response times are high and/or increasing as the number of users go up, then you have probably saturated the system you are testing. This is not a Locust problem, but here are some things you may want to check: - resource utilization (e.g. CPU, memory & network) - configuration (e.g. max threads for your web server) - back end response times (e.g. DB) There are a few common pitfalls specific to load testing too: - load balancing (make sure locust isn't hitting only a few of your instances) - flood protection (sometimes a load test with the high amount of load from only a few machines will trigger this) ================================================ FILE: docs/index.rst ================================================ ===================== Locust Documentation ===================== Getting started --------------- .. toctree :: :maxdepth: 2 what-is-locust installation quickstart Writing Locust tests -------------------- .. toctree :: :maxdepth: 2 writing-a-locustfile Running your Locust tests ------------------------- .. toctree :: :maxdepth: 1 configuration increasing-request-rate running-distributed running-in-debugger running-in-docker running-without-web-ui Other functionalities --------------------- .. toctree :: :maxdepth: 2 custom-load-shape retrieving-stats testing-other-systems increase-performance extending-locust logging use-as-lib telemetry extensions kubernetes-operator vscode-extension Further reading / knowledgebase ------------------------------- .. toctree :: :maxdepth: 1 developing-locust further-reading faq API --- .. toctree :: :maxdepth: 2 api Changelog --------- .. toctree :: :maxdepth: 2 changelog ================================================ FILE: docs/installation.rst ================================================ .. _installation: Installation ============ .. note:: Check `Troubleshooting Installation`_ if you encounter issues. 0. `Install Python `_ (if you dont already have it) 1. Install Locust .. code-block:: console $ pip install locust 2. Validate your installation .. code-block:: console :substitutions: $ locust -V locust |version| from /usr/local/lib/python3.12/site-packages/locust (Python 3.12.5) Using uvx (alternative) ----------------------- 0. `Install uv `_ 1. Install and run locust in an ephemeral environment .. code-block:: console :substitutions: $ uvx locust -V locust |version| from /.../uv/.../locust (Python 3.12.5) Done! ----- Now you can :ref:`create and run your first test ` ----------------- Pre-release builds ------------------ If you need the latest and greatest version of Locust and cannot wait for the next release, you can install a dev build like this: .. code-block:: console $ pip3 install -U --pre locust Pre-release builds are published every time a branch/PR is merged into master. Install for development ----------------------- If you want to modify Locust, or contribute to the project, see :ref:`developing-locust`. Troubleshooting installation ---------------------------- .. contents:: Some solutions for common installation issues :depth: 1 :local: :backlinks: none psutil/\_psutil_common.c:9:10: fatal error: Python.h: No such file or directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `Answered in Stackoverflow thread 63440765 `_ ERROR: Failed building wheel for xxx ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While Locust itself is a pure Python package, it has some dependencies (e.g. gevent and geventhttpclient) that are compiled from C code. Pretty much all common platforms have binary packages on PyPi, but sometimes there is a new release that doesn't, or you are running on some exotic platform. You have two options: - (on macos) Install xcode: ``xcode-select --install`` - Use ``pip install --prefer-binary locust`` to select a pre-compiled version of packages even if there is a more recent version available as source. - Try googling the error message for the specific package that failed (not Locust), ensure you have the appropriate build tools installed etc. Windows ~~~~~~~ `Answered in Stackoverflow thread 61592069 `_ Installation works, but the ``locust`` command is not found ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When running pip, did you get a warning saying ``The script locust is installed in '...' which is not on PATH``? Add that directory to your PATH environment variable. Increasing Maximum Number of Open Files Limit ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Every User/HTTP connection from Locust opens a new file (technically a file descriptor). Many operating systems by default set a low limit for the maximum number of files that can be open at the same time. Locust will try to adjust this automatically for you, but in a lot of cases your operating system will not allow it (in which case you will get a warning in the log). Instead you will have to do it manually. How to do this depends on your operating system, but you might find some useful information here: https://www.tecmint.com/increase-set-open-file-limits-in-linux/ and practical examples https://www.ibm.com/support/knowledgecenter/SS8NLW_11.0.2/com.ibm.discovery.es.in.doc/iiysiulimits.html For systemd-based systems (e.g. Debian/Ubuntu) different limits are used for graphical login sessions. See https://unix.stackexchange.com/a/443467 for additional settings. ================================================ FILE: docs/kubernetes-operator.rst ================================================ .. _kubernetes-operator: Kubernetes Operator =================== The Locust Operator for Kubernetes is an operator that manages the lifecyle of :ref:`running-distributed` inside a Kubernetes cluster. It is a Custom Resource Definition (CRD) and a Controller that run on your Kubernetes cluster and allow you to create and manage your Locust tests as Kubernetes resources. Automatically creates master/worker jobs, mounts locustfiles, exposes the web UI, collects metrics, and handles restarts when the spec changes. Installation ------------ Helm Charts ~~~~~~~~~~~ `Helm `_ is a package manager for Kubernetes that installs and manages Kubernetes applications. 1. Add the Helm repository .. code-block:: bash $ helm repo add locust-operator https://locustio.github.io/k8s-operator "locust-operator" has been added to your repositories $ helm repo update Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "locust-operator" chart repository Update Complete. ⎈Happy Helming!⎈ 2. Install Locust Operator for Kubernetes .. code-block:: bash $ helm install locust-operator locust-operator/locust-operator \ --namespace locust-operator --create-namespace 3. Check that the operator is running and the CRD is installed .. code-block:: bash $ kubectl get pods -A -l app.kubernetes.io/name=locust-operator NAMESPACE NAME READY STATUS RESTARTS AGE locust-operator locust-operator-xxxxxxxxx-xxxxx 1/1 Running 0 18s $ kubectl get crd [none 🚀] NAME CREATED AT locusttests.locust.io ... Manifest Files ~~~~~~~~~~~~~~ Locust Operator for Kubernetes can be installed using raw manifest files with `kubectl `_. To generate the raw resources, you can use the Helm chart and output the manifests without installing them. .. code-block:: bash $ helm repo add locust-operator https://locustio.github.io/k8s-operator $ helm repo update $ helm template locust-operator locust-operator/locust-operator \ --namespace locust-operator > locust-operator.yaml Then apply the generated manifest file: .. code-block:: bash $ kubectl apply -f locust-operator.yaml Quickstart ---------- 1. Create a YAML file defining a ``LocustTest`` resource. .. code-block:: yaml apiVersion: locust.io/v1 kind: LocustTest metadata: name: load-test spec: workers: 2 locustfile: content: | from locust import HttpUser, task class TestUser(HttpUser): @task def index(self): self.client.get("/") 2. Apply it using ``kubectl apply -f .yaml``. 3. You can verify the created resource using ``kubectl get locusttests`` and ``kubectl get pods`` to see the master and worker pods. .. code-block:: bash $ kubectl apply -f locust-test.yaml locusttest.locust.io/load-test created $ kubectl get locusttests NAME STATE WORKERS FAIL_RATIO RPS USERS AGE load-test READY 2/2 0% 0 0 30s $ kubectl get pods -l locust.io/test-run=load-test NAME READY STATUS RESTARTS AGE load-test-master-xxxxx 1/1 Running 0 35s load-test-worker-xxxxx 1/1 Running 0 35s load-test-worker-xxxxx 1/1 Running 0 35s 4. Access the Locust web UI by port-forwarding the service to your local machine. Then open your browser and navigate to `http://localhost:8089`. .. code-block:: bash $ kubectl port-forward svc/load-test-webui 8089:8089 Forwarding from 127.0.0.1:8089 -> 8089 Forwarding from [::1]:8089 -> 8089 5. You can see the master logs by running: .. code-block:: bash # Tailing the master pod logs directly $ kubectl logs -f pod/load-test-master-xxxxx # Using a selector to follow the master pod by labels $ kubectl logs -f -l locust.io/test-run=load-test,locust.io/component=master 6. Cleanup by deleting the ``LocustTest`` resource (this will also delete all managed resources): .. code-block:: bash $ kubectl delete loadtest load-test LocustTest CRD Configuration ---------------------------- General ~~~~~~~ ``spec.image`` (string, required, default: ``locustio/locust:latest``) Container image for master and workers pods. ``spec.workers`` (integer, required, default: ``1``) Number of worker pods to run. ``spec.args`` (string, optional) Additional CLI flags, e.g. ``--run-time=5m --users=200 --spawn-rate=20``. ``spec.env`` (array, optional) List of environment variables to set in the container. Locustfile source ~~~~~~~~~~~~~~~~~ ``spec.locustfile`` (object, optional; choose **one**) * ``content`` (string): inline locustfile.py * ``configMap``: reference an existing ConfigMap * ``name`` (string, required) * ``key`` (string, default: ``locustfile.py``) * Built into image The image contains a locustfile. If the filename isn't ``locustfile.py`` (default locustfile name), pass ``-f `` via ``spec.args``. Metadata ~~~~~~~~ ``spec.labels`` / ``spec.annotations`` (object, optional) User-provided labels/annotations merged onto all managed resources. Per-role overrides ~~~~~~~~~~~~~~~~~~ This allows customizing **master** and **worker** pods separately. ``spec.master`` / ``spec.worker`` (object, optional) * ``labels`` / ``annotations`` (object, optional) * ``resources`` (object, optional). Example: .. code-block:: yaml master: labels: my.custom.label/is-locust-master: "true" resources: requests: cpu: "500m" memory: "256Mi" limits: cpu: "1" memory: "512Mi" Extended ~~~~~~~~ ``spec.imagePullPolicy`` (string, optional) The `image pull policy `_ for master/worker pods. ``spec.imagePullSecrets`` (array, optional) The `image pull secrets `_ for master/worker pods. e.g. ``[{ name: my-regcred }]`` for private registries. Examples -------- Inline locustfile ~~~~~~~~~~~~~~~~~ .. code-block:: yaml apiVersion: locust.io/v1 kind: LocustTest metadata: name: load-test-v1 spec: image: locustio/locust:latest workers: 5 args: --host http://my.site.com/api/v1 --run-time=10m --users=500 --spawn-rate=50 env: - name: LOCUST_LOGLEVEL value: INFO locustfile: content: | from locust import HttpUser, task class TestUser(HttpUser): @task def index(self): self.client.get("/") External ConfigMap locustfile ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml apiVersion: v1 kind: ConfigMap metadata: name: v2-locustfile data: mytest.py: | from locust import HttpUser, task class PingUser(HttpUser): @task def ping(self): self.client.get("/ping") --- apiVersion: locust.io/v1 kind: LocustTest metadata: name: load-test-v2 spec: workers: 5 args: -f mytest.py --host http://my.site.com/api/v2 --run-time=10m --users=500 --spawn-rate=50 locustfile: configMap: name: v2-locustfile Custom Master/Worker pod configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: yaml apiVersion: locust.io/v1 kind: LocustTest metadata: name: locust-test spec: labels: # Merged into all managed resources labels my.custom.label/group: group1 image: my-private-registry/custom-image:1.0.0 imagePullSecrets: - name: regcred workers: 5 master: annotations: # Merged into master pod annotations my.custom.annotations/version: "1.0.0" labels: # Merged into master pod labels my.custom.label/is-locust-master: "true" resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "1" memory: "1Gi" worker: annotations: # Merged into worker pod annotations my.custom.annotations/version: "1.0.0" labels: # Merged into worker pod labels my.custom.label/is-locust-master: "false" resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "1" memory: "1Gi" Headless run ~~~~~~~~~~~~ .. code-block:: yaml apiVersion: locust.io/v1 kind: LocustTest metadata: name: headless-test spec: workers: 2 args: --host http://locust.io/ --headless --run-time=5m --users=300 --spawn-rate=30 locustfile: content: | from locust import HttpUser, task class TestUser(HttpUser): @task def index(self): self.client.get("/") Upgrade ------- Helm ~~~~ .. code-block:: bash $ helm repo update $ helm upgrade locust-operator locust-operator/locust-operator \ --namespace locust-operator \ --reuse-values Helm `does not support updating or deleting CRDs `_. You may need to update the CRD manually when upgrading the operator. .. code-block:: bash $ kubectl apply -f https://raw.githubusercontent.com/locustio/k8s-operator/refs/tags/helm-chart-/charts/locust-operator/crds/locusttest.yaml Uninstall --------- 1. Delete all ``LocustTest`` resources (optional but recommended) .. code-block:: bash $ kubectl get locusttests --all-namespaces $ kubectl delete locusttests --all --all-namespaces Helm ~~~~ .. code-block:: bash # Uninstall the Helm release $ helm uninstall locust-operator --namespace locust-operator # Remove the LocustTest CRD $ kubectl delete crd locusttests.locust.io Manifest Files ~~~~~~~~~~~~~~ .. code-block:: bash $ kubectl delete -f locust-operator.yaml ================================================ FILE: docs/logging.rst ================================================ .. _logging: ======= Logging ======= Locust uses Python's `built in logging framework `_ for handling logging. The default logging configuration that Locust applies, writes log messages directly to stderr. ``--loglevel`` and ``--logfile`` can be used to change the verbosity and/or make the log go to a file instead. The default logging configuration installs handlers for the ``root`` logger as well as the ``locust.*`` loggers, so using the root logger in your own test scripts will put the message into the log file if ``--logfile`` is used: .. code-block:: python import logging logging.info("this log message will go wherever the other locust log messages go") It's also possible to control the whole logging configuration in your own test scripts by using the ``--skip-log-setup`` option. You will then have to `configure the logging `_ yourself. Options ======= ``--skip-log-setup`` -------------------- Disable Locust's logging setup. Instead, the configuration is provided by the Locust test or Python defaults. ``--loglevel`` -------------- Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO. The short-hand version is ``-L``. ``--logfile`` ------------- Path to log file. If not set, log will go to stdout/stderr. Locust loggers ============== Here's a table of the loggers used within Locust (for reference when configuring logging settings manually): +------------------------+--------------------------------------------------------------------------------------+ | Logger name | Purpose | +------------------------+--------------------------------------------------------------------------------------+ | locust | The locust namespace is used for all loggers such as ``locust.main``, | | | ``locust.runners``, etc. | +------------------------+--------------------------------------------------------------------------------------+ | locust.stats_logger | This logger is used to periodically print the current stats to the console. The | | | stats does *not* go into the log file when ``--logfile`` is used by default. | +------------------------+--------------------------------------------------------------------------------------+ ================================================ FILE: docs/quickstart.rst ================================================ .. _quickstart: =============== Your first test =============== A Locust test is essentially just a Python program making requests to the system you want to test. This makes it very flexible and particularly good at implementing complex user flows. But it can do simple tests as well, so let's start with that: .. code-block:: python from locust import HttpUser, task class HelloWorldUser(HttpUser): @task def hello_world(self): self.client.get("/hello") self.client.get("/world") This user will make an HTTP request to ``/hello``, then to ``/world``, and then repeat. For a full explanation and a more realistic example see :ref:`writing-a-locustfile`. Change ``/hello`` and ``/world`` to some actual paths on the website/service you want to test, put the code in a file named ``locustfile.py`` in your current directory and then run ``locust``: .. code-block:: console :substitutions: $ locust [2021-07-24 09:58:46,215] .../INFO/locust.main: Starting web interface at http://0.0.0.0:8089 [2021-07-24 09:58:46,285] .../INFO/locust.main: Starting Locust |version| Locust's web interface ====================== Open http://localhost:8089 .. image:: images/webui-splash-light.png | Provide the host name of your server and try it out! The following screenshots show what it might look like when running this test using 50 concurrent users, with a ramp up rate of 1 user/s .. image:: images/webui-running-statistics-light.png | Under the *Charts* tab you'll find things like requests per second (RPS), response times and number of running users: .. image:: images/bottlenecked-server-light.png .. note:: Interpreting performance test results is quite complex (and mostly out of scope for this manual), but if your graphs start looking like this, the target service/system cannot handle the load and you have found a bottleneck. When we get to around 20 users, response times start increasing so fast that even though Locust is still spawning more users, the number of requests per second is no longer increasing. The target service is "overloaded" or "saturated". If your response times are *not* increasing then add even more users until you find the service's breaking point, or celebrate that your service is already performant enough for your expected load. If you're having trouble generating enough load to saturate your system, take a look at :ref:`increaserr`. Direct command line usage / headless ==================================== Using the Locust web UI is entirely optional. You can supply the load parameters on the command line and get reports on the results in text form: .. code-block:: console :substitutions: $ locust --headless --users 10 --spawn-rate 1 -H http://your-server.com [2021-07-24 10:41:10,947] .../INFO/locust.main: No run time limit set, use CTRL+C to interrupt. [2021-07-24 10:41:10,947] .../INFO/locust.main: Starting Locust |version| [2021-07-24 10:41:10,949] .../INFO/locust.runners: Ramping to 10 users using a 1.00 spawn rate Name # reqs # fails | Avg Min Max Median | req/s failures/s ---------------------------------------------------------------------------------------------- GET /hello 1 0(0.00%) | 115 115 115 115 | 0.00 0.00 GET /world 1 0(0.00%) | 119 119 119 119 | 0.00 0.00 ---------------------------------------------------------------------------------------------- Aggregated 2 0(0.00%) | 117 115 119 117 | 0.00 0.00 (...) [2021-07-24 10:44:42,484] .../INFO/locust.runners: All users spawned: {"HelloWorldUser": 10} (10 total users) (...) See :ref:`running-without-web-ui` for more details. More options ============ To run Locust distributed across multiple Python processes or machines, you start a single Locust master process with the ``--master`` command line parameter, and then any number of Locust worker processes using the ``--worker`` command line parameter. See :ref:`running-distributed` for more info. To see all available options type: ``locust --help`` or check :ref:`configuration`. Next steps ========== Now, let's have a more in-depth look at locustfiles and what they can do: :ref:`writing-a-locustfile`. ================================================ FILE: docs/retrieving-stats.rst ================================================ ====================================== Retrieve test statistics in CSV format ====================================== You may wish to consume your Locust results via a CSV file. In this case, there are two ways to do this. First, when running Locust with the web UI, you can retrieve CSV files under the Download Data tab. Secondly, you can run Locust with a flag which will periodically save four CSV files. This is particularly useful if you plan on running Locust in an automated way with the ``--headless`` flag: .. code-block:: console $ locust -f examples/basic.py --csv example --headless -t10m The files will be named ``example_stats.csv``, ``example_failures.csv``, ``example_exceptions.csv`` and ``example_stats_history.csv`` (when using ``--csv example``). The first two files will contain the stats and failures for the whole test run, with a row for every stats entry (URL endpoint) and an aggregated row. The ``example_stats_history.csv`` will get new rows with the *current* (10 seconds sliding window) stats appended during the whole test run. By default only the Aggregate row is appended regularly to the history stats, but if Locust is started with the ``--csv-full-history`` flag, a row for each stats entry (and the Aggregate) is appended every time the stats are written (once every 2 seconds by default). You can also customize how frequently this is written: .. code-block:: python import locust.stats locust.stats.CSV_STATS_INTERVAL_SEC = 5 # default is 1 second ================================================ FILE: docs/running-distributed.rst ================================================ .. _running-distributed: =========================== Distributed load generation =========================== A single process running Locust can simulate a reasonably high throughput. For a simple test plan and small payloads it can make more than a thousand requests per second, possibly over ten thousand if you use :ref:`FastHttpUser `. But if your test plan is complex or you want to run even more load, you'll need to scale out to multiple processes, maybe even multiple machines. Fortunately, Locust supports distributed runs out of the box. To do this, you start one instance of Locust with the ``--master`` flag and one or more using the ``--worker`` flag. The master instance runs Locust's web interface, and tells the workers when to spawn/stop Users. The worker instances run your Users and send statistics back to the master. The master instance doesn't run any Users itself. To simplify startup, you can use the ``--processes`` flag. It will launch a master process and the specified number of worker processes. It can also be used in combination with ``--worker``, then it will only launch workers. This feature relies on `fork() `_ so it doesn't work on Windows. .. note:: Because Python cannot fully utilize more than one core per process (see `GIL `_), you need to run one worker instance per processor core in order to have access to all your computing power. .. note:: There is almost no limit to how many Users you can run per worker. Locust/gevent can run thousands or even tens of thousands of Users per process just fine, as long as their total request rate (RPS) is not too high. If Locust *is* getting close to running out of CPU resources, it will log a warning. If there is no warning but you are still unable to generate the expected load, then the problem must be :ref:`increaserr`. Single machine ============== It is really simple to launch a master and 4 worker processes:: locust --processes 4 You can even auto-detect the number of logical cores in your machine and launch one worker for each of them:: locust --processes -1 Multiple machines ================= Start locust in master mode on one machine:: locust -f my_locustfile.py --master And then on each worker machine: .. code-block:: bash locust -f - --worker --master-host --processes 4 .. note:: The ``-f -`` argument tells Locust to get the locustfile from master instead of from its local filesystem. This only works for single locustfiles. Multiple machines, using locust-swarm ===================================== .. note:: locust-swarm is no longer actively maintained. When you make changes to the locustfile you'll need to restart all Locust processes. `locust-swarm `_ automates this for you. It also solves the issue of firewall/network access from workers to master using SSH tunnels (this is often a problem if the master is running on your workstation and workers are running in some datacenter). .. code-block:: bash pip install locust-swarm swarm -f my_locustfile.py --loadgen-list worker-server1,worker-server2 Options for distributed load generation ======================================= ``--master-host `` ---------------------------------- Optionally used together with ``--worker`` to set the hostname/IP of the master node (defaults to localhost) ``--master-port `` ------------------------------- Optionally used together with ``--worker`` to set the port number of the master node (defaults to 5557). ``--master-bind-host `` --------------------------- Optionally used together with ``--master``. Determines which network interface the master node will bind to. Defaults to * (all available interfaces). ``--master-bind-port `` ------------------------------------ Optionally used together with ``--master``. Determines what network ports that the master node will listen to. Defaults to 5557. ``--expect-workers `` ---------------------------------------- Used when starting the master node with ``--headless``. The master node will then wait until X worker nodes has connected before the test is started. Communicating across nodes ============================================= When running Locust in distributed mode, you may want to communicate between master and worker nodes in order to coordinate the test. This can be easily accomplished with custom messages using the built in messaging hooks: .. code-block:: python from locust import events from locust.runners import MasterRunner, WorkerRunner # Fired when the worker receives a message of type 'test_users' def setup_test_users(environment, msg, **kwargs): for user in msg.data: print(f"User {user['name']} received") environment.runner.send_message('acknowledge_users', f"Thanks for the {len(msg.data)} users!") # Fired when the master receives a message of type 'acknowledge_users' def on_acknowledge(msg, **kwargs): print(msg.data) @events.init.add_listener def on_locust_init(environment, **_kwargs): if not isinstance(environment.runner, MasterRunner): environment.runner.register_message('test_users', setup_test_users) if not isinstance(environment.runner, WorkerRunner): environment.runner.register_message('acknowledge_users', on_acknowledge) @events.test_start.add_listener def on_test_start(environment, **_kwargs): if not isinstance(environment.runner, WorkerRunner): users = [ {"name": "User1"}, {"name": "User2"}, {"name": "User3"}, ] environment.runner.send_message('test_users', users) Note that when running locally (i.e. non-distributed), this functionality will be preserved; the messages will simply be handled by the runner that sends them. .. note:: Using the default options while registering a message handler will run the listener function in a **blocking** way, resulting in the heartbeat and other messages being delayed for the amount of the execution. If you think that your message handler will need to run for more than a second then you can register it as **concurrent**. Locust will then make it run in its own greenlet. Note that these greenlets will never be join():ed. .. code-block:: environment.runner.register_message('test_users', setup_test_users, concurrent=True) For more details, see the `complete example `_. Running distributed with Docker ============================================= See :ref:`running-in-docker` Running Locust distributed without the web UI ============================================= See :ref:`running-distributed-without-web-ui` Increase Locust's performance ============================= If you're planning to run large-scale load tests, you might be interested to use the alternative HTTP client that's shipped with Locust. You can read more about it here: :ref:`increase-performance`. ================================================ FILE: docs/running-in-debugger.rst ================================================ .. _running-in-debugger: =========================== Running tests in a debugger =========================== Running Locust in a debugger is extremely useful when developing your tests. Among other things, you can examine a particular response or check some User instance variable. But debuggers sometimes have issues with complex gevent-applications like Locust, and there is a lot going on in the framework itself that you probably aren't interested in. To simplify this, Locust provides a method called :py:func:`run_single_user `: .. literalinclude:: ../examples/debugging.py :language: python It implicitly registers an event handler for the :ref:`request ` event to print some stats about every request made: .. code-block:: console type name resp_ms exception GET /hello 38 ConnectionRefusedError(61, 'Connection refused') GET /hello 4 ConnectionRefusedError(61, 'Connection refused') You can configure exactly what is printed by specifying parameters to :py:func:`run_single_user `. Make sure you have enabled gevent in your debugger settings. Debugging Locust is quite easy with Vscode: - Place breakpoints - Select a python file or a scenario (ex: ```examples/basic.py``) - Check that the desired virtualenv is correctly detected (bottom right) - Open the action *Debug using launch.json*. You will have the choice between debugging the python file, the scenario with WebUI or in headless mode - It could be rerun with the F5 shortkey VS Code's ``launch.json`` looks like this: .. literalinclude:: ../.vscode/launch.json :language: json If you want to the whole Locust runtime (with ramp up, command line parsing etc), you can do that too: .. literalinclude:: ../.vscode/launch_locust.json :language: json There is a similar setting in `PyCharm `_. .. note:: | VS Code/pydev may give you warnings about: | ``sys.settrace() should not be used when the debugger is being used`` | It can safely be ignored (and if you know how to get rid of it, please let us know) You can execute run_single_user multiple times, as shown in `debugging_advanced.py `_. Print HTTP communication ======================== Sometimes it can be hard to understand why an HTTP request fails in Locust when it works from a regular browser/other application. Here's how to examine the communication in detail: For ``HttpUser`` (`python-requests `_): .. code-block:: python # put this at the top of your locustfile (or just before the request you want to trace) import logging from http.client import HTTPConnection HTTPConnection.debuglevel = 1 logging.basicConfig() logging.getLogger().setLevel(logging.DEBUG) requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True For ``FastHttpUser`` (`geventhttpclient `_): .. code-block:: python import sys ... class MyUser(FastHttpUser): @task def t(self): self.client.get("http://example.com/", debug_stream=sys.stderr) Example output (for FastHttpUser): .. code-block:: console REQUEST: http://example.com/ GET / HTTP/1.1 user-agent: python/gevent-http-client-1.5.3 accept-encoding: gzip, deflate, br, zstd host: example.com RESPONSE: HTTP/1.1 200 Content-Encoding: gzip Accept-Ranges: bytes Age: 460461 Cache-Control: max-age=604800 Content-Type: text/html; charset=UTF-8 Date: Fri, 12 Aug 2022 09:20:07 GMT Etag: "3147526947+ident" Expires: Fri, 19 Aug 2022 09:20:07 GMT Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT Server: ECS (nyb/1D20) Vary: Accept-Encoding X-Cache: HIT Content-Length: 648 ... These approaches can of course be used when doing a full load test, but you might get a lot of output :) ================================================ FILE: docs/running-in-docker.rst ================================================ .. _running-in-docker: ================= Running in Docker ================= The official Docker image is at `locustio/locust `_. Use it like this (assuming that the ``locustfile.py`` exists in the current working directory):: docker run -p 8089:8089 -v $PWD:/mnt/locust locustio/locust -f /mnt/locust/locustfile.py On Windows, this command will sometimes cause errors. Windows users should try using this instead:: docker run -p 8089:8089 --mount type=bind,source=$pwd,target=/mnt/locust locustio/locust -f /mnt/locust/locustfile.py Docker Compose ============== Here's an example Docker Compose file that could be used to start both a master node, and worker nodes: .. literalinclude:: ../examples/docker-compose/docker-compose.yml :language: yaml The above compose configuration could be used to start a master node and 4 workers using the following command:: docker-compose up --scale worker=4 Use docker image as a base image ================================ It's very common to have test scripts that rely on third party python packages. In those cases you can use the official Locust docker image as a base image:: FROM locustio/locust RUN pip install some-package # some dependency you need RUN pip install locust[otel] # for OpenTelemetry support ================================================ FILE: docs/running-without-web-ui.rst ================================================ .. _running-without-web-ui: ================================= Running without the web UI ================================= You can run locust without the web UI by using the ``--headless`` flag together with ``-u/--users`` and ``-r/--spawn-rate``: .. code-block:: console :substitutions: $ locust -f locust_files/my_locust_file.py --headless -u 100 -r 5 [2021-07-24 10:41:10,947] .../INFO/locust.main: No run time limit set, use CTRL+C to interrupt. [2021-07-24 10:41:10,947] .../INFO/locust.main: Starting Locust |version| [2021-07-24 10:41:10,949] .../INFO/locust.runners: Ramping to 100 users using a 5.00 spawn rate Name # reqs # fails | Avg Min Max Median | req/s failures/s ---------------------------------------------------------------------------------------------- GET /hello 1 0(0.00%) | 115 115 115 115 | 0.00 0.00 GET /world 1 0(0.00%) | 119 119 119 119 | 0.00 0.00 ---------------------------------------------------------------------------------------------- Aggregated 2 0(0.00%) | 117 115 119 117 | 0.00 0.00 (...) [2021-07-24 10:44:42,484] .../INFO/locust.runners: All users spawned: {"HelloWorldUser": 100} (100 total users) (...) Even in headless mode you can change the user count while the test is running. Press ``w`` to add 1 user or ``W`` to add 10. Press ``s`` to remove 1 or ``S`` to remove 10. Setting a time limit for the test --------------------------------- To specify the run time for a test, use ``-t/--run-time``: .. code-block:: console $ locust --headless -u 100 --run-time 1h30m $ locust --headless -u 100 --run-time 60 # default unit is seconds Locust will shut down once the time is up. Time is calculated from the start of the test (not from when ramp up has finished). Allow tasks to finish their iteration on shutdown ------------------------------------------------- By default, Locust will stop your tasks immediately (without even waiting for requests to finish). To give running tasks some time to finish their iteration, use ``-s/--stop-timeout``: .. code-block:: console $ locust --headless --run-time 1h30m --stop-timeout 10s .. _running-distributed-without-web-ui: Running Locust distributed without the web UI --------------------------------------------- If you want to :ref:`run Locust distributed ` without the web UI, you should specify the ``--expect-workers`` option when starting the master node, to specify the number of worker nodes that are expected to connect. It will then wait until that many worker nodes have connected before starting the test. Controlling the exit code of the Locust process ----------------------------------------------- By default the locust process will give an exit code of 1 if there were any failed samples (use the ``--exit-code-on-error`` to change that behaviour). You can also manually control the exit code in your test scripts by setting the :py:attr:`process_exit_code ` of the :py:class:`Environment ` instance. This is particularly useful when running Locust as an automated/scheduled test, for example as part of a CI pipeline. Below is an example that'll set the exit code to non zero if any of the following conditions are met: * More than 1% of the requests failed * The average response time is longer than 200 ms * The 95th percentile for response time is larger than 800 ms .. code-block:: python import logging from locust import events @events.quitting.add_listener def _(environment, **kw): if environment.stats.total.fail_ratio > 0.01: logging.error("Test failed due to failure ratio > 1%") environment.process_exit_code = 1 elif environment.stats.total.avg_response_time > 200: logging.error("Test failed due to average response time ratio > 200 ms") environment.process_exit_code = 1 elif environment.stats.total.get_response_time_percentile(0.95) > 800: logging.error("Test failed due to 95th percentile response time > 800 ms") environment.process_exit_code = 1 else: environment.process_exit_code = 0 Note that this code could go into the locustfile.py or in any other file that is imported in the locustfile. Running in CI/CD ---------------- You can easily run a single instance of Locust in headless mode as part of a CI/CD pipeline. Here's an example using GitHub Actions. Use it in combination with the above snippet to fail the run based on metrics. .. code-block:: yaml env: PYTHONUNBUFFERED: 1 # ensure we see output right away jobs: loadtest: runs-on: ubuntu-latest timeout-minutes: 15 # just in case something goes wrong steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.11' - run: pip install locust - run: locust --headless --run-time 5m ================================================ FILE: docs/tasksets.rst ================================================ :orphan: .. _tasksets: TaskSet class ============= If you are performance testing a website that is structured in a hierarchical way, with sections and sub-sections, it may be useful to structure your load test the same way. For this purpose, Locust provides the TaskSet class. It is a collection of tasks that will be executed much like the ones declared directly on a User class. .. note:: TaskSets are an advanced feature and only rarely useful. Most of the time, you're better off using regular Python loops and control statements to achieve the same thing. There are a few gotchas as well, the most frequent one being forgetting to call self.interrupt() .. code-block:: python from locust import User, TaskSet, constant class ForumSection(TaskSet): wait_time = constant(1) @task(10) def view_thread(self): pass @task def create_thread(self): pass @task def stop(self): self.interrupt() class LoggedInUser(User): wait_time = constant(5) tasks = {ForumSection:2} @task def my_task(self): pass A TaskSet can also be inlined directly under a User/TaskSet class using the @task decorator: .. code-block:: python class MyUser(User): @task class MyTaskSet(TaskSet): ... The tasks of a TaskSet class can be other TaskSet classes, allowing them to be nested any number of levels. This allows us to define a behaviour that simulates users in a more realistic way. For example we could define TaskSets with the following structure:: - Main user behaviour - Index page - Forum page - Read thread - Reply - New thread - View next page - Browse categories - Watch movie - Filter movies - About page When a running User thread picks a TaskSet class for execution an instance of this class will be created and execution will then go into this TaskSet. What happens then is that one of the TaskSet's tasks will be picked and executed, and then the thread will sleep for a duration specified by the User's wait_time function (unless a ``wait_time`` function has been declared directly on the TaskSet class, in which case it'll use that function instead), then pick a new task from the TaskSet's tasks, wait again, and so on. The TaskSet instance contains a reference to the User - ``self.user``. It also has a shortcut to its User's client attribute. So you can make a request using ``self.client.request()``, just like if your task was defined directly on an HttpUser. Interrupting a TaskSet ---------------------- One important thing to know about TaskSets is that they will never stop executing their tasks, and hand over execution back to their parent User/TaskSet, by themselves. This has to be done by the developer by calling the :py:meth:`TaskSet.interrupt() ` method. .. autofunction:: locust.TaskSet.interrupt :noindex: In the following example, if we didn't have the stop task that calls ``self.interrupt()``, the simulated user would never stop running tasks from the Forum taskset once it has went into it: .. code-block:: python class RegisteredUser(User): @task class Forum(TaskSet): @task(5) def view_thread(self): pass @task(1) def stop(self): self.interrupt() @task def frontpage(self): pass Using the interrupt function, we can - together with task weighting - define how likely it is that a simulated user leaves the forum. Differences between tasks in TaskSet and User classes ------------------------------------------------------- One difference for tasks residing under a TaskSet, compared to tasks residing directly under a User, is that the argument that they are passed when executed (``self`` for tasks declared as methods with the :py:func:`@task ` decorator) is a reference to the TaskSet instance, instead of the User instance. The User instance can be accessed from within a TaskSet instance through the :py:attr:`TaskSet.user `. TaskSets also contains a convenience :py:attr:`client ` attribute that refers to the client attribute on the User instance. Referencing the User instance, or the parent TaskSet instance --------------------------------------------------------------- A TaskSet instance will have the attribute :py:attr:`user ` point to its User instance, and the attribute :py:attr:`parent ` point to its parent TaskSet instance. Tags and TaskSets ------------------ You can tag TaskSets using the :py:func:`@tag ` decorator in a similar way to normal tasks, but there are some nuances worth mentioning. Tagging a TaskSet will automatically apply the tag(s) to all of the TaskSet's tasks. Furthermore, if you tag a task within a nested TaskSet, Locust will execute that task even if the TaskSet isn't tagged. .. _sequential-taskset: SequentialTaskSet class ======================= :py:class:`SequentialTaskSet ` is a TaskSet whose tasks will be executed in the order that they are declared. If the ** dictionary is used, each task will be repeated by the amount given by the integer at the point of declaration. It is possible to nest SequentialTaskSets within a TaskSet and vice versa. For example, the following code will request URLs /1-/4 in order, and then repeat. .. code-block:: python def function_task(taskset): taskset.client.get("/3") class SequenceOfTasks(SequentialTaskSet): @task def first_task(self): self.client.get("/1") self.client.get("/2") # you can still use the tasks attribute to specify a list or dict of tasks tasks = [function_task] # tasks = {function_task: 1} @task def last_task(self): self.client.get("/4") Note that you dont need SequentialTaskSets to just do some requests in order. It is often easier to just do a whole user flow in a single task. .. _markov-taskset: MarkovTaskSet class =================== :py:class:`MarkovTaskSet ` is a TaskSet that defines a probabilistic sequence of tasks using a Markov chain. Unlike regular TaskSets where tasks are chosen randomly based on weight, MarkovTaskSets allow you to define specific transitions between tasks with associated probabilities. This is useful for modeling user behavior where the next action depends on the current state, creating more realistic user flows. For example, after viewing a product, a user might be more likely to add it to cart than to log out. .. note:: MarkovTaskSets require at least one task with transitions defined. All tasks must eventually be reachable from the first task, and tags are not supported with MarkovTaskSets as they could make the Markov chain invalid. .. code-block:: python from locust import User, constant from locust.user.markov_taskset import MarkovTaskSet, transition, transitions class ShoppingBehavior(MarkovTaskSet): wait_time = constant(1) @transition("view_product") def browse_catalog(self): self.client.get("/catalog") @transitions({ "add_to_cart": 3, # Higher probability "browse_catalog": 1, "checkout": 1 }) def view_product(self): self.client.get("/product/1") @transitions(["view_product", "checkout"]) def add_to_cart(self): self.client.post("/cart/add", json={"product_id": 1}) @transition("browse_catalog") def checkout(self): self.client.post("/checkout") class ShopperUser(HttpUser): host = "http://localhost" tasks = {ShoppingBehavior: 1} In this example, after browsing the catalog, the user will always view a product. After viewing a product, the user has a 60% chance (3/5) of adding it to cart, a 20% chance (1/5) of returning to browsing, and a 20% chance (1/5) of going directly to checkout. Defining Transitions -------------------- MarkovTaskSet provides two decorators for defining transitions between tasks: 1. ``@transition(task_name, weight=1)``: Defines a single transition to another task. .. code-block:: python @transition("next_task") def current_task(self): pass 2. ``@transitions(weights)``: Defines multiple transitions at once. The ``weights`` parameter can be: - A dictionary mapping task names to weights: .. code-block:: python @transitions({ "task_a": 3, "task_b": 1 }) def current_task(self): pass - A list of task names or (task_name, weight) tuples: .. code-block:: python @transitions(["task_a", ("task_b", 2)]) def current_task(self): pass ================================================ FILE: docs/telemetry.rst ================================================ .. _telemetry: ========================= OpenTelemetry Integration ========================= Locust now optionally integrates with OpenTelemetry (OTel), enabling you to automatically export traces and metrics from your load tests to any OTel-compatible backend (OTLP, Prometheus, Jaeger, Tempo, etc.). This makes it easy to correlate load-test activity with application and infrastructure telemetry in your observability stack. The configuration is done via environment variables. See the `OpenTelemetry documentation `_ for details on how to configure exporters, resource attributes, sampling, etc. Setup ----- To enable OpenTelemetry, you need to download ``locust`` with the OpenTelemetry dependencies: .. code-block:: console $ pip install locust[otel] Then, pass the command line argument ``--otel`` to enable OpenTelemetry: .. code-block:: console $ locust --otel ... If you're using the official locust Docker image, you need to install it in a custom Dockerfile, see :ref:`running-in-docker`. Exporters --------- Locust supports the following OpenTelemetry exporters, for both traces and metrics, out of the box: - OTLP (gRPC and HTTP) - this is the default exporter using gRPC protocol - Console (useful for debugging) For traces, ``BatchSpanProcessor`` is used and can be configured with these `variables `_. For metrics, ``PeriodicExportingMetricReader`` is used and is configurable with the corresponding `variables `_. Auto Instrumentation -------------------- .. note:: Currently, only the ``requests`` library is auto-instrumented. This mean that only ``HttpUser`` will have it's HTTP requests made during your load tests automatically generate spans, and metrics with no additional configuration needed. We plan to add auto-instrumentation for more libraries in future releases. Supported Users ~~~~~~~~~~~~~~~ +--------------+-----------------------+ | User Class | Instrumented Library | +--------------+-----------------------+ | ``HttpUser`` | ``requests`` | +--------------+-----------------------+ If you need instrumentation for other libraries (e.g., database clients, messaging libraries), you can manually set up additional instrumentation using the OpenTelemetry Python SDK as per the `OpenTelemetry Python documentation `_. Example ------- .. code-block:: console $ export OTEL_TRACES_EXPORTER=otlp $ export OTEL_EXPORTER_OTLP_ENDPOINT=https://... $ export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf $ locust --otel [2025-11-28 16:27:01,916] locust/INFO/locust.main: Starting Locust, OpenTelemetry enabled [2025-11-28 16:27:01,916] locust/INFO/locust.main: Starting web interface at http://0.0.0.0:8089, press enter to open your default browser. ... ================================================ FILE: docs/testing-other-systems.rst ================================================ .. _testing-other-systems: =============================== Testing other systems/protocols =============================== Locust only comes with built-in support for HTTP/HTTPS but it can be extended to test almost any system. This is normally done by wrapping the protocol library and triggering a :py:attr:`request ` event after each call has completed, to let Locust know what happened. .. note:: It is important that the protocol libraries you use can be `monkey-patched `_ by gevent. Almost any libraries that are pure Python (using the Python ``socket`` module or some other standard library function like ``subprocess``) should work fine out of the box - but if they do their I/O calls from compiled code C, gevent will be unable to patch it. This will block the whole Locust/Python process (in practice limiting you to running a single User per worker process). Some C libraries allow for other workarounds. For example, if you want to use psycopg2 to performance test PostgreSQL, you can use `psycogreen `_. If you are willing to get your hands dirty, you may be able to patch a library yourself, but that is beyond the scope of this documentation. XML-RPC ======= Lets assume we have an XML-RPC server that we want to load test. .. literalinclude:: ../examples/custom_xmlrpc_client/server.py We can build a generic XML-RPC client, by wrapping :py:class:`xmlrpc.client.ServerProxy`. .. literalinclude:: ../examples/custom_xmlrpc_client/xmlrpc_locustfile.py gRPC ==== Lets assume we have a `gRPC `_ server that we want to load test: .. literalinclude:: ../examples/grpc/hello_server.py The generic GrpcUser base class sends events to Locust using an `interceptor `_: .. literalinclude:: ../examples/grpc/grpc_user.py And a locustfile using the above would look like this: .. literalinclude:: ../examples/grpc/locustfile.py .. _testing-request-sdks: requests-based libraries/SDKs ============================= If you want to use a library that uses a `requests.Session `_ object under the hood you will most likely be able to skip all the above complexity. Some libraries allow you to pass a Session explicitly, like for example the SOAP client provided by `Zeep `_. In that case, just pass it your ``HttpUser``'s :py:attr:`client `, and any requests made using the library will be logged in Locust. Even if your library doesn't expose that in its interface, you may be able to get it working by overwriting some internally used Session. Here's an example of how to do that for the `Archivist `_ client. .. literalinclude:: ../examples/sdk_session_patching/session_patch_locustfile.py REST ==== See :ref:`FastHttpUser ` SocketIO ======== See :ref:`SocketIOUser ` .. note:: SocketIO support is experimental and may change without notice. pytest ====== Locust allows you to use `pytest `_ syntax to define Locust Users using pytest fixtures (currently :py:class:`HttpSession ` and :py:class:`FastHttpSession `). It has multiple benefits: * Simpler syntax than regular Locustfiles * Run or debug easily from any editor that supports pytest * Reliably reuse functional test cases for load testing .. literalinclude:: ../examples/test_pytest.py Example usage: .. code-block:: sh $ locust -f test_pytest.py $ pytest test_pytest.py # if you have issues with gevent patching (RecursionError: maximum recursion depth exceeded) $ python -m gevent.monkey -m pytest test_pytest.py Limitations: * Each test case becomes a Locust User under the hood. We don't (yet) support weighting users. * Locust will only look for pytest-style tests if there are no regular User classes defined. * Adding other pytest fixtures or pytest plugins may cause issues (do let us know though) * For a more complex example, see ``_ .. note:: pytest support is experimental and may change without a new major release version. OpenAI ====== Performance/load testing AI services is a little different. While you could call the OpenAI API using HttpUser or FastHttpUser, it is often convenient to use `the SDK `_. .. literalinclude:: ../examples/openai_ex.py .. note:: OpenAIUser is experimental and may change without notice. MQTT ==== Locust uses to `paho-mqtt `_ to provide Mqtt connection capabilities. .. literalinclude:: ../examples/mqtt/locustfile.py Alternatively, if you need more control over the Mqtt client you can use a custom implementation. .. literalinclude:: ../examples/mqtt/locustfile_custom_mqtt_client.py .. note:: MqttUser is experimental and may change without notice. Other examples ============== See `locust-plugins `_ it has users for Kafka, Selenium/WebDriver, Playwright and more. ================================================ FILE: docs/use-as-lib.rst ================================================ .. _use-as-lib: ========================== Using Locust as a library ========================== It is possible to start a load test from your own Python code, instead of running Locust using the ``locust`` command. Start by creating an :py:class:`Environment ` instance: .. code-block:: python from locust.env import Environment env = Environment(user_classes=[MyTestUser]) The :py:class:`Environment ` instance's :py:meth:`create_local_runner `, :py:meth:`create_master_runner ` can then be used to start a :py:class:`Runner ` instance, which can be used to start a load test: .. code-block:: python env.create_local_runner() env.runner.start(5000, spawn_rate=20) env.runner.greenlet.join() It is also possible to bypass the dispatch and distribution logic, and manually control the spawned users: .. code-block:: python new_users = env.runner.spawn_users({MyUserClass.__name__: 2}) new_users[1].my_custom_token = "custom-token-2" new_users[0].my_custom_token = "custom-token-1" The above example only works on standalone/local runner mode and is an experimental feature. A more common/better approach would be to use ``init`` or ``test_start`` :ref:`events` to get/create a list of tokens and use :ref:`on-start-on-stop` to read from that list and set them on your individual User instances. .. note:: While it is possible to create locust workers this way (using :py:meth:`create_worker_runner `), that almost never makes sense. Every worker needs to be in a separate Python process and interacting directly with the worker runner might break things. Just launch workers using the regular ``locust --worker ...`` command. We could also use the :py:class:`Environment ` instance's :py:meth:`create_web_ui ` method to start a Web UI that can be used to view the stats, and to control the runner (e.g. start and stop load tests): .. code-block:: python env.create_local_runner() env.create_web_ui() env.web_ui.greenlet.join() Skipping monkey patching ======================== Some packages such as boto3 may have incompatibility when using Locust as a library, where monkey patching is already applied. In this case monkey patching may be disabled by setting ``LOCUST_SKIP_MONKEY_PATCH=1`` as env variable. Full example ============ .. literalinclude:: ../examples/use_as_lib.py :language: python ================================================ FILE: docs/vscode-extension.rst ================================================ .. _vscode-extension: VS Code Extension ================= Locust has an official extension for Visual Studio Code that helps you set up your Python environment, create and run tests and even use Copilot to generate/adjust Locust test code. Check it out at: * `VS Code Marketplace `__ * `Open VSX Registry `__ * `Git Repo `__ ================================================ FILE: docs/what-is-locust.rst ================================================ =============================== What is Locust? =============================== Locust is an open source performance/load testing tool for HTTP and other protocols. Its developer-friendly approach lets you define your tests in regular Python code. Locust tests can be run from command line or using its web-based UI. Throughput, response times and errors can be viewed in real time and/or exported for later analysis. You can import regular Python libraries into your tests, and with Locust's pluggable architecture it is infinitely expandable. Unlike when using most other tools, your test design will never be limited by a GUI or domain-specific language. To start using Locust, go to :ref:`installation` Features ======== .. raw:: html

* **Write test scenarios in plain old Python** If you want your users to loop, perform some conditional behavior or do some calculations, you just use the regular programming constructs provided by Python. Locust runs every user inside its own greenlet (a lightweight process/coroutine). This enables you to write your tests like normal (blocking) Python code instead of having to use callbacks or some other mechanism. Because your scenarios are "just python" you can use your regular IDE, and version control your tests as regular code (as opposed to some other tools that use XML or binary formats) * **Distributed and scalable - supports hundreds of thousands of concurrent users** Locust makes it easy to run load tests distributed over multiple machines. It is event-based (using `gevent `_), which makes it possible for a single process to handle many thousands concurrent users. While there may be other tools that are capable of doing more requests per second on a given hardware, the low overhead of each Locust user makes it very suitable for testing highly concurrent workloads. * **Web-based UI** Locust has a user friendly web interface that shows the progress of your test in real-time. You can even change the load while the test is running. It can also be run without the UI, making it easy to use for CI/CD testing. * **Can test any system** Even though Locust primarily works with websites/services, it can be used to test almost any system or protocol. Just :ref:`write a client ` for what you want to test, or `explore some created by the community `_. * **Hackable** Locust is small and very flexible and we intend to keep it that way. If you want to `send reporting data to that database & graphing system you like `_, wrap calls to a REST API to handle the particulars of your system or run a :ref:`totally custom load pattern `, there is nothing stopping you! Name & background ================= Locust was born out of a frustration with existing solutions. No existing load testing tool was well-equipped to generate realistic load against a dynamic website where most pages had different content for different users. Existing tools used clunky interfaces or verbose configuration files to declare the tests. In Locust we took a different approach. Instead of configuration formats or UIs you'd get a python framework that would let you define the behavior of your users using Python code. Locust takes its name from the `grasshopper species `_, known for their swarming behavior. :ref:`history` Authors ======= - Jonatan Heyman (`@heyman `_) - Lars Holmberg (`@cyberw `_) - Andrew Baldwin (`@andrewbaldwin44 `_) Many thanks to our other great `contributors `_! License ======= Open source licensed under the MIT license (see LICENSE file for details). ================================================ FILE: docs/writing-a-locustfile.rst ================================================ .. _writing-a-locustfile: ====================== Writing a locustfile ====================== Now, lets look at a more complete/realistic example of what your tests might look like: .. code-block:: python import time from locust import HttpUser, task, between class QuickstartUser(HttpUser): wait_time = between(1, 5) @task def hello_world(self): self.client.get("/hello") self.client.get("/world") @task(3) def view_items(self): for item_id in range(10): self.client.get(f"/item?id={item_id}", name="/item") time.sleep(1) def on_start(self): self.client.post("/login", json={"username":"foo", "password":"bar"}) .. rubric:: Let's break it down .. code-block:: python import time from locust import HttpUser, task, between A locust file is just a normal Python module, it can import code from other files or packages. .. code-block:: python class QuickstartUser(HttpUser): Here we define a class for the users that we will be simulating. It inherits from :py:class:`HttpUser ` which gives each user a ``client`` attribute, which is an instance of :py:class:`HttpSession `, that can be used to make HTTP requests to the target system that we want to load test. When a test starts, locust will create an instance of this class for every user that it simulates, and each of these users will start running within their own green gevent thread. For a file to be a valid locustfile it must contain at least one class inheriting from :py:class:`User `. .. code-block:: python wait_time = between(1, 5) Our class defines a ``wait_time`` that will make the simulated users wait between 1 and 5 seconds after each task (see below) is executed. For more info see :ref:`wait-time`. .. code-block:: python @task def hello_world(self): self.client.get("/hello") self.client.get("/world") Methods decorated with ``@task`` are the core of your locust file. For every running User, Locust creates a `greenlet `_ (a coroutine or "micro-thread"), that will call those methods. Code within a task is executed sequentially (it is just regular Python code), so ``/world`` won't be called until the response from ``/hello`` has been received. .. code-block:: python @task def hello_world(self): ... @task(3) def view_items(self): ... We've declared two tasks by decorating two methods with ``@task``, one of which has been given a higher weight (3). When our ``QuickstartUser`` runs it'll pick one of the declared tasks - in this case either ``hello_world`` or ``view_items`` - and execute it. Tasks are picked at random, but you can give them different weighting. The above configuration will make Locust three times more likely to pick ``view_items`` than ``hello_world``. When a task has finished executing, the User will then sleep for its specified wait time (in this case between 1 and 5 seconds). Then it will pick a new task. Note that only methods decorated with ``@task`` will be picked, so you can define your own internal helper methods any way you like. .. code-block:: python self.client.get("/hello") The ``self.client`` attribute makes it possible to make HTTP calls that will be logged by Locust. For information on how to make other kinds of requests, validate the response, etc, see `Using the HTTP Client `_. .. note:: HttpUser is not a real browser, and thus will not parse an HTML response to load resources or render the page. It will keep track of cookies though. .. code-block:: python @task(3) def view_items(self): for item_id in range(10): self.client.get(f"/item?id={item_id}", name="/item") time.sleep(1) In the ``view_items`` task we load 10 different URLs by using a variable query parameter. In order to not get 10 separate entries in Locust's statistics - since the stats is grouped on the URL - we use the :ref:`name parameter ` to group all those requests under an entry named ``"/item"`` instead. .. code-block:: python def on_start(self): self.client.post("/login", json={"username":"foo", "password":"bar"}) Additionally we've declared an `on_start` method. A method with this name will be called for each simulated user when they start. For more info see :ref:`on-start-on-stop`. Auto-generating a locustfile ============================ You can use `har2locust `_ to generate locustfiles based on a browser recording (HAR-file). It is particularly useful for beginners that are not used to writing their own locustfile, but also highly customizable for more advanced use cases. .. note:: har2locust is still in beta. It may not always generate correct locustfiles, and its interface may change between versions. User class ========== A user class represents one type of user/scenario for your system. When you do a test run you specify the number of concurrent users you want to simulate and Locust will create an instance per user. You can add any attributes you like to these classes/instances, but there are some that have special meaning to Locust: .. _wait-time: wait_time attribute ------------------- A User's :py:attr:`wait_time ` method makes it easy to introduce delays after each task execution. If no `wait_time` is specified, the next task will be executed as soon as one finishes. * :py:attr:`constant ` for a fixed amount of time * :py:attr:`between ` for a random time between a min and max value For example, to make each user wait between 0.5 and 10 seconds between every task execution: .. code-block:: python from locust import User, task, between class MyUser(User): @task def my_task(self): print("executing my_task") wait_time = between(0.5, 10) * :py:attr:`constant_throughput ` for an adaptive time that ensures the task runs (at most) X times per second. * :py:attr:`constant_pacing ` for an adaptive time that ensures the task runs (at most) once every X seconds (it is the mathematical inverse of `constant_throughput`). .. note:: For example, if you want Locust to run 500 task iterations per second at peak load, you could use `wait_time = constant_throughput(0.1)` and a user count of 5000. Wait time can only constrain the throughput, not launch new Users to reach the target. So, in our example, the throughput will be less than 500 if the time for the task iteration exceeds 10 seconds. Wait time is applied *after* task execution, so if you have a high spawn rate/ramp up you may end up exceeding your target during ramp-up. Wait times apply to *tasks*, not requests. For example, if you specify `wait_time = constant_throughput(2)` and do two requests in your tasks, your request rate/RPS will be 4 per User. It's also possible to declare your own wait_time method directly on your class. For example, the following User class would sleep for one second, then two, then three, etc. .. code-block:: python class MyUser(User): last_wait_time = 0 def wait_time(self): self.last_wait_time += 1 return self.last_wait_time ... weight and fixed_count attributes --------------------------------- If more than one user class exists in the file, and no user classes are specified on the command line, Locust will spawn an equal number of each of the user classes. You can also specify which of the user classes to use from the same locustfile by passing them as command line arguments: .. code-block:: console $ locust -f locust_file.py WebUser MobileUser If you wish to simulate more users of a certain type than another you can set a weight attribute on those classes. The code below will make Locust spawn 3 times as many WebUsers as MobileUsers: .. code-block:: python class WebUser(User): weight = 3 ... class MobileUser(User): weight = 1 ... Also, you can set the :py:attr:`fixed_count ` attribute. In this case, the weight attribute will be ignored and only that exact number users will be spawned. These users are spawned before any regular, weighted ones. In the example below, only one instance of AdminUser will be spawned, to make some specific work with more accurate control of request count independently of total user count. .. code-block:: python class AdminUser(User): wait_time = constant(600) fixed_count = 1 @task def restart_app(self): ... class WebUser(User): ... host attribute -------------- The host attribute is a URL prefix (e.g. ``https://google.com``) to the host you want to test. It is automatically added to requests, so you can do ``self.client.get("/")`` for example. You can overwrite this value in Locust's web UI or on the command line, using the :code:`--host` option. tasks attribute --------------- A User class can have tasks declared as methods under it using the :py:func:`@task ` decorator, but one can also specify tasks using the *tasks* attribute, which is described in more details :ref:`below `. environment attribute --------------------- A reference to the :py:attr:`environment ` in which the user is running. Use this to interact with the environment, or the :py:attr:`runner ` which it contains. E.g. to stop the runner from a task method: .. code-block:: python self.environment.runner.quit() If run on a standalone locust instance, this will stop the entire run. If run on worker node, it will stop that particular node. .. _on-start-on-stop: on_start and on_stop methods ---------------------------- Users (and :ref:`TaskSets `) can declare an :py:meth:`on_start ` method and/or :py:meth:`on_stop ` method. A User will call its :py:meth:`on_start ` method when it starts running, and its :py:meth:`on_stop ` method when it stops running. For a TaskSet, the :py:meth:`on_start ` method is called when a simulated user starts executing that TaskSet, and :py:meth:`on_stop ` is called when the simulated user stops executing that TaskSet (when :py:meth:`interrupt() ` is called, or the user is killed). Tasks ===== When a load test is started, an instance of a User class will be created for each simulated user and they will start running within their own greenlet. When these users run they pick tasks that they execute, sleep for awhile, and then pick a new task and so on. @task decorator --------------- The easiest way to add a task for a User is by using the :py:meth:`task ` decorator. .. code-block:: python from locust import User, task, constant class MyUser(User): wait_time = constant(1) @task def my_task(self): print("User instance (%r) executing my_task" % self) **@task** takes an optional weight argument that can be used to specify the task's execution ratio. In the following example, *task2* will be twice as likely to be selected as *task1*: .. code-block:: python from locust import User, task, between class MyUser(User): wait_time = between(5, 15) @task(3) def task1(self): pass @task(6) def task2(self): pass .. _tasks-attribute: tasks attribute --------------- Another way to define the tasks of a User is by setting the :py:attr:`tasks ` attribute. The *tasks* attribute is either a list of Tasks, or a ** dict, where Task is either a python callable or a :ref:`TaskSet ` class. If the task is a normal python function they receive a single argument which is the User instance that is executing the task. Here is an example of a User task declared as a normal python function: .. code-block:: python from locust import User, constant def my_task(user): pass class MyUser(User): tasks = [my_task] wait_time = constant(1) If the tasks attribute is specified as a list, each time a task is to be performed, it will be randomly chosen from the *tasks* attribute. If however, *tasks* is a dict - with callables as keys and ints as values - the task that is to be executed will be chosen at random but with the int as ratio. So with a task that looks like this:: {my_task: 3, another_task: 1} *my_task* would be 3 times as likely to be executed as *another_task*. Internally the above dict will actually be expanded into a list (and the ``tasks`` attribute is updated) that looks like this:: [my_task, my_task, my_task, another_task] and then Python's ``random.choice()`` is used to pick tasks from the list. .. _tagging-tasks: @tag decorator -------------- By tagging tasks using the :py:func:`@tag ` decorator, you can be picky about what tasks are executed during the test using the :code:`--tags` and :code:`--exclude-tags` arguments. Consider the following example: .. code-block:: python from locust import User, constant, task, tag class MyUser(User): wait_time = constant(1) @tag('tag1') @task def task1(self): pass @tag('tag1', 'tag2') @task def task2(self): pass @tag('tag3') @task def task3(self): pass @task def task4(self): pass If you started this test with :code:`--tags tag1`, only *task1* and *task2* would be executed during the test. If you started it with :code:`--tags tag2 tag3`, only *task2* and *task3* would be executed. :code:`--exclude-tags` will behave in the exact opposite way. So, if you start the test with :code:`--exclude-tags tag3`, only *task1*, *task2*, and *task4* will be executed. Exclusion always wins over inclusion, so if a task has a tag you've included and a tag you've excluded, it will not be executed. Events ====== If you want to run some setup code as part of your test, it is often enough to put it at the module level of your locustfile, but sometimes you need to do things at particular times in the run. For this need, Locust provides event hooks. test_start and test_stop ------------------------ If you need to run some code at the start or stop of a load test, you should use the :py:attr:`test_start ` and :py:attr:`test_stop ` events. You can set up listeners for these events at the module level of your locustfile: .. code-block:: python from locust import events @events.test_start.add_listener def on_test_start(environment, **kwargs): print("A new test is starting") @events.test_stop.add_listener def on_test_stop(environment, **kwargs): print("A new test is ending") init ---- The ``init`` event is triggered at the beginning of each Locust process. This is especially useful in distributed mode where each worker process (not each user) needs a chance to do some initialization. For example, let's say you have some global state that all users spawned from this process will need: .. code-block:: python from locust import events from locust.runners import MasterRunner @events.init.add_listener def on_locust_init(environment, **kwargs): if isinstance(environment.runner, MasterRunner): print("I'm on master node") else: print("I'm on a worker or standalone node") Other events ------------ See :ref:`extending locust using event hooks ` for other events and more examples of how to use them. HttpUser class ============== :py:class:`HttpUser ` is the most commonly used :py:class:`User `. It adds a :py:attr:`client ` attribute which is used to make HTTP requests. .. code-block:: python from locust import HttpUser, task, between class MyUser(HttpUser): wait_time = between(5, 15) @task(4) def index(self): self.client.get("/") @task(1) def about(self): self.client.get("/about/") client attribute / HttpSession ------------------------------ :py:attr:`client ` is an instance of :py:class:`HttpSession `. HttpSession is a subclass/wrapper for :py:class:`requests.Session`, so its features are well documented and should be familiar to many. What HttpSession adds is mainly reporting of the request results into Locust (success/fail, response time, response length, name). It contains methods for all HTTP methods: :py:meth:`get `, :py:meth:`post `, :py:meth:`put `, ... Just like :py:class:`requests.Session`, it preserves cookies between requests so it can easily be used to log in to websites. .. code-block:: python :caption: Make a POST request, look at the response and implicitly reuse any session cookie we got for a second request response = self.client.post("/login", {"username":"testuser", "password":"secret"}) print("Response status code:", response.status_code) print("Response text:", response.text) response = self.client.get("/my-profile") HttpSession catches any :py:class:`requests.RequestException` thrown by Session (caused by connection errors, timeouts or similar), instead returning a dummy Response object with *status_code* set to 0 and *content* set to None. .. _catch-response: Validating responses -------------------- Requests are considered successful if the HTTP response code is OK (<400), but it is often useful to do some additional validation of the response. You can mark a request as failed by using the *catch_response* argument, a *with*-statement and a call to *response.failure()* .. code-block:: python with self.client.get("/", catch_response=True) as response: if response.text != "Success": response.failure("Got wrong response") elif response.elapsed.total_seconds() > 0.5: response.failure("Request took too long") You can also mark a request as successful, even if the response code was bad: .. code-block:: python with self.client.get("/does_not_exist/", catch_response=True) as response: if response.status_code == 404: response.success() You can even avoid logging a request at all by throwing an exception and then catching it outside the with-block. Or you can throw a :ref:`locust exception `, like in the example below, and let Locust catch it. .. code-block:: python from locust.exception import RescheduleTask ... with self.client.get("/does_not_exist/", catch_response=True) as response: if response.status_code == 404: raise RescheduleTask() REST/JSON APIs -------------- :ref:`FastHttpUser ` provides a ready-made ``rest`` method, but you can also do it yourself: .. code-block:: python from json import JSONDecodeError ... with self.client.post("/", json={"foo": 42, "bar": None}, catch_response=True) as response: try: if response.json()["greeting"] != "hello": response.failure("Did not get expected value in greeting") except JSONDecodeError: response.failure("Response could not be decoded as JSON") except KeyError: response.failure("Response did not contain expected key 'greeting'") .. _name-parameter: Grouping requests ----------------- It's very common for websites to have pages whose URLs contain some kind of dynamic parameter(s). Often it makes sense to group these URLs together in User's statistics. This can be done by passing a *name* argument to the :py:class:`HttpSession's ` different request methods. Example: .. code-block:: python # Statistics for these requests will be grouped under: /blog/?id=[id] for i in range(10): self.client.get("/blog?id=%i" % i, name="/blog?id=[id]") There may be situations where passing in a parameter into request function is not possible, such as when interacting with libraries/SDK's that wrap a Requests session. An alternative way of grouping requests is provided by setting the ``client.request_name`` attribute. .. code-block:: python # Statistics for these requests will be grouped under: /blog/?id=[id] self.client.request_name="/blog?id=[id]" for i in range(10): self.client.get("/blog?id=%i" % i) self.client.request_name=None If you want to chain multiple groupings with minimal boilerplate, you can use the ``client.rename_request()`` context manager. .. code-block:: python @task def multiple_groupings_example(self): # Statistics for these requests will be grouped under: /blog/?id=[id] with self.client.rename_request("/blog?id=[id]"): for i in range(10): self.client.get("/blog?id=%i" % i) # Statistics for these requests will be grouped under: /article/?id=[id] with self.client.rename_request("/article?id=[id]"): for i in range(10): self.client.get("/article?id=%i" % i) Using :ref:`catch_response ` and accessing `request_meta `_ directly, you can even rename requests based on something in the response. .. code-block:: python with self.client.get("/", catch_response=True) as resp: resp.request_meta["name"] = resp.json()["name"] HTTP Proxy settings ------------------- To improve performance, we configure requests to not look for HTTP proxy settings in the environment by setting requests.Session's trust_env attribute to ``False``. If you don't want this, you can manually set ``locust_instance.client.trust_env`` to ``True``. For further details, refer to the `documentation of requests `_. Connection reuse ---------------- By default, connections are reused by an HttpUser, even across tasks runs. To avoid connection reuse you can do: .. code-block:: python self.client.get("/", headers={"Connection": "close"}) self.client.get("/new_connection_here") Or you can close the entire requests.Session object (this also deletes cookies, closes the SSL session etc). This has some CPU overhead (and the response time of the next request will be higher due to SSL renegotiation etc), so dont use this unless you really need it. .. code-block:: python self.client.get("/") self.client.close() self.client.get("/new_connection_here") Connection pooling ------------------ As every :py:class:`HttpUser ` creates new :py:class:`HttpSession `, every user instance has its own connection pool. This is similar to how real users (browsers) would interact with a web server. If you instead want to share connections, you can use a single pool manager. To do this, set :py:attr:`pool_manager ` class attribute to an instance of :py:class:`urllib3.PoolManager`. .. code-block:: python from locust import HttpUser from urllib3 import PoolManager class MyUser(HttpUser): # All instances of this class will be limited to 10 concurrent connections at most. pool_manager = PoolManager(maxsize=10, block=True) For more configuration options, refer to the `urllib3 documentation `_. TaskSets ================================ TaskSets is a way to structure tests of hierarchical websites/systems. You can :ref:`read more about it here `. Examples ======== There are lots of locustfile examples `here `_ How to structure your test code ================================ It's important to remember that the locustfile.py is just an ordinary Python module that is imported by Locust. From this module you're free to import other python code just as you normally would in any Python program. The current working directory is automatically added to python's ``sys.path``, so any python file/module/packages that resides in the working directory can be imported using the python ``import`` statement. For small tests, keeping all the test code in a single ``locustfile.py`` should work fine, but for larger test suites, you'll probably want to split the code into multiple files and directories. How you structure the test source code is of course entirely up to you, but we recommend that you follow Python best practices. Here's an example file structure of an imaginary Locust project: * Project root * ``common/`` * ``__init__.py`` * ``auth.py`` * ``config.py`` * ``locustfile.py`` * ``requirements.txt`` (External Python dependencies is often kept in a requirements.txt) A project with multiple locustfiles could also keep them in a separate subdirectory: * Project root * ``common/`` * ``__init__.py`` * ``auth.py`` * ``config.py`` * ``my_locustfiles/`` * ``api.py`` * ``website.py`` * ``requirements.txt`` With any of the above project structure, your locustfile can import common libraries using: .. code-block:: python import common.auth ================================================ FILE: examples/add_command_line_argument.py ================================================ from locust import HttpUser, events, task @events.init_command_line_parser.add_listener def _(parser): parser.add_argument("--my-argument", type=str, env_var="LOCUST_MY_ARGUMENT", default="", help="It's working") # Choices will validate command line input and show a dropdown in the web UI parser.add_argument("--env", choices=["dev", "staging", "prod"], default="dev", help="Environment") # In combination with choices, is_multiple makes the dropdown in the web UI allow multiple selections parser.add_argument("--endpoints", choices=["a", "b", "c"], is_multiple=True, default=["a"], help="Endpoints") # Set `include_in_web_ui` to False if you want to hide from the web UI parser.add_argument("--my-ui-invisible-argument", include_in_web_ui=False, default="I am invisible") # Set `is_secret` to True if you want the text input to be password masked in the web UI parser.add_argument("--my-ui-password-argument", is_secret=True, default="I am a secret") # Use a boolean default value if you want the input to be a checkmark parser.add_argument("--my-ui-boolean-argument", default=True) # Set `is_required` to mark a form field as required parser.add_argument("--my-ui-required-argument", is_required=True, default="I am required") @events.test_start.add_listener def _(environment, **kw): print(f"Custom argument supplied: {environment.parsed_options.my_argument}") class WebsiteUser(HttpUser): @task def my_task(self): print(f"my_argument={self.environment.parsed_options.my_argument}") print(f"my_ui_invisible_argument={self.environment.parsed_options.my_ui_invisible_argument}") ================================================ FILE: examples/basic.py ================================================ from locust import HttpUser, TaskSet, between, task def index(l): l.client.get("/") def stats(l): l.client.get("/stats/requests") class UserTasks(TaskSet): # one can specify tasks like this tasks = [index, stats] # but it might be convenient to use the @task decorator @task def page404(self): self.client.get("/does_not_exist") class WebsiteUser(HttpUser): """ User class that does requests to the locust web server running on localhost """ host = "http://127.0.0.1:8089" wait_time = between(2, 5) tasks = [UserTasks] ================================================ FILE: examples/bottlenecked_server.py ================================================ """ This example uses extensions in Locust's own WebUI to simulate a bottlenecked server and runs a test against itself. The purpose of this is mainly to generate nice graphs in the UI to teach new users how to interpret load test results. See https://docs.locust.io/en/stable/quickstart.html#locust-s-web-interface """ from locust import HttpUser, events, run_single_user, task import time from threading import Semaphore # Only allow up to 10 concurrent requests. Similar to how a server with 10 threads might behave. sema = Semaphore(10) class WebsiteUser(HttpUser): host = "http://127.0.0.1:8089" @task def index(l): l.client.get("/slow") @events.init.add_listener def locust_init(environment, **kwargs): assert environment.web_ui, "you can't run this headless" @environment.web_ui.app.route("/slow") def my_added_page(): with sema: # only 10 requests can hold this lock at the same time time.sleep(1) # pretend each request takes 1 second to execute return "Another page" if __name__ == "__main__": run_single_user(WebsiteUser) ================================================ FILE: examples/browse_docs_sequence_test.py ================================================ # This locust test script example will simulate a user # browsing the Locust documentation on https://docs.locust.io/ from locust import HttpUser, SequentialTaskSet, between, task import random from pyquery import PyQuery class BrowseDocumentationSequence(SequentialTaskSet): def on_start(self): self.urls_on_current_page = self.toc_urls = None # assume all users arrive at the index page @task def index_page(self): r = self.client.get("/") pq = PyQuery(r.content) link_elements = pq(".toctree-wrapper a.internal") self.toc_urls = [l.attrib["href"] for l in link_elements] # it is fine to do multiple requests in a single task, you dont need SequentialTaskSet for that self.client.get("/favicon.ico") @task def load_page(self, url=None): url = random.choice(self.toc_urls) r = self.client.get(url) pq = PyQuery(r.content) link_elements = pq("a.internal") self.urls_on_current_page = [l.attrib["href"] for l in link_elements] @task def load_sub_page(self): url = random.choice(self.urls_on_current_page) r = self.client.get(url) class AwesomeUser(HttpUser): tasks = [BrowseDocumentationSequence] host = "https://docs.locust.io/en/latest/" # we assume someone who is browsing the Locust docs, # generally has a quite long waiting time (between # 20 and 600 seconds), since there's a bunch of text # on each page wait_time = between(20, 600) ================================================ FILE: examples/browse_docs_test.py ================================================ # This locust test script example will simulate a user # browsing the Locust documentation on https://docs.locust.io/ from locust import HttpUser, TaskSet, between, task import random from pyquery import PyQuery class BrowseDocumentation(TaskSet): def on_start(self): # assume all users arrive at the index page self.index_page() self.urls_on_current_page = self.toc_urls @task(10) def index_page(self): r = self.client.get("/") pq = PyQuery(r.content) link_elements = pq(".toctree-wrapper a.internal") self.toc_urls = [l.attrib["href"] for l in link_elements] @task(50) def load_page(self, url=None): url = random.choice(self.toc_urls) r = self.client.get(url) pq = PyQuery(r.content) link_elements = pq("a.internal") self.urls_on_current_page = [l.attrib["href"] for l in link_elements] @task(30) def load_sub_page(self): url = random.choice(self.urls_on_current_page) r = self.client.get(url) class AwesomeUser(HttpUser): tasks = [BrowseDocumentation] host = "https://docs.locust.io/en/latest/" # we assume someone who is browsing the Locust docs, # generally has a quite long waiting time (between # 20 and 600 seconds), since there's a bunch of text # on each page wait_time = between(20, 600) ================================================ FILE: examples/csrf_form_authentication.py ================================================ from locust import HttpUser, between, task import re class WebsiteUser(HttpUser): host = "http://127.0.0.1:8089" wait_time = between(2, 5) @task def authenticate(self): with self.client.get("/sign-in", catch_response=True) as response: match = re.search( r']*value="([^"]*)"', response.text, ) token = match.group(1) with self.client.post( "/sign-in", { "user[email]": "username", "user[password]": "password", "authenticity_token": token, }, catch_response=True, ) as response: if "welcome" not in response.url: response.failure("Login failed") ================================================ FILE: examples/custom_messages.py ================================================ from locust import HttpUser, between, events, task from locust.runners import LocalRunner, MasterRunner, WorkerRunner import gevent usernames = [] def setup_test_users(environment, msg, **kwargs): # Fired when the worker receives a message of type 'test_users' usernames.extend(map(lambda u: u["name"], msg.data)) environment.runner.send_message("acknowledge_users", f"Thanks for the {len(msg.data)} users!") environment.runner.send_message("concurrent_message", "Message to concurrent handler") def on_acknowledge(msg, **kwargs): # Fired when the master receives a message of type 'acknowledge_users' print(msg.data) def on_concurrent_message(msg, **kwargs): print(f"concurrent_message received with data: '{msg.data}'") gevent.sleep(10) # if this handler was run with concurrent=False it would halt the message handling loop in locust print("finished processing concurrent_message") @events.init.add_listener def on_locust_init(environment, **_kwargs): if not isinstance(environment.runner, MasterRunner): environment.runner.register_message("test_users", setup_test_users) if not isinstance(environment.runner, WorkerRunner): environment.runner.register_message("acknowledge_users", on_acknowledge) environment.runner.register_message("concurrent_message", on_concurrent_message, concurrent=True) @events.test_start.add_listener def on_test_start(environment, **_kwargs): # When the test is started, evenly divides list between # worker nodes to ensure unique data across threads if not isinstance(environment.runner, WorkerRunner): users = [] for i in range(environment.runner.target_user_count): users.append({"name": f"User{i}"}) worker_count = environment.runner.worker_count chunk_size = int(len(users) / worker_count) if isinstance(environment.runner, LocalRunner): workers = [environment.runner] else: workers = environment.runner.clients for i, worker in enumerate(workers): start_index = i * chunk_size if i + 1 < worker_count: end_index = start_index + chunk_size else: end_index = len(users) data = users[start_index:end_index] environment.runner.send_message("test_users", data, worker) class WebsiteUser(HttpUser): host = "http://127.0.0.1:8089" wait_time = between(2, 5) def __init__(self, parent): self.username = usernames.pop() super().__init__(parent) @task def task(self): print(self.username) ================================================ FILE: examples/custom_shape/double_wave.py ================================================ from locust import HttpUser, LoadTestShape, TaskSet, constant, task import math class UserTasks(TaskSet): @task def get_root(self): self.client.get("/") class WebsiteUser(HttpUser): wait_time = constant(0.5) tasks = [UserTasks] class DoubleWave(LoadTestShape): """ A shape to imitate some specific user behaviour. In this example, midday and evening meal times. First peak of users appear at time_limit/3 and second peak appears at 2*time_limit/3 Settings: min_users -- minimum users peak_one_users -- users in first peak peak_two_users -- users in second peak time_limit -- total length of test """ min_users = 20 peak_one_users = 60 peak_two_users = 40 time_limit = 600 def tick(self): run_time = round(self.get_run_time()) if run_time < self.time_limit: user_count = ( (self.peak_one_users - self.min_users) * math.e ** -(((run_time / (self.time_limit / 10 * 2 / 3)) - 5) ** 2) + (self.peak_two_users - self.min_users) * math.e ** -(((run_time / (self.time_limit / 10 * 2 / 3)) - 10) ** 2) + self.min_users ) return (round(user_count), round(user_count)) else: return None ================================================ FILE: examples/custom_shape/stages.py ================================================ from locust import HttpUser, LoadTestShape, TaskSet, constant, task class UserTasks(TaskSet): @task def get_root(self): self.client.get("/") class WebsiteUser(HttpUser): wait_time = constant(0.5) tasks = [UserTasks] class StagesShape(LoadTestShape): """ A simply load test shape class that has different user and spawn_rate at different stages. Keyword arguments: stages -- A list of dicts, each representing a stage with the following keys: duration -- When this many seconds pass the test is advanced to the next stage users -- Total user count spawn_rate -- Number of users to start/stop per second stop -- A boolean that can stop that test at a specific stage stop_at_end -- Can be set to stop once all stages have run. """ stages = [ {"duration": 60, "users": 10, "spawn_rate": 10}, {"duration": 100, "users": 50, "spawn_rate": 10}, {"duration": 180, "users": 100, "spawn_rate": 10}, {"duration": 220, "users": 30, "spawn_rate": 10}, {"duration": 230, "users": 10, "spawn_rate": 10}, {"duration": 240, "users": 1, "spawn_rate": 1}, ] def tick(self): run_time = self.get_run_time() for stage in self.stages: if run_time < stage["duration"]: tick_data = (stage["users"], stage["spawn_rate"]) return tick_data return None ================================================ FILE: examples/custom_shape/staging_user_classes.py ================================================ from locust import HttpUser, LoadTestShape, TaskSet, constant, task class UserTasks(TaskSet): @task def get_root(self): self.client.get("/") class WebsiteUserA(HttpUser): wait_time = constant(0.5) tasks = [UserTasks] class WebsiteUserB(HttpUser): wait_time = constant(0.5) tasks = [UserTasks] class StagesShapeWithCustomUsers(LoadTestShape): """ A simply load test shape class that has different user and spawn_rate at different stages. Keyword arguments: stages -- A list of dicts, each representing a stage with the following keys: duration -- When this many seconds pass the test is advanced to the next stage users -- Total user count spawn_rate -- Number of users to start/stop per second stop -- A boolean that can stop that test at a specific stage stop_at_end -- Can be set to stop once all stages have run. """ stages = [ {"duration": 60, "users": 10, "spawn_rate": 10, "user_classes": [WebsiteUserA]}, {"duration": 100, "users": 50, "spawn_rate": 10, "user_classes": [WebsiteUserB]}, {"duration": 180, "users": 100, "spawn_rate": 10, "user_classes": [WebsiteUserA]}, {"duration": 220, "users": 30, "spawn_rate": 10}, {"duration": 230, "users": 10, "spawn_rate": 10}, {"duration": 240, "users": 1, "spawn_rate": 1}, ] def tick(self): run_time = self.get_run_time() for stage in self.stages: if run_time < stage["duration"]: # Not the smartest solution, TODO: find something better try: tick_data = (stage["users"], stage["spawn_rate"], stage["user_classes"]) except KeyError: tick_data = (stage["users"], stage["spawn_rate"]) return tick_data return None ================================================ FILE: examples/custom_shape/step_load.py ================================================ from locust import HttpUser, LoadTestShape, TaskSet, constant, task import math class UserTasks(TaskSet): @task def get_root(self): self.client.get("/") class WebsiteUser(HttpUser): wait_time = constant(0.5) tasks = [UserTasks] class StepLoadShape(LoadTestShape): """ A step load shape Keyword arguments: step_time -- Time between steps step_load -- User increase amount at each step spawn_rate -- Users to stop/start per second at every step time_limit -- Time limit in seconds """ step_time = 30 step_load = 10 spawn_rate = 10 time_limit = 600 def tick(self): run_time = self.get_run_time() if run_time > self.time_limit: return None current_step = math.floor(run_time / self.step_time) + 1 return (current_step * self.step_load, self.spawn_rate) ================================================ FILE: examples/custom_shape/wait_user_count.py ================================================ from locust import HttpUser, LoadTestShape, TaskSet, constant, task import random import time from collections import namedtuple class UserTasks(TaskSet): @task def get_root(self): self.client.get("/") class WebsiteUser(HttpUser): wait_time = constant(0.5) tasks = [UserTasks] def __init__(self, *args, **kwargs): # we arbitrarily make the users very slow to create, and also # unpredictably slow. One way this might happen in real use cases is if # the User has a slow initialization for gathering data to randomly # select. time.sleep(random.randint(0, 5)) super().__init__(*args, **kwargs) Step = namedtuple("Step", ["users", "dwell"]) class StepLoadShape(LoadTestShape): """ A step load shape that waits until the target user count has been reached before waiting on a per-step timer. The purpose here is to ensure that a target number of users is always reached, regardless of how slow the user spawn rate is. The dwell time is there to observe the steady state at that number of users. Keyword arguments: targets_with_times -- iterable of 2-tuples, with the desired user count first, and the dwell (hold) time with that user count second """ targets_with_times = (Step(10, 10), Step(20, 15), Step(10, 10)) def __init__(self, *args, **kwargs): self.step = 0 self.time_active = False super().__init__(*args, **kwargs) def tick(self): if self.step >= len(self.targets_with_times): return None target = self.targets_with_times[self.step] users = self.get_current_user_count() if target.users == users: if not self.time_active: self.reset_time() self.time_active = True run_time = self.get_run_time() if run_time > target.dwell: self.step += 1 self.time_active = False # Spawn rate is the second value here. It is not relevant because we are # rate-limited by the User init rate. We set it arbitrarily high, which # means "spawn as fast as you can" return (target.users, 100) ================================================ FILE: examples/custom_wait_function.py ================================================ from locust import HttpUser, TaskSet, task import random def index(l): l.client.get("/") def stats(l): l.client.get("/stats/requests") class UserTasks(TaskSet): # one can specify tasks like this tasks = [index, stats] # but it might be convenient to use the @task decorator @task def page404(self): self.client.get("/does_not_exist") class WebsiteUser(HttpUser): """ User class that does requests to the locust web server running on localhost """ host = "http://127.0.0.1:8089" # Most task inter-arrival times approximate to exponential distributions # We will model this wait time as exponentially distributed with a mean of 1 second wait_time = lambda self: random.expovariate(1) tasks = [UserTasks] def strictExp(min_wait, max_wait, mu=1): """ Returns an exponentially distributed time strictly between two bounds. """ while True: x = random.expovariate(mu) increment = (max_wait - min_wait) / (mu * 6.0) result = min_wait + (x * increment) if result < max_wait: break return result class StrictWebsiteUser(HttpUser): """ User class that makes exponential requests but strictly between two bounds. """ host = "http://127.0.0.1:8089" wait_time = lambda self: strictExp(3, 7) tasks = [UserTasks] ================================================ FILE: examples/custom_xmlrpc_client/server.py ================================================ import random import time from xmlrpc.server import SimpleXMLRPCServer def get_time(): time.sleep(random.random()) return time.time() def get_random_number(low, high): time.sleep(random.random()) return random.randint(low, high) server = SimpleXMLRPCServer(("localhost", 8877)) print("Listening on port 8877...") server.register_function(get_time, "get_time") server.register_function(get_random_number, "get_random_number") server.serve_forever() ================================================ FILE: examples/custom_xmlrpc_client/xmlrpc_locustfile.py ================================================ from locust import User, task import time from xmlrpc.client import Fault, ServerProxy class XmlRpcClient(ServerProxy): """ XmlRpcClient is a wrapper around the standard library's ServerProxy. It proxies any function calls and fires the *request* event when they finish, so that the calls get recorded in Locust. """ def __init__(self, host, request_event): super().__init__(host) self._request_event = request_event def __getattr__(self, name): func = ServerProxy.__getattr__(self, name) def wrapper(*args, **kwargs): request_meta = { "request_type": "xmlrpc", "name": name, "start_time": time.time(), "response_length": 0, # calculating this for an xmlrpc.client response would be too hard "response": None, "context": {}, # see HttpUser if you actually want to implement contexts "exception": None, } start_perf_counter = time.perf_counter() try: request_meta["response"] = func(*args, **kwargs) except Fault as e: request_meta["exception"] = e request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000 self._request_event.fire(**request_meta) # This is what makes the request actually get logged in Locust return request_meta["response"] return wrapper class XmlRpcUser(User): """ A minimal Locust user class that provides an XmlRpcClient to its subclasses """ abstract = True # dont instantiate this as an actual user when running Locust def __init__(self, environment): super().__init__(environment) self.client = XmlRpcClient(self.host, request_event=environment.events.request) # The real user class that will be instantiated and run by Locust # This is the only thing that is actually specific to the service that we are testing. class MyUser(XmlRpcUser): host = "http://127.0.0.1:8877/" @task def get_time(self): self.client.get_time() @task def get_random_number(self): self.client.get_random_number(0, 100) ================================================ FILE: examples/debugging.py ================================================ from locust import HttpUser, run_single_user, task class QuickstartUser(HttpUser): host = "http://localhost" @task def hello_world(self): with self.client.get("/hello", catch_response=True) as resp: pass # maybe set a breakpoint here to analyze the resp object? # if launched directly, e.g. "python3 debugging.py", not "locust -f debugging.py" if __name__ == "__main__": run_single_user(QuickstartUser) ================================================ FILE: examples/debugging_advanced.py ================================================ from locust import HttpUser, run_single_user, task from locust.exception import StopUser class User1(HttpUser): host = "http://localhost" @task def hello_world(self): with self.client.get("/hello1", catch_response=True) as resp: pass raise StopUser() class User2(HttpUser): host = "http://localhost" @task def hello_world(self): with self.client.get("/hello2", catch_response=True) as resp: pass raise StopUser() if __name__ == "__main__": print("running User1") run_single_user(User1) print("running User2") run_single_user(User2) print("done!") ================================================ FILE: examples/dispatch_test_scripts/locustfile.py ================================================ from locust import HttpUser, LoadTestShape, constant, task class UserA(HttpUser): wait_time = constant(600) # host = "https://example.com" @task def get_root(self): self.client.get("/", name="UserA") class UserB(HttpUser): wait_time = constant(600) # host = "https://example.com" @task def get_root(self): self.client.get("/", name="UserB") class UserC(HttpUser): wait_time = constant(600) # host = "https://example.com" @task def get_root(self): self.client.get("/", name="UserC") # class StepLoadShape(LoadTestShape): # step_time = 30 # step_load = 10 # spawn_rate = 1 # time_limit = 300 # # def tick(self): # run_time = self.get_run_time() # # if run_time > self.time_limit: # return None # # current_step = math.floor(run_time / self.step_time) + 1 # return current_step * self.step_load, self.spawn_rate class RampUpThenDownLoadShape(LoadTestShape): stages = [ {"duration": 5, "users": 1, "spawn_rate": 1}, {"duration": 35, "users": 30, "spawn_rate": 1}, {"duration": 35, "users": 1, "spawn_rate": 1}, {"duration": 35, "users": 73, "spawn_rate": 6}, {"duration": 35, "users": 1, "spawn_rate": 6}, {"duration": 35, "users": 153, "spawn_rate": 17}, {"duration": 10, "users": 145, "spawn_rate": 1}, {"duration": 60, "users": 130, "spawn_rate": 0.25}, {"duration": 15, "users": 50, "spawn_rate": 25}, {"duration": 20, "users": 1, "spawn_rate": 5}, ] for previous_stage, stage in zip(stages[:-1], stages[1:]): stage["duration"] += previous_stage["duration"] for previous_stage, stage in zip(stages[:-1], stages[1:]): assert stage["duration"] > previous_stage["duration"] def tick(self): run_time = self.get_run_time() for stage in self.stages: if run_time < stage["duration"]: tick_data = (stage["users"], stage["spawn_rate"]) return tick_data return None ================================================ FILE: examples/dispatch_test_scripts/run-disributed-headless.sh ================================================ #!/usr/bin/env bash set -o errexit set -o pipefail set -o nounset script_dir="$(realpath "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )")" root_dir="$(realpath "${script_dir}/..")" pushd "${root_dir}" "${VIRTUAL_ENV}/bin/pip" install --editable . popd pushd "${script_dir}" pids=() function cleanup(){ for pid in ${pids} do echo "Killing worker with pid ${pid}" kill -9 "${pid}" || echo "Failed killing worker with pid ${pid}" done } trap cleanup EXIT export LOCUST_WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP=5 n_workers=4 for n in $(seq 1 ${n_workers}) do tmp_file="$(mktemp /tmp/locust-worker-${n}.log.XXXXXX)" echo "Starting worker ${n} (logs in ${tmp_file})" "${VIRTUAL_ENV}/bin/python" -m locust \ --locustfile "${script_dir}/locustfile.py" \ --host "https://example.com" \ --worker \ --master-host "0.0.0.0" \ --master-port "5557" \ --logfile "${tmp_file}" \ --loglevel "DEBUG" & pids+=($!) done echo "Starting master" "${VIRTUAL_ENV}/bin/python" -m locust \ --locustfile "${script_dir}/locustfile.py" \ --host "https://example.com" \ --headless \ --master \ --master-bind-host "0.0.0.0" \ --master-bind-port "5557" \ --expect-workers ${n_workers} \ --loglevel "DEBUG" popd ================================================ FILE: examples/dispatch_test_scripts/run-disributed-web.sh ================================================ #!/usr/bin/env bash set -o errexit set -o pipefail set -o nounset script_dir="$(realpath "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )")" root_dir="$(realpath "${script_dir}/..")" pushd "${root_dir}" "${VIRTUAL_ENV}/bin/pip" install --editable . popd pushd "${script_dir}" pids=() function cleanup(){ for pid in ${pids} do echo "Killing worker with pid ${pid}" kill -9 "${pid}" || echo "Failed killing worker with pid ${pid}" done } trap cleanup EXIT export LOCUST_WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP=5 n_workers=13 for n in $(seq 1 ${n_workers}) do tmp_file="$(mktemp /tmp/locust-worker-${n}.log.XXXXXX)" echo "Starting worker ${n} (logs in ${tmp_file})" "${VIRTUAL_ENV}/bin/python" -m locust \ --locustfile "${script_dir}/locustfile.py" \ --host "https://example.com" \ --worker \ --master-host "0.0.0.0" \ --master-port "5557" \ --logfile "${tmp_file}" \ --loglevel "DEBUG" & pids+=($!) done echo "Starting master" "${VIRTUAL_ENV}/bin/python" -m locust \ --locustfile "${script_dir}/locustfile.py" \ --host "https://example.com" \ --master \ --master-bind-host "0.0.0.0" \ --master-bind-port "5557" \ --expect-workers ${n_workers} \ --loglevel "DEBUG" popd ================================================ FILE: examples/dispatch_test_scripts/run-local-headless.sh ================================================ #!/usr/bin/env bash set -o errexit set -o pipefail set -o nounset script_dir="$(realpath "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )")" root_dir="$(realpath "${script_dir}/..")" pushd "${root_dir}" "${VIRTUAL_ENV}/bin/pip" install --editable . popd pushd "${script_dir}" "${VIRTUAL_ENV}/bin/python" -m locust \ --locustfile "${script_dir}/locustfile.py" \ --headless \ --host "https://example.com" \ --loglevel "INFO" popd ================================================ FILE: examples/dispatch_test_scripts/run-local-web.sh ================================================ #!/usr/bin/env bash set -o errexit set -o pipefail set -o nounset script_dir="$(realpath "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )")" root_dir="$(realpath "${script_dir}/..")" pushd "${root_dir}" "${VIRTUAL_ENV}/bin/pip" install --editable . popd pushd "${script_dir}" "${VIRTUAL_ENV}/bin/python" -m locust \ --locustfile "${script_dir}/locustfile.py" \ --host "https://example.com" \ --loglevel "DEBUG" popd ================================================ FILE: examples/dns_ex.py ================================================ from locust import run_single_user, task from locust.contrib.dns import DNSUser import time import dns.message import dns.rdatatype class MyDNSUser(DNSUser): @task def t(self): message = dns.message.make_query("example.com", dns.rdatatype.A) # self.client wraps all dns.query methods https://dnspython.readthedocs.io/en/stable/query.html self.client.udp(message, "8.8.8.8") self.client.tcp(message, "1.1.1.1") self.client.udp( dns.message.make_query("doesnot-exist-1234234.com", dns.rdatatype.A), "1.1.1.1", name="You can rename requests", ) # don't spam other people's DNS servers time.sleep(10) if __name__ == "__main__": run_single_user(MyDNSUser) ================================================ FILE: examples/docker-compose/docker-compose.yml ================================================ version: '3' services: master: image: locustio/locust ports: - "8089:8089" volumes: - ./:/mnt/locust command: -f /mnt/locust/locustfile.py --master -H http://master:8089 worker: image: locustio/locust volumes: - ./:/mnt/locust command: -f /mnt/locust/locustfile.py --worker --master-host master ================================================ FILE: examples/dynamic_user_credentials.py ================================================ # locustfile.py from locust import HttpUser, TaskSet, between, task USER_CREDENTIALS = [ ("user1", "password"), ("user2", "password"), ("user3", "password"), ] class UserBehaviour(TaskSet): def on_start(self): if len(USER_CREDENTIALS) > 0: user, passw = USER_CREDENTIALS.pop() self.client.post("/login", {"username": user, "password": passw}) @task def some_task(self): # user should be logged in here (unless the USER_CREDENTIALS ran out) self.client.get("/protected/resource") class User(HttpUser): tasks = [UserBehaviour] wait_time = between(5, 60) ================================================ FILE: examples/events.py ================================================ """ This is an example of a locustfile that uses Locust's built in event hooks to track the sum of the content-length header in all successful HTTP responses """ from locust import HttpUser, TaskSet, between, events, task class MyTaskSet(TaskSet): @task(2) def index(l): l.client.get("/") @task(1) def stats(l): l.client.get("/stats/requests") class WebsiteUser(HttpUser): host = "http://127.0.0.1:8089" wait_time = between(2, 5) tasks = [MyTaskSet] stats = {"content-length": 0} @events.init.add_listener def locust_init(environment, **kwargs): """ We need somewhere to store the stats. On the master node stats will contain the aggregated sum of all content-lengths, while on the worker nodes this will be the sum of the content-lengths since the last stats report was sent to the master """ if environment.web_ui: # this code is only run on the master node (the web_ui instance doesn't exist on workers) @environment.web_ui.app.route("/content-length") def total_content_length(): """ Add a route to the Locust web app, where we can see the total content-length """ return "Total content-length received: %i" % stats["content-length"] @events.request.add_listener def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): """ Event handler that get triggered on every request. """ stats["content-length"] += response_length @events.report_to_master.add_listener def on_report_to_master(client_id, data): """ This event is triggered on the worker instances every time a stats report is to be sent to the locust master. It will allow us to add our extra content-length data to the dict that is being sent, and then we clear the local stats in the worker. """ data["content-length"] = stats["content-length"] stats["content-length"] = 0 @events.worker_report.add_listener def on_worker_report(client_id, data): """ This event is triggered on the master instance when a new stats report arrives from a worker. Here we just add the content-length to the master's aggregated stats dict. """ stats["content-length"] += data["content-length"] ================================================ FILE: examples/extend_web_ui.py ================================================ """ This is an example of a locustfile that uses Locust's built in event and web UI extension hooks to track the sum of the content-length header in all successful HTTP responses and display them in the web UI. """ from locust import HttpUser, TaskSet, between, events, task import json import os from time import time from flask import Blueprint, make_response, render_template, request class MyTaskSet(TaskSet): @task(2) def index(l): l.client.get("/") @task(1) def stats(l): l.client.get("/stats/requests") class WebsiteUser(HttpUser): host = "http://127.0.0.1:8089" wait_time = between(2, 5) tasks = [MyTaskSet] stats = {} path = os.path.dirname(os.path.abspath(__file__)) extend = Blueprint( "extend", "extend_web_ui", static_folder=f"{path}/static/", static_url_path="/extend/static/", template_folder=f"{path}/templates/", ) @events.init.add_listener def locust_init(environment, **kwargs): """ We need somewhere to store the stats. On the master node stats will contain the aggregated sum of all content-lengths, while on the worker nodes this will be the sum of the content-lengths since the last stats report was sent to the master """ if environment.web_ui: def get_content_length_stats(): """ This is used by the Content Length tab in the extended web UI to show the stats. """ if stats: stats_tmp = [] for name, inner_stats in stats.items(): content_length = inner_stats["content-length"] stats_tmp.append({"name": name, "content_length": content_length}) # Truncate the total number of stats and errors displayed since a large number of rows will cause the app # to render extremely slowly. return stats_tmp[:500] return stats @environment.web_ui.app.after_request def extend_stats_response(response): if request.path != "/stats/requests": return response response.set_data( json.dumps( {**response.json, "extended_stats": [{"key": "content-length", "data": get_content_length_stats()}]} ) ) return response @extend.route("/extend") def extend_web_ui(): """ Add route to access the extended web UI with our new tab. """ # ensure the template_args are up to date before using them environment.web_ui.update_template_args() return render_template( "index.html", template_args={ **environment.web_ui.template_args, "extended_tabs": [{"title": "Content Length", "key": "content-length"}], "extended_tables": [ { "key": "content-length", "structure": [ {"key": "name", "title": "Name"}, {"key": "content_length", "title": "Total content length"}, ], } ], "extended_csv_files": [ {"href": "/content-length/csv", "title": "Download content length statistics CSV"} ], }, ) @extend.route("/content-length/csv") def request_content_length_csv(): """ Add route to enable downloading of content-length stats as CSV """ response = make_response(content_length_csv()) file_name = f"content_length{time()}.csv" disposition = f"attachment;filename={file_name}" response.headers["Content-type"] = "text/csv" response.headers["Content-disposition"] = disposition return response def content_length_csv(): """Returns the content-length stats as CSV.""" rows = [ ",".join( [ '"Name"', '"Total content-length"', ] ) ] if stats: for url, inner_stats in stats.items(): rows.append(f'"{url}",{inner_stats["content-length"]:.2f}') return "\n".join(rows) # register our new routes and extended UI with the Locust web UI environment.web_ui.app.register_blueprint(extend) @events.request.add_listener def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): """ Event handler that get triggered on every request """ stats.setdefault(name, {"content-length": 0}) stats[name]["content-length"] += response_length @events.reset_stats.add_listener def on_reset_stats(): """ Event handler that get triggered on click of web UI Reset Stats button """ global stats stats = {} ================================================ FILE: examples/fast_http_locust.py ================================================ from locust import FastHttpUser, task class WebsiteUser(FastHttpUser): """ User class that does requests to the locust web server running on localhost, using the fast HTTP client """ host = "http://127.0.0.1:8089" # some things you can configure on FastHttpUser # connection_timeout = 60.0 # insecure = True # max_redirects = 5 # max_retries = 1 # network_timeout = 60.0 # proxy_host = my-proxy.com # proxy_port = 8080 @task def index(self): self.client.get("/") @task def stats(self): self.client.get("/stats/requests") ================================================ FILE: examples/grpc/grpc_user.py ================================================ from locust import User from locust.exception import LocustError import time from collections.abc import Callable from typing import Any import grpc import grpc.experimental.gevent as grpc_gevent from grpc_interceptor import ClientInterceptor # patch grpc so that it uses gevent instead of asyncio grpc_gevent.init_gevent() class LocustInterceptor(ClientInterceptor): def __init__(self, environment, *args, **kwargs): super().__init__(*args, **kwargs) self.env = environment def intercept( self, method: Callable, request_or_iterator: Any, call_details: grpc.ClientCallDetails, ): response = None exception = None start_perf_counter = time.perf_counter() response_length = 0 try: response = method(request_or_iterator, call_details) response_length = response.result().ByteSize() except grpc.RpcError as e: exception = e self.env.events.request.fire( request_type="grpc", name=call_details.method, response_time=(time.perf_counter() - start_perf_counter) * 1000, response_length=response_length, response=response, context=None, exception=exception, ) return response class GrpcUser(User): abstract = True stub_class = None def __init__(self, environment): super().__init__(environment) for attr_value, attr_name in ((self.host, "host"), (self.stub_class, "stub_class")): if attr_value is None: raise LocustError(f"You must specify the {attr_name}.") self._channel = grpc.insecure_channel(self.host) interceptor = LocustInterceptor(environment=environment) self._channel = grpc.intercept_channel(self._channel, interceptor) self.stub = self.stub_class(self._channel) ================================================ FILE: examples/grpc/hello.proto ================================================ syntax = "proto3"; package locust.hello; service HelloService { rpc SayHello (HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } ================================================ FILE: examples/grpc/hello_pb2.py ================================================ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: hello.proto """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name="hello.proto", package="locust.hello", syntax="proto3", serialized_options=None, create_key=_descriptor._internal_create_key, serialized_pb=b'\n\x0bhello.proto\x12\x0clocust.hello"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t" \n\rHelloResponse\x12\x0f\n\x07message\x18\x01 \x01(\t2U\n\x0cHelloService\x12\x45\n\x08SayHello\x12\x1a.locust.hello.HelloRequest\x1a\x1b.locust.hello.HelloResponse"\x00\x62\x06proto3', ) _HELLOREQUEST = _descriptor.Descriptor( name="HelloRequest", full_name="locust.hello.HelloRequest", filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name="name", full_name="locust.hello.HelloRequest.name", index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, ), ], extensions=[], nested_types=[], enum_types=[], serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], serialized_start=29, serialized_end=57, ) _HELLORESPONSE = _descriptor.Descriptor( name="HelloResponse", full_name="locust.hello.HelloResponse", filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name="message", full_name="locust.hello.HelloResponse.message", index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode("utf-8"), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key, ), ], extensions=[], nested_types=[], enum_types=[], serialized_options=None, is_extendable=False, syntax="proto3", extension_ranges=[], oneofs=[], serialized_start=59, serialized_end=91, ) DESCRIPTOR.message_types_by_name["HelloRequest"] = _HELLOREQUEST DESCRIPTOR.message_types_by_name["HelloResponse"] = _HELLORESPONSE _sym_db.RegisterFileDescriptor(DESCRIPTOR) HelloRequest = _reflection.GeneratedProtocolMessageType( "HelloRequest", (_message.Message,), { "DESCRIPTOR": _HELLOREQUEST, "__module__": "hello_pb2", # @@protoc_insertion_point(class_scope:locust.hello.HelloRequest) }, ) _sym_db.RegisterMessage(HelloRequest) HelloResponse = _reflection.GeneratedProtocolMessageType( "HelloResponse", (_message.Message,), { "DESCRIPTOR": _HELLORESPONSE, "__module__": "hello_pb2", # @@protoc_insertion_point(class_scope:locust.hello.HelloResponse) }, ) _sym_db.RegisterMessage(HelloResponse) _HELLOSERVICE = _descriptor.ServiceDescriptor( name="HelloService", full_name="locust.hello.HelloService", file=DESCRIPTOR, index=0, serialized_options=None, create_key=_descriptor._internal_create_key, serialized_start=93, serialized_end=178, methods=[ _descriptor.MethodDescriptor( name="SayHello", full_name="locust.hello.HelloService.SayHello", index=0, containing_service=None, input_type=_HELLOREQUEST, output_type=_HELLORESPONSE, serialized_options=None, create_key=_descriptor._internal_create_key, ), ], ) _sym_db.RegisterServiceDescriptor(_HELLOSERVICE) DESCRIPTOR.services_by_name["HelloService"] = _HELLOSERVICE # @@protoc_insertion_point(module_scope) ================================================ FILE: examples/grpc/hello_pb2_grpc.py ================================================ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import hello_pb2 as hello__pb2 class HelloServiceStub: """Missing associated documentation comment in .proto file.""" def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.SayHello = channel.unary_unary( "/locust.hello.HelloService/SayHello", request_serializer=hello__pb2.HelloRequest.SerializeToString, response_deserializer=hello__pb2.HelloResponse.FromString, ) class HelloServiceServicer: """Missing associated documentation comment in .proto file.""" def SayHello(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details("Method not implemented!") raise NotImplementedError("Method not implemented!") def add_HelloServiceServicer_to_server(servicer, server): rpc_method_handlers = { "SayHello": grpc.unary_unary_rpc_method_handler( servicer.SayHello, request_deserializer=hello__pb2.HelloRequest.FromString, response_serializer=hello__pb2.HelloResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler("locust.hello.HelloService", rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class HelloService: """Missing associated documentation comment in .proto file.""" @staticmethod def SayHello( request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None, ): return grpc.experimental.unary_unary( request, target, "/locust.hello.HelloService/SayHello", hello__pb2.HelloRequest.SerializeToString, hello__pb2.HelloResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata, ) ================================================ FILE: examples/grpc/hello_server.py ================================================ import logging import time from concurrent import futures import grpc import hello_pb2 import hello_pb2_grpc logger = logging.getLogger(__name__) class HelloServiceServicer(hello_pb2_grpc.HelloServiceServicer): def SayHello(self, request, context): name = request.name time.sleep(1) return hello_pb2.HelloResponse(message=f"Hello from Locust, {name}!") def start_server(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=100)) hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloServiceServicer(), server) server.add_insecure_port("localhost:50051") server.start() logger.info("gRPC server started") server.wait_for_termination() if __name__ == "__main__": start_server() ================================================ FILE: examples/grpc/locustfile.py ================================================ from locust import events, task import gevent import grpc_user import hello_pb2 import hello_pb2_grpc from hello_server import start_server # Start the dummy server. This is not something you would do in a real test. @events.init.add_listener def run_grpc_server(environment, **_kwargs): gevent.spawn(start_server) class HelloGrpcUser(grpc_user.GrpcUser): host = "localhost:50051" stub_class = hello_pb2_grpc.HelloServiceStub @task def sayHello(self): self.stub.SayHello(hello_pb2.HelloRequest(name="Test")) ================================================ FILE: examples/locustfile.py ================================================ from locust import HttpUser, between, task import time class QuickstartUser(HttpUser): wait_time = between(1, 2) @task def hello_world(self): self.client.get("/hello") self.client.get("/world") @task(3) def view_item(self): for item_id in range(10): self.client.get(f"/item?id={item_id}", name="/item") time.sleep(1) def on_start(self): self.client.post("/login", json={"username": "foo", "password": "bar"}) ================================================ FILE: examples/manual_stats_reporting.py ================================================ """ Example of a manual_report() function that can be used either as a context manager (with statement), or a decorator, to manually add entries to Locust's statistics. Usage as a context manager: with manual_report("stats entry name"): # Run time of this block will be reported under a stats entry called "stats entry name" # do stuff here, if an Exception is raised, it'll be reported as a failure Usage as a decorator: @task @manual_report def my_task(self): # The run time of this task will be reported under a stats entry called "my task" (type "manual"). # If an Exception is raised, it'll be reported as a failure """ from locust import User, constant, events, task import random from contextlib import contextmanager from time import sleep, time @contextmanager def _manual_report(name): start_time = time() try: yield except Exception as e: events.request.fire( request_type="manual", name=name, response_time=(time() - start_time) * 1000, response_length=0, exception=e, ) raise else: events.request.fire( request_type="manual", name=name, response_time=(time() - start_time) * 1000, response_length=0, exception=None, ) def manual_report(name_or_func): if callable(name_or_func): # used as decorator without name argument specified return _manual_report(name_or_func.__name__)(name_or_func) else: return _manual_report(name_or_func) class MyUser(User): wait_time = constant(1) @task def successful_task(self): with manual_report("successful_task"): sleep(random.random()) @task @manual_report def decorator_test(self): if random.random() > 0.5: raise Exception("decorator_task failed") sleep(random.random()) @task def failing_task(self): with manual_report("failing_task"): sleep(random.random()) raise Exception("Oh nooes!") ================================================ FILE: examples/markov_taskset.py ================================================ from locust import MarkovTaskSet, User, constant, transition, transitions """ This example demonstrates the different ways to specify transitions in a MarkovTaskSet. The MarkovTaskSet class supports several ways to define transitions between tasks: 1. Using @transition decorator for a single transition 2. Stacking multiple @transition decorators 3. Using @transitions with a dictionary of task names and weights 4. Using @transitions with a list of task names (default weight 1) 5. Using @transitions with a list of tuples (task_name, weight) 6. Using @transitions with a mixed list of strings and tuples """ class TransitionsExample(MarkovTaskSet): """ This MarkovTaskSet demonstrates all the different ways to specify transitions. """ @transition("method2") def method1(self): print("Method 1: Using a single @transition decorator") print(" Next: Will transition to method2") @transition("method3", weight=2) @transition("method1") def method2(self): print("Method 2: Using multiple stacked @transition decorators") print(" Next: Will transition to method3 (weight 2) or method1 (weight 1)") @transitions({"method4": 3, "method2": 1, "method1": 1}) def method3(self): print("Method 3: Using @transitions with a dictionary") print(" Next: Will transition to method4 (weight 3), method2 (weight 1), or method1 (weight 1)") @transitions(["method5", "method3"]) def method4(self): print("Method 4: Using @transitions with a list of task names") print(" Next: Will transition to method5 or method3 with equal probability (weight 1 each)") @transitions([("method6", 4), ("method4", 1)]) def method5(self): print("Method 5: Using @transitions with a list of tuples") print(" Next: Will transition to method6 (weight 4) or method4 (weight 1)") @transitions([("method1", 2), "method5"]) def method6(self): print("Method 6: Using @transitions with a mixed list") print(" Next: Will transition to method1 (weight 2) or method5 (weight 1)") class TransitionsUser(User): tasks = [TransitionsExample] wait_time = constant(1) if __name__ == "__main__": from locust import run_single_user run_single_user(TransitionsUser) ================================================ FILE: examples/milvus/README.md ================================================ # Milvus Load Testing with Locust Simple example demonstrating load testing for Milvus vector database operations. ## Prerequisites Install Locust with Milvus support: ```bash # Using pip pip install locust[milvus] # Using uv uv add locust[milvus] # or uv sync --extra milvus ``` This installs the required dependencies: - [`pymilvus`](https://github.com/milvus-io/pymilvus) - Official Milvus Python SDK (>=2.5.0) ## Usage ```bash # Run with web UI locust -f locustfile.py --host=http://localhost:19530 # Run headless locust -f locustfile.py --host=http://localhost:19530 --headless --users=10 --spawn-rate=2 --run-time=60s ``` ## Operations Tested - Insert vectors - Search vectors - Query by filter - Delete records For more advanced usage, see the [Locust documentation](https://docs.locust.io/). ================================================ FILE: examples/milvus/locustfile.py ================================================ """ Minimal example demonstrating Milvus load testing with Locust. """ from locust import between, task from locust.contrib.milvus import MilvusUser import random from pymilvus import CollectionSchema, DataType, FieldSchema from pymilvus.milvus_client import IndexParams class SimpleMilvusUser(MilvusUser): """Minimal Milvus user for load testing.""" wait_time = between(1, 3) def on_start(self): """Generate test vectors.""" self.dimension = 128 self.test_vectors = [[random.random() for _ in range(self.dimension)] for _ in range(10)] def __init__(self, environment): # Define collection schema schema = CollectionSchema( fields=[ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True), FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128), FieldSchema(name="name", dtype=DataType.VARCHAR, max_length=50), ], description="Test collection", ) # Define index parameters index_params = IndexParams() index_params.add_index( field_name="vector", index_type="IVF_FLAT", metric_type="L2", ) super().__init__( environment, uri=environment.host, collection_name="load_test_collection", schema=schema, index_params=index_params, enable_dynamic_field=True, num_shards=2, consistency_level="Eventually", ) @task(3) def insert_data(self): """Insert data into Milvus.""" data = [ { "id": random.randint(1, 10000), "vector": random.choice(self.test_vectors), "name": f"item_{random.randint(1, 1000)}", } ] self.insert(data) @task(5) def search_vectors(self): """Search for similar vectors.""" search_vector = random.choice(self.test_vectors) self.search(data=[search_vector], anns_field="vector", limit=5) @task(2) def query_data(self): """Query data by ID.""" query_id = random.randint(1, 10000) self.query(filter=f"id == {query_id}") @task(1) def delete_data(self): """Delete data.""" delete_id = random.randint(1, 10000) self.delete(filter=f"id == {delete_id}") ================================================ FILE: examples/mongodb/README.md ================================================ # Overview ### Prerequisites: - [`gevent`](https://www.gevent.org/install.html) - [For pymongo to use greenlets instead of standard threads](https://pymongo.readthedocs.io/en/stable/examples/gevent.html) - [`pymongo`](https://pymongo.readthedocs.io/en/stable/installation.html) ### How to run the test: - Set your environment variables for: `MONGODB_URI` - Run locust as usual, see https://docs.locust.io/en/stable/quickstart.html Note: - It is recommended that you use the `--processes` parameter when running this test - see https://docs.locust.io/en/stable/running-distributed.html ================================================ FILE: examples/mongodb/locustfile.py ================================================ from locust import task from locust.contrib.mongodb import MongoDBUser import os class MongoUser(MongoDBUser): conn_string = os.getenv("MONGODB_URI", "mongodb://localhost:27017/defaultdb") db_name = "test" # change to your db name @task def db_query(self): self.client.execute_query("collection", {"field": "value"}) # update to match your collection, field, and value ================================================ FILE: examples/mqtt/README.md ================================================ # MQTT Load testing with Locust ## Prerequisites Have access to a running mqtt broker. ```bash # start mosquitto locally # the configuration file is required to start # WINDOWS docker run -d -p 1883:1883 --name mqtt-broker -v .\\examples\\mqtt\\mosquitto_config\\mosquitto.conf:/mosquitto/config/mosquitto.conf eclipse-mosquitto:latest # UNIX docker run -d -p 1883:1883 --name mqtt-broker -v ./examples/mqtt/mosquitto_config/mosquitto.conf:/mosquitto/config/mosquitto.conf eclipse-mosquitto:latest ``` Install Locust with MQTT support: ```bash # Using pip pip install locust[mqtt] ``` ## Usage ```bash # Run simple example without web UI locust -f examples/mqtt/locustfile.py --headless # Run simple custom client example without web UI locust -f examples/mqtt/locustfile_custom_mqtt_client.py --headless ``` ================================================ FILE: examples/mqtt/locustfile.py ================================================ from locust import task from locust.contrib.mqtt import MqttUser from locust.user.wait_time import between import time class MyUser(MqttUser): host = "localhost" port = 1883 # We could uncomment below to use the WebSockets transport # transport = "websockets" # ws_path = "/mqtt/custom/path" # We'll probably want to throttle our publishing a bit: let's limit it to # 10-100 messages per second. wait_time = between(0.01, 0.1) # Uncomment below if you need to set MQTTv5 # protocol = paho.mqtt.client.MQTTv5 # Sleep for a while to allow the client time to connect. # This is probably not the most "correct" way to do this: a better method # might be to add a gevent.event.Event to the MqttClient's on_connect # callback and wait for that (with a timeout) here. # However, this works well enough for the sake of an example. def on_start(self): time.sleep(5) @task def say_hello(self): self.client.publish("hello/locust", b"hello world") ================================================ FILE: examples/mqtt/locustfile_custom_mqtt_client.py ================================================ from locust import task from locust.contrib.mqtt import MqttClient, MqttUser from locust.user.wait_time import between import time # extend the MqttClient class with your own custom implementation class MyMqttClient(MqttClient): # you can override the event name with your custom implementation def _generate_event_name(self, event_type: str, qos: int, topic: str): return f"mqtt:{event_type}:{qos}" class MyUser(MqttUser): host = "localhost" port = 1883 # We could uncomment below to use the WebSockets transport # transport = "websockets" # ws_path = "/mqtt/custom/path" # We'll probably want to throttle our publishing a bit: let's limit it to # 10-100 messages per second. wait_time = between(0.01, 0.1) # override the client_cls with your custom MqttClient implementation client_cls = MyMqttClient # Sleep for a while to allow the client time to connect. # This is probably not the most "correct" way to do this: a better method # might be to add a gevent.event.Event to the MqttClient's on_connect # callback and wait for that (with a timeout) here. # However, this works well enough for the sake of an example. def on_start(self): time.sleep(5) @task def say_hello(self): self.client.publish("hello/locust", b"hello world locust custom client") ================================================ FILE: examples/mqtt/mosquitto_config/mosquitto.conf ================================================ # mosquitto_config/mosquitto.conf listener 1883 allow_anonymous true ================================================ FILE: examples/multiple_hosts.py ================================================ from locust import HttpUser, TaskSet, between, task from locust.clients import HttpSession import os class MultipleHostsUser(HttpUser): abstract = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.api_client = HttpSession( base_url=os.environ["API_HOST"], request_event=self.client.request_event, user=self ) class UserTasks(TaskSet): # but it might be convenient to use the @task decorator @task def index(self): self.user.client.get("/") @task def index_other_host(self): self.user.api_client.get("/stats/requests") class WebsiteUser(MultipleHostsUser): """ User class that does requests to the locust web server running on localhost """ host = "http://127.0.0.1:8089" wait_time = between(2, 5) tasks = [UserTasks] ================================================ FILE: examples/nested_inline_tasksets.py ================================================ from locust import HttpUser, TaskSet, between, task class WebsiteUser(HttpUser): """ Example of the ability of inline nested TaskSet classes """ host = "http://127.0.0.1:8089" wait_time = between(2, 5) @task class TopLevelTaskSet(TaskSet): @task class IndexTaskSet(TaskSet): @task(10) def index(self): self.client.get("/") @task(1) def stop(self): self.interrupt() @task def stats(self): self.client.get("/stats/requests") ================================================ FILE: examples/open_closed_workload.py ================================================ from locust import HttpUser, constant, constant_pacing, task class ClosedWorkload(HttpUser): wait_time = constant(10) # sleep 10s after each task, regardless of execution time @task def t(self): pass class OpenWorkload(HttpUser): wait_time = constant_pacing(10) # sleep just enough so that we run one task iteration every 10s @task def t(self): pass # Note: A test using constant_pacing is still limited by the number of Users you spawn, # so if response times increase to the point where one task execution takes more than 10s, # you still wont reach your target throughput. ================================================ FILE: examples/openai_ex.py ================================================ # You need to install the openai package and set OPENAI_API_KEY env var to run this # OpenAIUser tracks the number of output tokens in the response_length field, # because it is more useful than the actual payload size. This field is available to event handlers. from locust import run_single_user, task from locust.contrib.oai import OpenAIUser class MyUser(OpenAIUser): @task def t(self): self.client.responses.create( model="gpt-4o", instructions="You are a coding assistant that speaks like it were a Monty Python skit.", input="How do I check if a Python object is an instance of a class?", ) # print(response.output_text) with self.client.rename_request("mini"): # here's how to rename requests self.client.responses.create( model="gpt-4o-mini", instructions="You are a coding assistant that speaks like it were a Monty Python skit.", input="How do I check if a Python object is an instance of a class?", ) if __name__ == "__main__": run_single_user(MyUser) ================================================ FILE: examples/postgres/README.md ================================================ # Overview Read the instruction below for your specific database ## PostgreSQL ### How to run the test - Prerequisites: - `psycopg3` - https://www.psycopg.org/psycopg3/docs/basic/install.html - Set your environment variables for: - PGHOST - PGPORT - PGDATABASE - PGUSER - PGPASSWORD - Run locust as usual, see https://docs.locust.io/en/stable/quickstart.html ================================================ FILE: examples/postgres/locustfile.py ================================================ from locust import task from locust.contrib.postgres import PostgresUser import os import random class MyUser(PostgresUser): @task def run_select_query(self): self.client.execute_query( "SELECT * FROM loadtesting.invoice WHERE amount > 500", ) @task def run_update_query(self): random_amount = random.randint(1, 12) self.client.execute_query( f"UPDATE loadtesting.invoice SET amount={random_amount} WHERE amount < 10", ) # Use environment variables or default values PGHOST = os.getenv("PGHOST", "localhost") PGPORT = os.getenv("PGPORT", "5432") PGDATABASE = os.getenv("PGDATABASE", "test_db") PGUSER = os.getenv("PGUSER", "postgres") PGPASSWORD = os.getenv("PGPASSWORD", "postgres") conn_string = f"postgresql://{PGUSER}:{PGPASSWORD}@{PGHOST}:{PGPORT}/{PGDATABASE}" ================================================ FILE: examples/qdrant/README.md ================================================ # Qdrant Load Testing with Locust Simple example demonstrating load testing for Qdrant vector database operations. ## Prerequisites Install Locust with Qdrant support: ```bash # Using pip pip install locust[qdrant] # Using uv uv add locust[qdrant] # or uv sync --extra qdrant ``` This installs the required dependencies: - [`qdrant-client`](https://github.com/qdrant/qdrant-client) - Official Qdrant Python SDK ## Usage ```bash # Navigate to this directory cd examples/qdrant ``` ```bash # Run Qdrant docker run -p 6333:6333 -d qdrant/qdrant ``` ```bash # Run headless locust -f locustfile.py --host=http://localhost:6333 --headless --users=10 --spawn-rate=2 --run-time=60s # Run with web UI locust -f locustfile.py --host=http://localhost:6333 ``` ## Operations Tested - Upsert points - Search points - Scroll points - Delete points For more advanced usage, see the [Locust documentation](https://docs.locust.io/). ================================================ FILE: examples/qdrant/locustfile.py ================================================ """ Minimal example demonstrating Qdrant load testing with Locust. """ from locust import between, task from locust.contrib.qdrant import QdrantUser import random from qdrant_client.models import Distance, PointStruct, VectorParams class SimpleQdrantUser(QdrantUser): """Minimal Qdrant user for load testing.""" wait_time = between(1, 3) def on_start(self): self.dimension = 128 self.test_vectors = [[random.random() for _ in range(self.dimension)] for _ in range(10)] collection_name = "load_test_collection" vectors_config = VectorParams( size=128, distance=Distance.COSINE, ) def __init__(self, environment): self.url = environment.host super().__init__(environment) @task(3) def upsert_data(self): points = [ PointStruct( id=random.randint(1, 10000), vector=[random.random() for _ in range(self.dimension)], payload={"name": f"item_{random.randint(1, 1000)}"}, ) ] self.upsert(points) @task(5) def search_vectors(self): search_vector = random.choice(self.test_vectors) self.search(query=search_vector, limit=5) @task(2) def scroll_data(self): self.scroll(limit=5) @task(1) def delete_data(self): delete_id = random.randint(1, 10000) self.delete(points_selector=[delete_id]) ================================================ FILE: examples/response_validations.py ================================================ """ This shows some useful ways to validate responses and how to exit tasks early on failure. """ from locust import FastHttpUser, events, run_single_user, task from locust.exception import RescheduleTask class BadUser(FastHttpUser): @task def t(self): self.client.request("POST", "/authenticate", json={"username": "foo", "password": "bar"}) # ... self.client.request("POST", "/checkout/confirm", json={"foo": "bar"}) # This User will behave identically to BadUser long as things go well, # but give earlier and more detailed information if things go wrong: class GoodUser(FastHttpUser): @task def t(self): with self.rest("POST", "/authenticate", json={"username": "foo", "password": "bar"}) as resp: # check if there was an error field in the response: if error := resp.js.get("error"): resp.failure(error) # to be even more sure things went well, lets validate a success criteria: elif message := resp.js.get("message"): if message != "Welcome foo!": resp.failure(f"Wrong welcome message: {message}") # ... # If you have long flows and can't be bothered to add validations # for each step then at least do it for the final step: with self.rest("POST", "/checkout/confirm", json={"foo": "bar"}) as resp: if not resp.js.get("orderId"): resp.failure("orderId missing") # Break tasks early if there is a failed request @events.init.add_listener def register_request_listener(environment, **kwargs): @events.request.add_listener def request(exception, **kwargs): if exception: raise RescheduleTask() # We'll now add endpoints to Locust's own WebUI to use as a target for the test. # This means so you can't use headless or run_single_user, unless you also start a "headful" Locust instance. # # It is not very relevant to what this example is explaining, so feel free to ignore it. FastHttpUser.host = "http://127.0.0.1:8089" from flask import request @events.init.add_listener def locust_init(environment, **kwargs): if environment.web_ui: @environment.web_ui.app.route("/authenticate", methods=["POST"]) def authenticate(): username = request.get_json()["username"] return {"message": f"Welcome {username}!"} @environment.web_ui.app.route("/checkout/confirm", methods=["POST"]) def checkout_confirm(): foo = request.get_json()["foo"] return {"orderId": 42} if __name__ == "__main__": run_single_user(GoodUser) ================================================ FILE: examples/rest.py ================================================ from locust import FastHttpUser, run_single_user, task from locust.contrib.fasthttp import RestResponseContextManager from locust.user.wait_time import constant from collections.abc import Generator from contextlib import contextmanager class MyUser(FastHttpUser): host = "https://postman-echo.com" wait_time = constant(180) # be nice to postman-echo.com, and dont run this at scale. @task def t(self): # should work with self.rest("GET", "/get", json={"foo": 1}) as resp: if resp.js["args"]["foo"] != 1: resp.failure(f"Unexpected value of foo in response {resp.text}") # should work with self.rest("POST", "/post", json={"foo": 1}) as resp: if resp.js["data"]["foo"] != 1: resp.failure(f"Unexpected value of foo in response {resp.text}") # assertions are a nice short way to express your expectations about the response. The AssertionError thrown will be caught # and fail the request, including the message and the payload in the failure content. assert resp.js["data"]["foo"] == 1, "Unexpected value of foo in response" # assertions are a nice short way to validate the response. The AssertionError they raise # will be caught by rest() and mark the request as failed with self.rest("POST", "/post", json={"foo": 1}) as resp: # mark the request as failed with the message "Assertion failed" assert resp.js["data"]["foo"] == 2 with self.rest("POST", "/post", json={"foo": 1}) as resp: # custom failure message assert resp.js["data"]["foo"] == 2, "my custom error message" with self.rest("POST", "/post", json={"foo": 1}) as resp: # use a trailing comma to append the response text to the custom message assert resp.js["data"]["foo"] == 2, "my custom error message with response text," with self.rest("", "/post", json={"foo": 1}) as resp: # assign and assert in one line assert (foo := resp.js["foo"]) print(f"the number {foo} is awesome") # rest() catches most exceptions, so any programming mistakes you make automatically marks the request as a failure # and stores the callstack in the failure message with self.rest("POST", "/post", json={"foo": 1}) as resp: 1 / 0 # pylint: disable=pointless-statement # response isn't even json, but RestUser will already have been marked it as a failure, so we dont have to do it again with self.rest("GET", "/") as resp: pass with self.rest("GET", "/") as resp: # If resp.js is None (which it will be when there is a connection failure, a non-json responses etc), # reading from resp.js will raise a TypeError (instead of an AssertionError), so lets avoid that: if resp.js: assert resp.js["foo"] == 2 # or, as a mildly confusing oneliner: assert not resp.js or resp.js["foo"] == 2 # 404 with self.rest("GET", "http://example.com/") as resp: pass # connection closed with self.rest("POST", "http://example.com:42/", json={"foo": 1}) as resp: pass # An example of how you might write a common base class for an API that always requires # certain headers, or where you always want to check the response in a certain way class RestUserThatLooksAtErrors(FastHttpUser): abstract = True @contextmanager def rest(self, method, url, **kwargs) -> Generator[RestResponseContextManager]: extra_headers = {"my_header": "my_value"} with super().rest(method, url, headers=extra_headers, **kwargs) as resp: if resp.js and "error" in resp.js and resp.js["error"] is not None: resp.failure(resp.js["error"]) yield resp class MyOtherRestUser(RestUserThatLooksAtErrors): host = "https://postman-echo.com" wait_time = constant(180) # be nice to postman-echo.com, and dont run this at scale. @task def t(self): with self.rest("GET", "/") as _resp: pass if __name__ == "__main__": run_single_user(MyUser) ================================================ FILE: examples/sdk_session_patching/session_patch_locustfile.py ================================================ import locust from locust.user import task from archivist.archivist import Archivist # Example library under test class ArchivistUser(locust.HttpUser): def on_start(self): AUTH_TOKEN = None with open("auth.text") as f: AUTH_TOKEN = f.read() # Start an instance of of the library-provided client self.arch: Archivist = Archivist(url=self.host, auth=AUTH_TOKEN) # overwrite the internal _session attribute with the locust session self.arch._session = self.client @task def Create_assets(self): """User creates assets as fast as possible""" while True: self.arch.assets.create(behaviours=["Builtin", "RecordEvidence", "Attachments"], attrs={"foo": "bar"}) ================================================ FILE: examples/semaphore_wait.py ================================================ from locust import HttpUser, events, task from gevent.lock import Semaphore all_users_spawned = Semaphore() all_users_spawned.acquire() @events.spawning_complete.add_listener def on_spawning_complete(**kw): all_users_spawned.release() class WebsiteUser(HttpUser): host = "http://127.0.0.1:8089" def on_start(self): all_users_spawned.wait() @task def index(self): self.client.get("/") ================================================ FILE: examples/socketio/echo_server.py ================================================ # Used by socketio_ex.py as a mock target. Requires installing gevent-websocket import gevent.monkey gevent.monkey.patch_all() import time import socketio from flask import Flask from gevent import pywsgi from geventwebsocket.handler import WebSocketHandler # Create a Socket.IO server sio = socketio.Server(async_mode="gevent") app = Flask(__name__) app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) DELAY = 0.01 # When a client connects @sio.event def connect(sid, environ): time.sleep(DELAY) print(f"Client connected: {sid}") # Join a room @sio.event def join_room(sid, data): time.sleep(DELAY) room = data.get("room") sio.enter_room(sid, room) print(f"Client {sid} joined room {room}") # Optionally notify the room sio.emit("room_joined", f"{sid} joined {room}", room=room) return f"Joined room {room}" # Leave a room @sio.event def leave_room(sid, data): time.sleep(DELAY) room = data.get("room") sio.leave_room(sid, room) print(f"Client {sid} left room {room}") # Broadcast message to a room @sio.event def send_message(sid, data): time.sleep(DELAY) room = data.get("room") msg = data.get("message") print(f"Sent message ({msg} to room {room}") sio.emit("chat_message", msg, room=room) # When a client disconnects @sio.event def disconnect(sid): time.sleep(DELAY) print(f"Client disconnected: {sid}") server = pywsgi.WSGIServer(("0.0.0.0", 5001), app, handler_class=WebSocketHandler) server.serve_forever() ================================================ FILE: examples/socketio/socketio_ex.py ================================================ from locust import HttpUser, task from locust.contrib.socketio import SocketIOUser from threading import Event import gevent from socketio import Client class MySIOHttpUser(SocketIOUser, HttpUser): options = { # "logger": True, # "engineio_logger": True, } event: Event def on_start(self) -> None: self.sio.connect("ws://localhost:5001", wait_timeout=10) self.sio_greenlet = gevent.spawn(self.sio.wait) # If you need authorization, here's how to do it: # resp = self.client.post("/login", json={"username": "foo", "password": "bar"}) # token = resp.json()["access_token"] # self.sio.connect( # "ws://localhost:5001", # # Option 1: using Authorization header: # headers={"Authorization": f"Bearer {token}"}, # # Option 2: using auth: # # auth={"token": token}, # ) @task def my_task(self): self.event = Event() # Send message and wait for confirmation self.sio.call("join_room", {"room": "room1"}) # Register an event handler self.sio.on("chat_message", self.on_chat_message) # Use socketio.Client to send a message that wont be logged as a request Client.call(self.sio, "send_message", {"room": "room1", "message": "foo"}) # Emit doesnt wait for confirmation self.sio.emit("send_message", {"room": "room1", "message": "bar"}) self.event.wait() # wait for on_chat_message to set this event self.sio.call("leave_room", {"room": "room1"}) # We've used multiple inheritance to combine this with HttpUser, so we can also make normal HTTP requests self.client.get("/") def on_chat_message(self, event: str, data: str) -> None: if data.startswith("bar"): self.event.set() self.sio.on_message(event, data) def on_stop(self) -> None: self.sio.disconnect() ================================================ FILE: examples/stop_on_threshold.py ================================================ # An example of how to stop locust if a threshold (in this case the fail ratio) is exceeded from locust import HttpUser, events, task from locust.runners import STATE_CLEANUP, STATE_STOPPED, STATE_STOPPING, WorkerRunner import time import gevent class MyUser(HttpUser): host = "http://www.google.com" @task def my_task(self): for _ in range(10): self.client.get("/") time.sleep(1) for _ in range(5): self.client.get("/error") time.sleep(1) def checker(environment): while not environment.runner.state in [STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP]: time.sleep(1) if environment.runner.stats.total.fail_ratio > 0.2: print(f"fail ratio was {environment.runner.stats.total.fail_ratio}, quitting") environment.runner.quit() return @events.init.add_listener def on_locust_init(environment, **_kwargs): if not isinstance(environment.runner, WorkerRunner): gevent.spawn(checker, environment) ================================================ FILE: examples/terraform/aws/README.md ================================================ ## This doesnt seem to work right now for some people. See https://github.com/locustio/locust/issues/2357 ## 1. AWS Authentication ```bash export AWS_ACCESS_KEY_ID=AIAXXXXXXXXXXXXXXXXX export AWS_SECRET_ACCESS_KEY=T9HyXXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` ## 2. Configure your provisioning - Don't forget to provide the correct subnet name in the variable file - Define location and file of your locust plan script - Define the number of nodes to create **variables.tf** ```bash variable "node_size" { description = "Size of total nodes" default = 2 } variable "loadtest_dir_source" { default = "plan/" } variable "locust_plan_filename" { default = "basic.py" } variable "subnet_name" { default = "subnet-prd-a" description = "Subnet name" } ``` --- ## 3. Execute Terraform ```bash cd examples/terraform/aws terraform init terraform apply --auto-approve ``` --- ## 4. Access UI Click on the link below to access the UI: Result example: ```bash Apply complete! Resources: 14 added, 0 changed, 0 destroyed. Outputs: dashboard_url = "http://3.237.255.123" leader_private_ip = "10.17.5.119" leader_public_ip = "3.237.255.123" nodes_private_ip = [ "10.17.5.167", "10.17.5.39", ] nodes_public_ip = [ "3.235.45.218", "100.24.124.0", ] ``` ![locust-home](https://github.com/marcosborges/terraform-aws-loadtest-distribuited/raw/v0.4.0/assets/locust-home.png) --- ## 5. Cleanup ```bash terraform destroy --auto-approve ``` --- ## 6. More information - [Terraform aws-get-started >> install-terraform-on-linux](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started#install-terraform-on-linux) - [Terraform module aws loadtest distribuited](https://registry.terraform.io/modules/marcosborges/loadtest-distribuited/aws/latest) ================================================ FILE: examples/terraform/aws/data_subnet.tf ================================================ data "aws_subnet" "current" { filter { name = "tag:Name" values = [var.subnet_name] } } ================================================ FILE: examples/terraform/aws/main.tf ================================================ module "loadtest" { # https://registry.terraform.io/modules/marcosborges/loadtest-distribuited/aws/latest source = "marcosborges/loadtest-distribuited/aws" name = "provision-name" nodes_size = var.node_size executor = "locust" loadtest_dir_source = var.loadtest_dir_source # LEADER ENTRYPOINT loadtest_entrypoint = <<-EOT nohup locust \ -f ${var.locust_plan_filename} \ --web-port=8080 \ --expect-workers=${var.node_size} \ --master > locust-leader.out 2>&1 & EOT # NODES ENTRYPOINT node_custom_entrypoint = <<-EOT nohup locust \ -f ${var.locust_plan_filename} \ --worker \ --master-host={LEADER_IP} > locust-worker.out 2>&1 & EOT subnet_id = data.aws_subnet.current.id locust_plan_filename = var.locust_plan_filename ssh_export_pem = var.ssh_export_pem } ================================================ FILE: examples/terraform/aws/output.tf ================================================ output "leader_public_ip" { value = module.loadtest.leader_public_ip description = "The public IP address of the leader server instance." } output "leader_private_ip" { value = module.loadtest.leader_private_ip description = "The private IP address of the leader server instance." } output "nodes_public_ip" { value = module.loadtest.nodes_public_ip description = "The public IP address of the nodes instances." } output "nodes_private_ip" { value = module.loadtest.nodes_private_ip description = "The private IP address of the nodes instances." } output "dashboard_url" { value = "http://${coalesce(module.loadtest.leader_public_ip, module.loadtest.leader_private_ip)}" description = "The URL of the Locust UI." } ================================================ FILE: examples/terraform/aws/plan/basic.py ================================================ from locust import HttpUser, between, task class Quickstart(HttpUser): wait_time = between(1, 5) @task def google(self): self.client.request_name = "google" self.client.get("https://google.com/") @task def microsoft(self): self.client.request_name = "microsoft" self.client.get("https://microsoft.com/") @task def facebook(self): self.client.request_name = "facebook" self.client.get("https://facebook.com/") ================================================ FILE: examples/terraform/aws/provisioner.tf ================================================ provider "aws" { region = var.aws_region } ================================================ FILE: examples/terraform/aws/variables.tf ================================================ variable "aws_region" { type = string default = "us-east-1" description = "AWS Region" } variable "node_size" { description = "Size of total nodes" default = 2 } variable "loadtest_dir_source" { default = "plan/" } variable "locust_plan_filename" { default = "basic.py" } variable "subnet_name" { default = "subnet-prd-a" description = "Subnet name" } variable "ssh_export_pem" { description = "Export private ssh key" type = bool default = false } ================================================ FILE: examples/test_data_management.py ================================================ # This example shows the various ways to run things before/outside of the normal task execution flow, # which is very useful for fetching test data. # # 1. Locustfile parse time # 2. Locust start (init) # 3. Test start # 4. User start # 5. Inside a task # M1. CPU & memory usage # M2. master sent heartbeat to worker 1-N # M3. worker 1-N received heartbeat from master # (M* are repeated as long as locust is running) # ... # 6. Test run stopping # 7. User stop # 8. Test run stop # (3-8 are repeated if you restart the test in the UI) # 9. Locust quitting # 10. Locust quit # # try it out by running: # locust -f test_data_management.py --headless -u 2 -t 5 --processes 2 from __future__ import annotations from locust import HttpUser, events, task from locust.env import Environment from locust.runners import MasterRunner from locust.user.wait_time import constant import datetime from typing import Any import requests def timestring() -> str: now = datetime.datetime.now() return datetime.datetime.strftime(now, "%m:%S.%f")[:-5] print("1. Parsing locustfile, happens before anything else") # If you want to get something over HTTP at this time you can use `requests` directly: global_test_data = requests.post( "https://postman-echo.com/post", data="global_test_data_" + timestring(), ).json()["data"] test_run_specific_data = None @events.init.add_listener def init(environment: Environment, **_kwargs: Any) -> None: print("2. Initializing locust, happens after parsing the locustfile but before test start") @events.quitting.add_listener def quitting(environment: Environment, **_kwargs: Any) -> None: print("9. locust is about to shut down") @events.test_start.add_listener def test_start(environment: Environment, **_kwargs) -> None: # happens only once in headless runs, but can happen multiple times in web ui-runs global test_run_specific_data print("3. Starting test run") # in a distributed run, the master does not typically need any test data if not isinstance(environment.runner, MasterRunner): test_run_specific_data = requests.post( "https://postman-echo.com/post", data="test-run-specific_" + timestring(), ).json()["data"] @events.heartbeat_sent.add_listener def heartbeat_sent(client_id: str, timestamp: float) -> None: print(f"M2. master sent heartbeat to worker {client_id} at {datetime.datetime.fromtimestamp(timestamp)}") @events.heartbeat_received.add_listener def heartbeat_received(client_id: str, timestamp: float) -> None: print(f"M3. worker {client_id} received heartbeat from master at {datetime.datetime.fromtimestamp(timestamp)}") @events.usage_monitor.add_listener def usage_monitor(environment: Environment, cpu_usage: float, memory_usage: int) -> None: # convert from bytes to Mebibytes memory_usage = memory_usage / 1024 / 1024 print(f"M1. {environment.runner.__class__.__name__}: cpu={cpu_usage}%, memory={memory_usage}M") @events.quit.add_listener def quit(exit_code: int, **kwargs: Any) -> None: print(f"10. Locust has shut down with code {exit_code}") @events.test_stopping.add_listener def test_stopping(environment: Environment, **_kwargs: Any) -> None: print("6. stopping test run") @events.test_stop.add_listener def test_stop(environment: Environment, **_kwargs: Any) -> None: print("8. test run stopped") class MyUser(HttpUser): host = "https://postman-echo.com" wait_time = constant(180) # be nice to postman-echo first_start = True def on_start(self) -> None: if MyUser.first_start: MyUser.first_start = False # This is useful for similar things as to test_start, but happens in the context of a User # In the case of a distributed run, this would be run once per worker. # It will not be re-run on repeated runs (unless you clear the first_start flag) print("X. Here's where you would put things you want to run the first time a User is started") print("4. A user was started") # This is a good place to fetch user-specific test data. It is executed once per User # If you do not want the request logged, you can replace self.client. with requests. self.user_specific_testdata = self.client.post( "https://postman-echo.com/post", data="user-specific_" + timestring(), ).json()["data"] @task def t(self) -> None: self.client.get(f"/get?{global_test_data}") self.client.get(f"/get?{test_run_specific_data}") self.client.get(f"/get?{self.user_specific_testdata}") print("5. Getting task-run-specific testdata") # If every iteration is meant to use new test data this is the most common way to do it task_run_specific_testdata = self.client.post( "https://postman-echo.com/post", data="task_run_specific_testdata_" + timestring(), ).json()["data"] self.client.get(f"/get?{task_run_specific_testdata}") def on_stop(self) -> None: # this is a good place to clean up/release any user-specific test data print("7. a user was stopped") ================================================ FILE: examples/test_pytest.py ================================================ from locust.clients import HttpSession # this import is just for type hints import time # pytest/locust will discover any functions prefixed with "test_" as test cases. # session and fastsession are pytest fixtures provided by Locust's pytest plugin. def test_stuff(session): resp = session.get("https://www.locust.io/") # Bad HTTP status codes in the response dont automatically raise an exception, # so if that is what you want, you need to call: resp.raise_for_status() # In Locust, request-related exceptions are caught (and the test case restarted), # in pytest any exceptions fail the test case # Just like with Locust, you can set a base URL using --host/-H when using pytest. # Or you can set a default: if not session.base_url: session.base_url = "https://www.locust.io" # catch_response works just like in regular locustfiles with session.get("/", catch_response=True) as resp: if not resp.text or not "Locust" in resp.text: resp.failure("important text was missing in response") # raise_for_status also respects calls to resp.failure()/.success() # so this will raise an exception and fail the test case if "Load" was missing resp.raise_for_status() # you can call helper functions as needed helper_function(session) # unlike regular Locust Users, there's no wait_time, so use time.sleep instead time.sleep(0.1) # this is not a test case and won't be detected by pytest/locust def helper_function(session: HttpSession): session.get("/") ================================================ FILE: examples/testdata_from_csv.csv ================================================ myuser1@example.com,password1 myuser2@example.com,password2 ================================================ FILE: examples/testdata_from_csv.py ================================================ """ This shows an easy way to get testdata from a csv file into locust and use it for requests """ from locust import FastHttpUser, events, run_single_user, task import csv import os csvfile = open(os.path.join(os.path.dirname(__file__), "testdata_from_csv.csv")) iter = csv.reader(csvfile) class DemoUser(FastHttpUser): @task def t(self): try: username, password = next(iter) except StopIteration: # go back to start of the file once its been exhausted csvfile.seek(0, 0) username, password = next(iter) with self.rest("POST", "/authenticate", json={"username": username, "password": password}) as resp: if message := resp.js and resp.js.get("message"): if not "Welcome" in message: resp.failure(f"bad response: {message}") # We'll now add endpoints to Locust's own WebUI to use as a target for the test. # This means so you can't use headless or run_single_user, unless you also start a "headful" Locust instance. # # It is not very relevant to what this example is explaining, so feel free to ignore it. FastHttpUser.host = "http://127.0.0.1:8089" from flask import request @events.init.add_listener def locust_init(environment, **kwargs): if environment.web_ui: @environment.web_ui.app.route("/authenticate", methods=["POST"]) def authenticate(): username = request.get_json()["username"] return {"message": f"Welcome {username}!"} @environment.web_ui.app.route("/checkout/confirm", methods=["POST"]) def checkout_confirm(): foo = request.get_json()["foo"] return {"orderId": 42} if __name__ == "__main__": run_single_user(DemoUser) ================================================ FILE: examples/use_as_lib.py ================================================ #!/usr/bin/env python3 from locust import HttpUser, events, task from locust.env import Environment from locust.log import setup_logging from locust.stats import stats_history, stats_printer import gevent setup_logging("INFO") class MyUser(HttpUser): host = "https://docs.locust.io" @task def t(self): self.client.get("/") # setup Environment and Runner env = Environment(user_classes=[MyUser], events=events) runner = env.create_local_runner() # start a WebUI instance web_ui = env.create_web_ui("127.0.0.1", 8089) # execute init event handlers (only really needed if you have registered any) env.events.init.fire(environment=env, runner=runner, web_ui=web_ui) # start a greenlet that periodically outputs the current stats gevent.spawn(stats_printer(env.stats)) # start a greenlet that save current stats to history gevent.spawn(stats_history, env.runner) # start the test runner.start(1, spawn_rate=10) # in 30 seconds stop the runner gevent.spawn_later(30, runner.quit) # wait for the greenlets runner.greenlet.join() # stop the web server for good measures web_ui.stop() ================================================ FILE: examples/vagrant/README.md ================================================ ================================================ FILE: examples/vagrant/supervisord.conf ================================================ [inet_http_server] ; inet (TCP) server disabled by default port=*:9001 ; (ip_address:port specifier, *:port for all iface) ;username=user ; (default is no username (open server)) ;password=123 ; (default is no password (open server)) [supervisord] logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log) logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) logfile_backups=10 ; (num of main logfile rotation backups;default 10) loglevel=info ; (log level;default info; others: debug,warn,trace) pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) nodaemon=false ; (start in foreground if true;default false) minfds=1024 ; (min. avail startup file descriptors;default 1024) minprocs=200 ; (min. avail process descriptors;default 200) [supervisorctl] serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [program:locustmaster] command=locust --master -f examples/basic.py ; TODO host should perhaps be configurable through the web UI process_name=master autostart=true directory=/vagrant priority=1 [program:locustworkers] command=locust --worker -f examples/basic.py ; TODO host should perhaps be configurable through the web UI process_name=worker_%(process_num)s numprocs=2 numprocs_start=1 autostart=true priority=2 directory=/vagrant ================================================ FILE: examples/web_ui_auth/basic.py ================================================ """ Example of implementing authentication for Locust when the --web-login flag is given This is only to serve as a starting point, proper authentication should be implemented according to your projects specifications. For more information, see https://docs.locust.io/en/stable/extending-locust.html#authentication """ from locust import HttpUser, events, task import os from flask import Blueprint, redirect, request, session, url_for from flask_login import UserMixin, login_user class LocustHttpUser(HttpUser): @task def example(self): self.client.get("/") class AuthUser(UserMixin): def __init__(self, username): self.username = username def get_id(self): return self.username def load_user(username): return AuthUser(username) @events.init.add_listener def locust_init(environment, **_kwargs): if environment.web_ui: auth_blueprint = Blueprint("auth", "web_ui_auth", url_prefix=environment.parsed_options.web_base_path) environment.web_ui.login_manager.user_loader(load_user) environment.web_ui.app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY") environment.web_ui.auth_args = { "username_password_callback": f"{environment.parsed_options.web_base_path}/login_submit", "auth_providers": [ { "label": "Github", "callback_url": "/login/github", "icon_url": "https://static-00.iconduck.com/assets.00/github-icon-1024x994-4h5sdmko.png", }, ], } @auth_blueprint.route("/login/github") def google_login(): # Implement authentication with desired auth provider username = "username" session["username"] = username login_user(AuthUser("username")) return redirect(url_for("locust.index")) @auth_blueprint.route("/login_submit", methods=["POST"]) def login_submit(): username = request.form.get("username") password = request.form.get("password") # Implement real password verification here if password: login_user(AuthUser(username)) return redirect(url_for("locust.index")) session["auth_error"] = "Invalid username or password" return redirect(url_for("locust.login")) environment.web_ui.app.register_blueprint(auth_blueprint) ================================================ FILE: examples/web_ui_auth/custom_form.py ================================================ """ Example of implementing authentication with a custom form for Locust when the --web-login flag is given This is only to serve as a starting point, proper authentication should be implemented according to your projects specifications. For more information, see https://docs.locust.io/en/stable/extending-locust.html#authentication """ from __future__ import annotations from locust import HttpUser, events, task import os from flask import Blueprint, redirect, request, session, url_for from flask_login import UserMixin, login_user class LocustHttpUser(HttpUser): @task def example(self): self.client.get("/") class AuthUser(UserMixin): def __init__(self, username): self.username = username self.is_admin = False self.user_group: str | None = None def get_id(self): return self.username def load_user(user_id): return AuthUser(user_id) @events.init.add_listener def locust_init(environment, **_kwargs): if environment.web_ui: auth_blueprint = Blueprint("auth", "web_ui_auth", url_prefix=environment.parsed_options.web_base_path) environment.web_ui.login_manager.user_loader(load_user) environment.web_ui.app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY") environment.web_ui.auth_args = { "custom_form": { "inputs": [ { "label": "Username", "name": "username", # make field required "is_required": True, # override input type for HTML validation # applies if !is_secret and !choices, and default_value is string | None "type": "email", }, # boolean checkmark field {"label": "Admin", "name": "is_admin", "default_value": False}, # select field {"label": "User Group", "name": "user_group", "choices": ["developer", "manager"]}, { "label": "Password", "name": "password", "is_secret": True, }, { "label": "Confirm Password", "name": "confirm_password", "is_secret": True, }, ], "callback_url": f"{environment.parsed_options.web_base_path}/login_submit", "submit_button_text": "Submit", }, } @auth_blueprint.route("/login_submit", methods=["POST"]) def login_submit(): username = request.form.get("username") password = request.form.get("password") confirm_password = request.form.get("confirm_password") is_admin = request.form.get("is_admin") == "on" user_group = request.form.get("user_group") if password != confirm_password: session["auth_error"] = "Passwords do not match!" return redirect(url_for("locust.login")) # Implement real password verification here if password: current_user = AuthUser(username) # do something with your custom variables current_user.is_admin = is_admin current_user.user_group = user_group login_user(AuthUser(username)) return redirect(url_for("locust.index")) session["auth_error"] = "Invalid username or password" return redirect(url_for("locust.login")) environment.web_ui.app.register_blueprint(auth_blueprint) ================================================ FILE: examples/web_ui_cache_stats.py ================================================ """ This is an example of a locustfile that uses Locust's built in event and web UI extension hooks to track the sum of Varnish cache hit/miss headers and display them in the web UI. """ from locust import HttpUser, TaskSet, between, events, task import json import os from time import time from flask import Blueprint, make_response, render_template, request class MyTaskSet(TaskSet): @task(1) def miss(l): """MISS X-Cache header""" l.client.get("/response-headers?X-Cache=MISS") @task(2) def hit(l): """HIT X-Cache header""" l.client.get("/response-headers?X-Cache=HIT") @task(1) def noinfo(l): """No X-Cache header (noinfo counter)""" l.client.get("/") class WebsiteUser(HttpUser): host = "http://httpbin.org" wait_time = between(2, 5) tasks = [MyTaskSet] # This example is based on the Varnish hit/miss headers (https://docs.varnish-software.com/tutorials/hit-miss-logging/). # It could easily be customised for matching other caching systems, CDN or custom headers. CACHE_HEADER = "X-Cache" cache_stats = {} page_stats = {"hit": 0, "miss": 0, "noinfo": 0} path = os.path.dirname(os.path.abspath(__file__)) extend = Blueprint( "extend", "extend_web_ui", static_folder=f"{path}/static/", static_url_path="/extend/static/", template_folder=f"{path}/templates/", ) @events.init.add_listener def locust_init(environment, **kwargs): """ Load data on locust init. :param environment: :param kwargs: :return: """ if environment.web_ui: # this code is only run on the master node (the web_ui instance doesn't exist on workers) def get_cache_stats(): """ This is used by the Cache tab in the extended web UI to show the stats. """ if cache_stats: stats_tmp = [] for name, inner_stats in cache_stats.items(): stats_tmp.append( { "name": name, "hit": inner_stats["hit"], "miss": inner_stats["miss"], "noinfo": inner_stats["noinfo"], } ) # Truncate the total number of stats and errors displayed since a large number # of rows will cause the app to render extremely slowly. return stats_tmp[:500] return cache_stats @environment.web_ui.app.after_request def extend_stats_response(response): if request.path != "/stats/requests": return response # extended_stats contains the data where extended_tables looks for its data: "cache-statistics" response.set_data( json.dumps( {**response.json, "extended_stats": [{"key": "cache-statistics", "data": get_cache_stats()}]} ) ) return response @extend.route("/extend") def extend_web_ui(): """ Add route to access the extended web UI with our new tab. """ # ensure the template_args are up to date before using them environment.web_ui.update_template_args() return render_template( "index.html", template_args={ **environment.web_ui.template_args, # extended_tabs and extended_tables keys must match. "extended_tabs": [{"title": "Cache statistics", "key": "cache-statistics"}], "extended_tables": [ { "key": "cache-statistics", "structure": [ {"key": "name", "title": "Name"}, {"key": "hit", "title": "Hit"}, {"key": "miss", "title": "Miss"}, {"key": "noinfo", "title": "No Info"}, ], } ], "extended_csv_files": [{"href": "/cache/csv", "title": "Download Cache statistics CSV"}], }, ) @extend.route("/cache/csv") def request_cache_csv(): """ Add route to enable downloading of cache stats as CSV """ response = make_response(cache_csv()) file_name = f"cache-{time()}.csv" disposition = f"attachment;filename={file_name}" response.headers["Content-type"] = "text/csv" response.headers["Content-disposition"] = disposition return response def cache_csv(): """Returns the cache stats as CSV.""" rows = [",".join(['"Name"', '"hit"', '"miss"', '"noinfo"'])] if cache_stats: for name, stats in cache_stats.items(): rows.append(f'"{name}",' + ",".join(str(v) for v in stats.values())) return "\n".join(rows) # register our new routes and extended UI with the Locust web UI environment.web_ui.app.register_blueprint(extend) @events.request.add_listener def on_request(name, response, exception, **kwargs): """ Event handler that get triggered on every request """ cache_stats.setdefault(name, page_stats.copy()) if CACHE_HEADER not in response.headers: cache_stats[name]["noinfo"] += 1 elif response.headers[CACHE_HEADER] == "HIT": cache_stats[name]["hit"] += 1 elif response.headers[CACHE_HEADER] == "MISS": cache_stats[name]["miss"] += 1 @events.report_to_master.add_listener def on_report_to_master(client_id, data): """ This event is triggered on the worker instances every time a stats report is to be sent to the locust master. It will allow us to add our extra cache data to the dict that is being sent, and then we clear the local stats in the worker. """ global cache_stats data["cache_stats"] = cache_stats cache_stats = {} @events.worker_report.add_listener def on_worker_report(client_id, data): """ This event is triggered on the master instance when a new stats report arrives from a worker. Here we just add the cache to the master's aggregated stats dict. """ for name in data["cache_stats"]: cache_stats.setdefault(name, page_stats.copy()) for stat_name, value in data["cache_stats"][name].items(): cache_stats[name][stat_name] += value @events.reset_stats.add_listener def on_reset_stats(): """ Event handler that get triggered on click of web UI Reset Stats button """ global cache_stats cache_stats = {} ================================================ FILE: examples/worker_index.py ================================================ # How to use worker_index to read from a pre-partitioned CSV file (mythings_0.csv, mythings_1.csv, ...) # so that each worker uses their own file from locust import User, events, runners, task from locust_plugins import csvreader # install locust-plugins first class DemoUser(User): reader: csvreader.CSVDictReader @task def t(self): thing = next(self.reader) print(thing) @events.init.add_listener def on_locust_init(environment, **_kwargs): if not isinstance(environment.runner, runners.MasterRunner): DemoUser.reader = csvreader.CSVDictReader(f"mythings_{environment.runner.worker_index}.csv") ================================================ FILE: examples/x-forwarded-for.py ================================================ from locust import HttpUser, run_single_user, task import random class ForwardedForUser(HttpUser): subnet = "10.10." def __init__(self, environment): super().__init__(environment) self.fake_ip = self.subnet + str(random.randint(0, 254)) + "." + str(random.randint(0, 254)) self.client.headers["X-Forwarded-For"] = self.fake_ip class WebsiteUser(ForwardedForUser): @task def index(self): with self.client.get("/", catch_response=True) as resp: pass if __name__ == "__main__": run_single_user(WebsiteUser) ================================================ FILE: generate_changelog.py ================================================ #!/usr/bin/env python3 import os import subprocess import sys github_api_token = ( os.getenv("CHANGELOG_GITHUB_TOKEN") if os.getenv("CHANGELOG_GITHUB_TOKEN") else input("Enter Github API token: ") ) if len(sys.argv) < 2: raise Exception("Provide a version number as parameter (--future-release argument)") version = sys.argv[1] cmd = [ "github_changelog_generator", "-t", github_api_token, "-u", "locustio", "-p", "locust", "--exclude-labels", "duplicate,question,invalid,wontfix,cantfix,stale,no-changelog", "--header-label", "# Detailed changelog\nThe most important changes can also be found in [the documentation](https://docs.locust.io/en/latest/changelog.html).", "--since-tag", "2.27.0", # "--since-commit", # these cause issues # "2020-07-01 00:00:00", "--future-release", version, ] print(f"Running command: {' '.join(cmd)}\n") subprocess.run(cmd) ================================================ FILE: hatch_build.py ================================================ import os import subprocess from typing import Any from hatchling.builders.hooks.plugin.interface import BuildHookInterface # type: ignore class BuildFrontend(BuildHookInterface): def initialize(self, version: str, build_data: dict[str, Any]) -> None: # Only build the front end once, in the source dist, the wheel build just copies it from there if self.target_name == "sdist": if not os.environ.get("SKIP_PRE_BUILD"): print("Building front end...") try: subprocess.check_output("yarn install", cwd="locust/webui", shell=True, stderr=subprocess.STDOUT) subprocess.check_output("yarn build", cwd="locust/webui", shell=True, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: raise RuntimeError(f"'{e.cmd}' got exit code {e.returncode}: {e.output}") return super().initialize(version, build_data) ================================================ FILE: locust/__init__.py ================================================ import os if os.getenv("LOCUST_PLAYWRIGHT", None): print("LOCUST_PLAYWRIGHT setting is no longer needed (because locust-plugins no longer installs trio)") print("Uninstall trio package and remove the setting.") try: # preserve backwards compatibility for now import trio # noqa: F401 except ModuleNotFoundError: # dont show a massive callstack if trio is not installed os._exit(1) if not os.getenv("LOCUST_SKIP_MONKEY_PATCH", None): from gevent import monkey, queue monkey.patch_all() if not os.getenv("LOCUST_SKIP_URLLIB3_PATCH", None): import urllib3 urllib3.connectionpool.ConnectionPool.QueueCls = queue.LifoQueue # https://github.com/locustio/locust/issues/2812 from ._version import version as __version__ from .contrib.fasthttp import FastHttpUser from .debug import run_single_user from .event import Events from .shape import LoadTestShape from .user import wait_time from .user.markov_taskset import MarkovTaskSet, transition, transitions from .user.sequential_taskset import SequentialTaskSet from .user.task import TaskSet, tag, task from .user.users import HttpUser, User from .user.wait_time import between, constant, constant_pacing, constant_throughput events = Events() __all__ = ( "SequentialTaskSet", "MarkovTaskSet", "transition", "transitions", "wait_time", "task", "tag", "TaskSet", "HttpUser", "FastHttpUser", "User", "between", "constant", "constant_pacing", "constant_throughput", "events", "LoadTestShape", "run_single_user", "HttpLocust", "Locust", "__version__", ) # Used for raising a DeprecationWarning if old Locust/HttpLocust is used from .util.deprecation import DeprecatedHttpLocustClass as HttpLocust from .util.deprecation import DeprecatedLocustClass as Locust ================================================ FILE: locust/__main__.py ================================================ from .main import main main() ================================================ FILE: locust/argument_parser.py ================================================ from __future__ import annotations import locust from locust import runners from locust.rpc import Message, zmqrpc import argparse import ast import atexit import difflib import json import os import platform import socket import sys import tempfile import textwrap from collections import OrderedDict from typing import Any, NamedTuple from urllib.parse import urlparse from uuid import uuid4 if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib import configargparse import gevent import requests from .util.directory import get_abspaths_in from .util.timespan import parse_timespan from .util.url import is_url version = locust.__version__ DEFAULT_CONFIG_FILES = ("~/.locust.conf", "locust.conf", "pyproject.toml") # Clean up downloaded locustfile on exit def exit_handler(filename) -> None: try: os.remove(filename) except FileNotFoundError: pass # when multiple workers are running on the same machine, another one may already have deleted it except PermissionError: pass # this happens occasionally on windows on GH, maybe for the same reason? class LocustArgumentParser(configargparse.ArgumentParser): """Drop-in replacement for `configargparse.ArgumentParser` that adds support for optionally exclude arguments from the UI. """ def error(self, message): if "unrecognized arguments:" in message: bad_arg = message.split("unrecognized arguments:")[1].strip().split()[0] options = [opt for action in self._actions for opt in action.option_strings] suggestion = difflib.get_close_matches(bad_arg, options, n=1) if suggestion: message += f"\nDid you mean '{suggestion[0]}'?" self.exit(2, f"{self.prog}: error: {message}\n") def add_argument(self, *args, **kwargs) -> configargparse.Action: """ This method supports the same args as ArgumentParser.add_argument(..) as well as the additional args below. Arguments: include_in_web_ui: If True (default), the argument will show in the UI. is_secret: If True (default is False) and include_in_web_ui is True, the argument will show in the UI with a password masked text input. is_required: If True (default is False) and include_in_web_ui is True, the argument will show in the UI as a required form field. is_multiple: If True (default is False) and include_in_web_ui is True, the argument will show in the UI as a multiple select form field. Returns: argparse.Action: the new argparse action """ include_in_web_ui = kwargs.pop("include_in_web_ui", True) is_secret = kwargs.pop("is_secret", False) is_required = kwargs.pop("is_required", False) is_multiple = kwargs.pop("is_multiple", False) action = super().add_argument(*args, **kwargs) action.include_in_web_ui = include_in_web_ui action.is_secret = is_secret action.is_required = is_required action.is_multiple = is_multiple return action @property def args_included_in_web_ui(self) -> dict[str, configargparse.Action]: return {a.dest: a for a in self._actions if hasattr(a, "include_in_web_ui") and a.include_in_web_ui} @property def secret_args_included_in_web_ui(self) -> dict[str, configargparse.Action]: return { a.dest: a for a in self._actions if a.dest in self.args_included_in_web_ui and hasattr(a, "is_secret") and a.is_secret } @property def required_args_included_in_web_ui(self) -> dict[str, configargparse.Action]: return { a.dest: a for a in self._actions if a.dest in self.args_included_in_web_ui and hasattr(a, "is_required") and a.is_required } @property def multiple_args_included_in_web_ui(self) -> dict[str, configargparse.Action]: return { a.dest: a for a in self._actions if a.dest in self.args_included_in_web_ui and hasattr(a, "is_multiple") and a.is_multiple } class LocustTomlConfigParser(configargparse.TomlConfigParser): def parse(self, stream): try: config = tomllib.loads(stream.read()) except Exception as e: raise configargparse.ConfigFileParserException(f"Couldn't parse TOML file: {e}") # convert to dict and filter based on section names result = OrderedDict() for section in self.sections: if data := configargparse.get_toml_section(config, section): for key, value in data.items(): if isinstance(value, list): result[key] = value elif value is None: pass else: result[key] = str(value) break else: if not stream.name.endswith("toml"): raise configargparse.ConfigFileParserException("Not a toml file. Fall back to DefaultConfigFileParser.") return result class LocustConfigParser(configargparse.ConfigFileParser): def parse(self, stream): if stream.name.endswith(".toml"): return LocustTomlConfigParser(["tool.locust"]).parse(stream) return configargparse.DefaultConfigFileParser().parse(stream) def parse_locustfile_paths(paths: list[str]) -> list[str]: """ Returns a list of relative file paths. Args: paths (list[str]): paths taken from the -f command Returns: list[str]: Parsed locust file paths """ # Parse each path and unpack the returned lists as a single list return [parsed for path in paths for parsed in _parse_locustfile_path(path)] def _parse_locustfile_path(path: str) -> list[str]: parsed_paths = [] if is_url(path): # Download the file and use the new path as locustfile parsed_paths.append(download_locustfile_from_url(path)) elif os.path.isdir(path): # Find all .py files in directory tree parsed_paths.extend(get_abspaths_in(path, extension=".py")) if not parsed_paths: sys.stderr.write(f"Could not find any locustfiles in directory '{path}'") sys.exit(1) else: # If file exists add the abspath if os.path.exists(path) and path.endswith(".py"): parsed_paths.append(os.path.abspath(path)) else: note_about_file_endings = "Ensure your locustfile ends with '.py' or is a directory with locustfiles. " sys.stderr.write(f"Could not find '{path}'. {note_about_file_endings}See --help for available options.\n") sys.exit(1) return parsed_paths def download_locustfile_from_url(url: str) -> str: """ Attempt to download and save locustfile from url. Returns path to downloaded file. """ try: response = requests.get(url) except requests.exceptions.RequestException as e: sys.stderr.write(f"Failed to get locustfile from: {url}. Exception: {e}") sys.exit(1) else: try: # Check if response is valid python code ast.parse(response.text) except SyntaxError: sys.stderr.write( f"Failed to get locustfile from: {url}. Response was not valid python code: '{response.text[:100]}'" ) sys.exit(1) with open(os.path.join(tempfile.gettempdir(), urlparse(url).path.split("/")[-1]), "w") as locustfile: locustfile.write(response.text) atexit.register(exit_handler, locustfile.name) return locustfile.name def get_empty_argument_parser(add_help=True, default_config_files=DEFAULT_CONFIG_FILES) -> LocustArgumentParser: parser = LocustArgumentParser( default_config_files=default_config_files, config_file_parser_class=LocustConfigParser, add_env_var_help=False, add_config_file_help=False, add_help=add_help, formatter_class=configargparse.RawDescriptionHelpFormatter, usage=configargparse.SUPPRESS, description=textwrap.dedent( """ Usage: locust [options] [UserClass ...] """ ), epilog="""Examples: locust -f my_test.py -H https://www.example.com locust --headless -u 100 -t 20m --processes 4 MyHttpUser AnotherUser locust --headless -u 100 -r 10 -t 50 --print-stats --html "test_report_{u}_{r}_{t}.html" (The above run would generate an html file with the name "test_report_100_10_50.html") See documentation for more details, including how to set options using a file or environment variables: https://docs.locust.io/en/stable/configuration.html""", ) parser.add_argument( "-f", "--locustfile", metavar="", default="locustfile.py", help="The Python file or module that contains your test, e.g. 'my_test.py'. Accepts multiple comma-separated .py files, a package name/directory or a url to a remote locustfile. Defaults to 'locustfile.py'.", env_var="LOCUST_LOCUSTFILE", ) parser.add_argument( "--config", is_config_file_arg=True, help="File to read additional configuration from. See https://docs.locust.io/en/stable/configuration.html#configuration-file", metavar="", ) return parser def download_locustfile_from_master(master_host: str, master_port: int) -> str: client_id = socket.gethostname() + "_download_locustfile_" + uuid4().hex tempclient = zmqrpc.Client(master_host, master_port, client_id) got_reply = False def ask_for_locustfile(): while not got_reply: tempclient.send(Message("locustfile", {"version": version}, client_id)) gevent.sleep(1) def log_warning(): gevent.sleep(10) while not got_reply: sys.stderr.write("Waiting to connect to master to receive locustfile...\n") gevent.sleep(60) def wait_for_reply(): return tempclient.recv() gevent.spawn(ask_for_locustfile) gevent.spawn(log_warning) try: # wait same time as for client_ready ack. not that it is really relevant... msg = gevent.spawn(wait_for_reply).get(timeout=runners.CONNECT_TIMEOUT * runners.CONNECT_RETRY_COUNT) got_reply = True except gevent.Timeout: sys.stderr.write( f"Got no locustfile response from master, gave up after {runners.CONNECT_TIMEOUT * runners.CONNECT_RETRY_COUNT}s\n" ) sys.exit(1) if msg.type != "locustfile": sys.stderr.write(f"Got wrong message type from master {msg.type}\n") sys.exit(1) if "error" in msg.data: sys.stderr.write(f"Got error from master: {msg.data['error']}\n") sys.exit(1) tempclient.close() return msg.data.get("locustfiles", []) def parse_locustfile_option(args=None) -> tuple[argparse.Namespace, list[str]]: """ Construct a command line parser that is only used to parse the -f argument so that we can import the test scripts in case any of them adds additional command line arguments to the parser Returns: parsed_paths (List): List of locustfile paths """ parser = get_empty_argument_parser(add_help=False) parser.add_argument( "-h", "--help", action="store_true", default=False, ) parser.add_argument( "--version", "-V", action="store_true", default=False, ) # the following arguments are only used for downloading the locustfile from master parser.add_argument( "--worker", action="store_true", env_var="LOCUST_MODE_WORKER", ) parser.add_argument( "--master", # this is just here to prevent argparse from giving the dreaded "ambiguous option: --master could match --master-host, --master-port" action="store_true", env_var="LOCUST_MODE_MASTER", ) parser.add_argument( "--master-host", default="127.0.0.1", env_var="LOCUST_MASTER_NODE_HOST", ) parser.add_argument( "--master-port", type=int, default=5557, env_var="LOCUST_MASTER_NODE_PORT", ) options, _ = parser.parse_known_args(args=args) if options.help or options.version: # if --help or --version is specified we'll call parse_args which will print the help/version message get_parser().parse_args(args=args) return options def get_locustfiles_locally(options): if options.locustfile == "-": locustfile_list = retrieve_locustfiles_from_master(options) else: locustfile_list = [f.strip() for f in options.locustfile.split(",")] return parse_locustfile_paths(locustfile_list) def parse_locustfiles_from_master(locustfile_sources) -> list[str]: locustfiles = [] for source in locustfile_sources: if "contents" in source: filename = source["filename"] file_contents = source["contents"] with open(os.path.join(tempfile.gettempdir(), filename), "w", encoding="utf-8") as locustfile: locustfile.write(file_contents) locustfiles.append(locustfile.name) else: locustfiles.append(source) return locustfiles def retrieve_locustfiles_from_master(options) -> list[str]: if not options.worker: sys.stderr.write( "locustfile was set to '-' (meaning to download from master) but --worker was not specified.\n" ) sys.exit(1) # having this in argument_parser module is a bit weird, but it needs to be done early locustfile_sources = download_locustfile_from_master(options.master_host, options.master_port) return parse_locustfiles_from_master(locustfile_sources) # A hack for setting up an action that raises ArgumentError with configurable error messages. # This is meant to be used to immediately block use of deprecated arguments with some helpful messaging. def raise_argument_type_error(err_msg): class ErrorRaisingAction(configargparse.Action): def __call__(self, parser, namespace, values, option_string=None): raise configargparse.ArgumentError(self, err_msg) return ErrorRaisingAction # Definitions for some "types" to use with the arguments def timespan(time_str) -> int: try: return parse_timespan(time_str) except ValueError as e: raise configargparse.ArgumentTypeError(str(e)) def positive_integer(string) -> int: try: value = int(string) except ValueError: raise configargparse.ArgumentTypeError(f"invalid int value: '{string}'") if value < 1: raise configargparse.ArgumentTypeError( f"Invalid --expect-workers argument ({value}), must be a positive number" ) return value def gt_zero(t): def checker(value): v = t(value) if v <= 0: raise argparse.ArgumentTypeError("must be > 0") return v return checker def json_user_config(string): try: if string.endswith(".json"): with open(string) as file: user_config = json.load(file) else: user_config = json.loads(string) if not isinstance(user_config, list): user_config = [user_config] for config in user_config: if "user_class_name" not in config: raise configargparse.ArgumentTypeError("The user config must specify a user_class_name") return user_config except json.decoder.JSONDecodeError as e: raise configargparse.ArgumentTypeError(f"The --config-users argument must be a valid JSON string or file: {e}") except FileNotFoundError as e: raise configargparse.ArgumentTypeError(str(e)) def setup_parser_arguments(parser): """ Setup command-line options Takes a configargparse.ArgumentParser as argument and calls it's add_argument for each of the supported arguments """ parser._optionals.title = "Common options" parser.add_argument( "-H", "--host", metavar="", help="Host to load test, in the following format: https://www.example.com", env_var="LOCUST_HOST", ) parser.add_argument( "-u", "--users", type=int, metavar="", dest="num_users", help="Peak number of concurrent Locust users. Primarily used together with --headless or --autostart. Can be changed during a test by keyboard inputs w, W (spawn 1, 10 users) and s, S (stop 1, 10 users)", env_var="LOCUST_USERS", ) parser.add_argument( "-r", "--spawn-rate", type=gt_zero(float), metavar="", help="Rate to spawn users at (users per second). Primarily used together with --headless or --autostart", env_var="LOCUST_SPAWN_RATE", ) parser.add_argument( "-t", "--run-time", metavar="