Repository: artilleryio/artillery Branch: main Commit: 5ccaa8bb39c5 Files: 770 Total size: 27.7 MB Directory structure: gitextract_y554pkx1/ ├── .artilleryrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── codeql-analysis.yml │ ├── create-release-pr.yml │ ├── docker-ecs-worker-image.yml │ ├── docker-publish-artillery.yml │ ├── examples.yml │ ├── npm-publish-all-packages-canary.yml │ ├── npm-publish-all-packages.yml │ ├── npm-publish-artillery-engine-posthog.yml │ ├── npm-publish-artillery-plugin-memory-inspector.yml │ ├── npm-publish-artillery-types.yml │ ├── npm-publish-specific-package.yml │ ├── run-aws-tests-on-pr.yml │ ├── run-distributed-tests.yml │ ├── run-tests-windows.yml │ ├── run-tests.yml │ ├── s3-publish-cf-templates.yml │ └── scripts/ │ ├── get-all-packages-by-name.js │ ├── get-tests-in-package-location.js │ ├── npm-command-retry.sh │ └── replace-package-versions.js ├── .gitignore ├── .npmignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE-BSL.txt ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── biome.json ├── commitlint.config.js ├── examples/ │ ├── README.md │ ├── artillery-engine-example/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── example.yaml │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ └── index.js │ ├── artillery-plugin-hello-world/ │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── test.yml │ ├── automated-checks/ │ │ ├── README.md │ │ └── load-test-with-automated-checks.yml │ ├── browser-load-testing-playwright/ │ │ ├── README.md │ │ ├── browser-load-test.ts │ │ ├── browser-load-test.yml │ │ ├── browser-smoke-test.ts │ │ ├── browser-smoke-test.yml │ │ ├── browser-test-with-steps.yml │ │ ├── flows.js │ │ └── pages.csv │ ├── browser-playwright-reuse-authentication/ │ │ ├── README.md │ │ ├── flow.js │ │ ├── package.json │ │ ├── scenario.yml │ │ └── storage.json │ ├── browser-playwright-reuse-typescript/ │ │ ├── README.md │ │ ├── e2e/ │ │ │ ├── .gitignore │ │ │ ├── helpers/ │ │ │ │ └── index.ts │ │ │ ├── playwright.config.ts │ │ │ └── tests/ │ │ │ └── get-issues.spec.ts │ │ ├── package.json │ │ └── performance/ │ │ ├── processor.ts │ │ └── search-for-ts-doc.yml │ ├── cicd/ │ │ ├── README.md │ │ ├── aws-codebuild/ │ │ │ ├── README.md │ │ │ ├── buildspec.yml │ │ │ └── tests/ │ │ │ └── performance/ │ │ │ └── socket-io.yml │ │ ├── azure-devops/ │ │ │ ├── README.md │ │ │ ├── azure-pipelines.yml │ │ │ └── tests/ │ │ │ └── performance/ │ │ │ └── socket-io.yml │ │ ├── circleci/ │ │ │ ├── .circleci/ │ │ │ │ └── config.yml │ │ │ ├── README.md │ │ │ └── tests/ │ │ │ └── performance/ │ │ │ └── socket-io.yml │ │ ├── github-actions/ │ │ │ ├── .github/ │ │ │ │ └── workflows/ │ │ │ │ └── load-test.yml │ │ │ ├── README.md │ │ │ └── tests/ │ │ │ └── performance/ │ │ │ └── socket-io.yml │ │ ├── gitlab-ci-cd/ │ │ │ ├── .gitlab-ci.yml │ │ │ ├── README.md │ │ │ └── tests/ │ │ │ └── performance/ │ │ │ └── socket-io.yml │ │ └── jenkins/ │ │ ├── Jenkinsfile │ │ ├── README.md │ │ └── tests/ │ │ └── performance/ │ │ └── socket-io.yml │ ├── functional-testing-with-expect-plugin/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── functional-load-tests.yml │ │ └── package.json │ ├── generating-vu-tokens/ │ │ ├── README.md │ │ ├── auth-with-token.yml │ │ └── helpers.js │ ├── graphql-api-server/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── graphql.yaml │ │ ├── package.json │ │ └── prisma/ │ │ ├── migrations/ │ │ │ ├── 20211005051218_init/ │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── http-file-uploads/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── file-uploads.yml │ │ ├── package.json │ │ └── uploads/ │ │ └── .keep │ ├── http-metrics-by-endpoint/ │ │ └── endpoint-metrics.yml │ ├── http-set-custom-header/ │ │ ├── README.md │ │ ├── helpers.js │ │ └── set-header.yml │ ├── http-socketio-server/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── data/ │ │ │ └── movies.json │ │ ├── http-socket.yml │ │ ├── http.js │ │ ├── package.json │ │ └── socketio.js │ ├── k8s-testing-with-kubectl-artillery/ │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── README.md │ │ ├── app.js │ │ ├── data/ │ │ │ └── movies.json │ │ ├── hack/ │ │ │ └── kind/ │ │ │ └── kind-with-registry.sh │ │ ├── http.js │ │ ├── k8s-deploy.yaml │ │ └── package.json │ ├── multiple-scenario-specs/ │ │ ├── README.md │ │ ├── common-config.yml │ │ └── scenarios/ │ │ ├── armadillo.yml │ │ ├── dino.yml │ │ └── pony.yml │ ├── prometheus-grafana-dashboards/ │ │ ├── README.md │ │ ├── dashboard-http-metrics-1652971310916.json │ │ └── dashboard-vusers-metrics-1652971366368.json │ ├── refresh-auth-token/ │ │ ├── README.md │ │ ├── refresh.mjs │ │ └── refresh.yml │ ├── rpc-twirp-with-custom-function/ │ │ ├── README.md │ │ ├── test/ │ │ │ ├── processor.mjs │ │ │ └── scenario.yml │ │ └── twirp/ │ │ ├── package.json │ │ ├── protos/ │ │ │ ├── haberdasher.pb.js │ │ │ └── haberdasher.proto │ │ └── server/ │ │ ├── haberdasher/ │ │ │ └── index.js │ │ └── index.js │ ├── scenario-weights/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── package.json │ │ └── scenario-weights.yml │ ├── script-overrides/ │ │ ├── README.md │ │ └── test.yaml │ ├── soap-with-custom-function/ │ │ ├── README.md │ │ ├── package.json │ │ ├── processor.js │ │ ├── server/ │ │ │ ├── MyService.wsdl │ │ │ ├── app.js │ │ │ └── package.json │ │ └── soap.yml │ ├── socket-io/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ └── socket-io.yml │ ├── starter-kit/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── processors/ │ │ │ ├── _baseProcessor.js │ │ │ ├── sample_task_01.js │ │ │ └── sample_task_02.js │ │ ├── reports/ │ │ │ └── .gitkeep │ │ └── scenarios/ │ │ ├── sample_task_01.yaml │ │ ├── sample_task_02.yaml │ │ └── sample_task_03.yaml │ ├── table-driven-functional-tests/ │ │ ├── README.md │ │ ├── functional-test.yml │ │ ├── package.json │ │ └── request-response.csv │ ├── tracetest/ │ │ └── README.md │ ├── track-custom-metrics/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── custom-metrics.yml │ │ ├── metrics.js │ │ └── package.json │ ├── using-cookies/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.js │ │ ├── cookies.yml │ │ └── package.json │ ├── using-data-from-csv/ │ │ ├── csv/ │ │ │ └── urls.csv │ │ └── website-test.yml │ ├── using-data-from-redis/ │ │ ├── README.md │ │ ├── package.json │ │ ├── processor.js │ │ ├── scenario.yml │ │ └── scripts/ │ │ └── seed-redis-with-users.js │ └── websockets/ │ ├── .gitignore │ ├── README.md │ ├── app.js │ ├── my-functions.js │ ├── package.json │ └── test.yml ├── package.json ├── packages/ │ ├── artillery/ │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── README.md │ │ ├── bin/ │ │ │ ├── run │ │ │ └── run.cmd │ │ ├── console-reporter.js │ │ ├── doc/ │ │ │ └── CLA.md │ │ ├── lib/ │ │ │ ├── artillery-global.js │ │ │ ├── cli/ │ │ │ │ ├── banner.js │ │ │ │ ├── common-flags.js │ │ │ │ └── hooks/ │ │ │ │ └── version.js │ │ │ ├── cmds/ │ │ │ │ ├── dino.js │ │ │ │ ├── quick.js │ │ │ │ ├── report.js │ │ │ │ ├── run-aci.js │ │ │ │ ├── run-fargate.js │ │ │ │ ├── run-lambda.js │ │ │ │ └── run.js │ │ │ ├── console-capture.js │ │ │ ├── console-reporter.js │ │ │ ├── create-bom/ │ │ │ │ ├── built-in-plugins.js │ │ │ │ └── create-bom.js │ │ │ ├── dispatcher.js │ │ │ ├── dist.js │ │ │ ├── index.js │ │ │ ├── launch-platform.js │ │ │ ├── load-plugins.js │ │ │ ├── platform/ │ │ │ │ ├── aws/ │ │ │ │ │ ├── aws-cloudwatch.js │ │ │ │ │ ├── aws-create-sqs-queue.js │ │ │ │ │ ├── aws-ensure-s3-bucket-exists.js │ │ │ │ │ ├── aws-get-account-id.js │ │ │ │ │ ├── aws-get-bucket-region.js │ │ │ │ │ ├── aws-get-credentials.js │ │ │ │ │ ├── aws-get-default-region.js │ │ │ │ │ ├── aws-whoami.js │ │ │ │ │ ├── constants.js │ │ │ │ │ └── iam-cf-templates/ │ │ │ │ │ ├── aws-iam-fargate-cf-template.yml │ │ │ │ │ ├── aws-iam-lambda-cf-template.yml │ │ │ │ │ ├── gh-oidc-fargate.yml │ │ │ │ │ └── gh-oidc-lambda.yml │ │ │ │ ├── aws-ecs/ │ │ │ │ │ ├── ecs.js │ │ │ │ │ ├── legacy/ │ │ │ │ │ │ ├── aws-util.js │ │ │ │ │ │ ├── bom.js │ │ │ │ │ │ ├── constants.js │ │ │ │ │ │ ├── create-s3-client.js │ │ │ │ │ │ ├── create-test.js │ │ │ │ │ │ ├── errors.js │ │ │ │ │ │ ├── find-public-subnets.js │ │ │ │ │ │ ├── plugins/ │ │ │ │ │ │ │ ├── artillery-plugin-inspect-script/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── artillery-plugin-sqs-reporter/ │ │ │ │ │ │ │ ├── azure-aqs.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── plugins.js │ │ │ │ │ │ ├── run-cluster.js │ │ │ │ │ │ ├── sqs-reporter.js │ │ │ │ │ │ ├── tags.js │ │ │ │ │ │ ├── test-run-status.js │ │ │ │ │ │ ├── time.js │ │ │ │ │ │ └── util.js │ │ │ │ │ └── worker/ │ │ │ │ │ ├── Dockerfile │ │ │ │ │ ├── helpers.sh │ │ │ │ │ └── loadgen-worker │ │ │ │ ├── aws-lambda/ │ │ │ │ │ ├── dependencies.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── lambda-handler/ │ │ │ │ │ │ ├── a9-handler-dependencies.js │ │ │ │ │ │ ├── a9-handler-helpers.js │ │ │ │ │ │ ├── a9-handler-index.js │ │ │ │ │ │ └── package.json │ │ │ │ │ └── prices.js │ │ │ │ ├── az/ │ │ │ │ │ ├── aci.js │ │ │ │ │ ├── aqs-queue-consumer.js │ │ │ │ │ └── regions.js │ │ │ │ ├── cloud/ │ │ │ │ │ ├── api.js │ │ │ │ │ ├── cloud.js │ │ │ │ │ └── http-client.js │ │ │ │ ├── local/ │ │ │ │ │ ├── artillery-worker-local.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── worker.js │ │ │ │ └── worker-states.js │ │ │ ├── queue-consumer/ │ │ │ │ └── index.js │ │ │ ├── stash.js │ │ │ ├── telemetry.js │ │ │ ├── util/ │ │ │ │ ├── await-on-ee.js │ │ │ │ ├── generate-id.js │ │ │ │ ├── parse-tag-string.js │ │ │ │ ├── prepare-test-execution-plan.js │ │ │ │ ├── sleep.js │ │ │ │ └── validate-script.js │ │ │ ├── util.js │ │ │ └── utils-config.js │ │ ├── man/ │ │ │ ├── artillery.1 │ │ │ └── artillery.1.md │ │ ├── package.json │ │ ├── test/ │ │ │ ├── cli/ │ │ │ │ ├── async-hooks-esm.test.js │ │ │ │ ├── command-report.test.js │ │ │ │ ├── command-run.test.js │ │ │ │ ├── custom-plugin.test.js │ │ │ │ ├── errors-and-warnings.test.js │ │ │ │ ├── run-smoke.test.js │ │ │ │ ├── run-typescript.test.js │ │ │ │ ├── suggested-exit-codes.test.js │ │ │ │ ├── unknown-engine.test.js │ │ │ │ └── variables-from-external-files.test.js │ │ │ ├── cloud-e2e/ │ │ │ │ ├── fargate/ │ │ │ │ │ ├── bom.test.js │ │ │ │ │ ├── cloud-api-key.test.js │ │ │ │ │ ├── cw-adot.test.js │ │ │ │ │ ├── dd-adot.test.js │ │ │ │ │ ├── ensure-plugin.test.js │ │ │ │ │ ├── expect-plugin.test.js │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ ├── adot/ │ │ │ │ │ │ │ ├── adot-cloudwatch.yml │ │ │ │ │ │ │ ├── adot-dd-pass.yml │ │ │ │ │ │ │ ├── flow.js │ │ │ │ │ │ │ └── helpers.js │ │ │ │ │ │ ├── cli-exit-conditions/ │ │ │ │ │ │ │ ├── with-expect-ensure.yml │ │ │ │ │ │ │ └── with-expect.yml │ │ │ │ │ │ ├── cli-kitchen-sink/ │ │ │ │ │ │ │ ├── kitchen-sink-env │ │ │ │ │ │ │ └── kitchen-sink.yml │ │ │ │ │ │ ├── cloud-api-key-load/ │ │ │ │ │ │ │ └── scenario.yml │ │ │ │ │ │ ├── heartbeat/ │ │ │ │ │ │ │ └── heartbeat.yml │ │ │ │ │ │ ├── large-output/ │ │ │ │ │ │ │ ├── lots-of-output.yml │ │ │ │ │ │ │ └── processor.js │ │ │ │ │ │ ├── memory-hog/ │ │ │ │ │ │ │ ├── memory-hog.yml │ │ │ │ │ │ │ └── processor.js │ │ │ │ │ │ ├── mixed-hierarchy/ │ │ │ │ │ │ │ ├── code/ │ │ │ │ │ │ │ │ ├── functions.js │ │ │ │ │ │ │ │ ├── lib/ │ │ │ │ │ │ │ │ │ └── signer.js │ │ │ │ │ │ │ │ ├── meow.js │ │ │ │ │ │ │ │ └── set-url.js │ │ │ │ │ │ │ ├── config/ │ │ │ │ │ │ │ │ ├── config-no-file-uploads.yml │ │ │ │ │ │ │ │ └── config.yml │ │ │ │ │ │ │ ├── data/ │ │ │ │ │ │ │ │ └── variables.csv │ │ │ │ │ │ │ ├── package.json │ │ │ │ │ │ │ └── scenarios/ │ │ │ │ │ │ │ ├── mixed-hierarchy-dino.yml │ │ │ │ │ │ │ └── mixed-hierarchy-pony.yml │ │ │ │ │ │ ├── simple-bom/ │ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ │ ├── deps/ │ │ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ │ │ ├── data/ │ │ │ │ │ │ │ │ │ ├── lists.json │ │ │ │ │ │ │ │ │ ├── names-local.csv │ │ │ │ │ │ │ │ │ ├── names-prod.csv │ │ │ │ │ │ │ │ │ ├── names-test.csv │ │ │ │ │ │ │ │ │ └── user-data.csv │ │ │ │ │ │ │ │ ├── dummy-util.js │ │ │ │ │ │ │ │ ├── functions.js │ │ │ │ │ │ │ │ ├── local-mod-dir/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ └── not-included.json │ │ │ │ │ │ │ └── simple-bom.yml │ │ │ │ │ │ ├── ts-external-pkg/ │ │ │ │ │ │ │ ├── package.json │ │ │ │ │ │ │ ├── processor.ts │ │ │ │ │ │ │ └── with-external-foreign-pkg.yml │ │ │ │ │ │ └── uses-ensure/ │ │ │ │ │ │ └── with-ensure.yaml │ │ │ │ │ ├── heartbeat.test.js │ │ │ │ │ ├── memory.test.js │ │ │ │ │ ├── misc.test.js │ │ │ │ │ └── processors.test.js │ │ │ │ └── lambda/ │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── dotenv/ │ │ │ │ │ │ ├── .env-test │ │ │ │ │ │ ├── dotenv-test.yml │ │ │ │ │ │ └── processor.js │ │ │ │ │ ├── quick-loop-with-csv/ │ │ │ │ │ │ ├── blitz.yml │ │ │ │ │ │ ├── config.yml │ │ │ │ │ │ ├── helpers.js │ │ │ │ │ │ └── test.csv │ │ │ │ │ └── ts-external-pkg/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── processor.ts │ │ │ │ │ └── with-external-foreign-pkg.yml │ │ │ │ ├── lambda-bom.test.js │ │ │ │ ├── lambda-dotenv.test.js │ │ │ │ ├── lambda-ensure.test.js │ │ │ │ ├── lambda-expect.test.js │ │ │ │ └── lambda-smoke.test.js │ │ │ ├── data/ │ │ │ │ ├── calc-test-data-1.csv │ │ │ │ ├── calc-test-data-2.csv │ │ │ │ ├── geometric.json │ │ │ │ ├── multi-period-local-report.json │ │ │ │ ├── response-times-histograms.json │ │ │ │ └── ssms-buckets.json │ │ │ ├── helpers/ │ │ │ │ ├── expectations.js │ │ │ │ ├── index.js │ │ │ │ └── sleep.js │ │ │ ├── index.js │ │ │ ├── integration/ │ │ │ │ └── core/ │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── http-file-upload-processor.js │ │ │ │ │ └── http-file-upload.yml │ │ │ │ └── http-file-upload.test.js │ │ │ ├── lib/ │ │ │ │ ├── index.js │ │ │ │ ├── run.sh │ │ │ │ ├── test_util.js │ │ │ │ └── validate-script.test.js │ │ │ ├── plugins/ │ │ │ │ ├── artillery-plugin-dummy-csv-logger/ │ │ │ │ │ └── index.js │ │ │ │ └── artillery-plugin-httphooks/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── publish-metrics/ │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── flow.js │ │ │ │ │ ├── helpers.js │ │ │ │ │ ├── http-trace.yml │ │ │ │ │ └── playwright-trace.yml │ │ │ │ └── tracing/ │ │ │ │ ├── http-trace-assertions.js │ │ │ │ ├── http-trace.test.js │ │ │ │ ├── playwright-trace-assertions.js │ │ │ │ └── playwright-trace.test.js │ │ │ ├── runner.sh │ │ │ ├── scripts/ │ │ │ │ ├── environments.yaml │ │ │ │ ├── environments2.json │ │ │ │ ├── gh_215_add_token.json │ │ │ │ ├── hello.json │ │ │ │ ├── hello_config.json │ │ │ │ ├── hello_plugin.json │ │ │ │ ├── hello_with_xpath.json │ │ │ │ ├── http/ │ │ │ │ │ ├── async_function.json │ │ │ │ │ ├── error_code_function.json │ │ │ │ │ ├── error_message_function.json │ │ │ │ │ ├── processors.js │ │ │ │ │ ├── simple_function.json │ │ │ │ │ └── undefined_function.json │ │ │ │ ├── local-urls.csv │ │ │ │ ├── multiple_payloads.json │ │ │ │ ├── pets.csv │ │ │ │ ├── pets.txt │ │ │ │ ├── processor.js │ │ │ │ ├── ramp-regression-1682.json │ │ │ │ ├── ramp.json │ │ │ │ ├── report.json │ │ │ │ ├── scenario-async-esm-hooks/ │ │ │ │ │ ├── helpers.mjs │ │ │ │ │ └── test.yml │ │ │ │ ├── scenario-cli-variables/ │ │ │ │ │ ├── processor.js │ │ │ │ │ ├── scenario-with-other-nested-config.yml │ │ │ │ │ └── scenario-with-variables.yml │ │ │ │ ├── scenario-config-different-folder/ │ │ │ │ │ ├── __processor__/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── config-processor-backward-compatibility.yml │ │ │ │ │ │ └── config.yml │ │ │ │ │ └── scenario.yml │ │ │ │ ├── scenario-named/ │ │ │ │ │ └── scenario.yml │ │ │ │ ├── scenario-payload-with-envs/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── artillery-config.yml │ │ │ │ │ ├── data/ │ │ │ │ │ │ └── my-data.csv │ │ │ │ │ └── scenario.yml │ │ │ │ ├── scenario-with-custom-plugin/ │ │ │ │ │ ├── custom-plugin.yml │ │ │ │ │ └── processor.js │ │ │ │ ├── scenario-with-parallel/ │ │ │ │ │ ├── processor.js │ │ │ │ │ └── scenario.yml │ │ │ │ ├── scenarios-typescript/ │ │ │ │ │ ├── error.yml │ │ │ │ │ ├── lodash.yml │ │ │ │ │ └── processor.ts │ │ │ │ ├── single_payload.json │ │ │ │ ├── single_payload_object.json │ │ │ │ ├── single_payload_options.json │ │ │ │ ├── test-calc-server.yml │ │ │ │ ├── test-suggest-exit-code.js │ │ │ │ ├── test-suggest-exit-code.yml │ │ │ │ ├── unknown_engine.json │ │ │ │ ├── urls.csv │ │ │ │ ├── wildcard_processor.js │ │ │ │ ├── wildcard_socketio.json │ │ │ │ ├── with-dotenv/ │ │ │ │ │ ├── my-vars │ │ │ │ │ └── with-dotenv.yml │ │ │ │ └── with-process-env/ │ │ │ │ ├── processor.js │ │ │ │ ├── with-env.yml │ │ │ │ └── with-processEnvironment.yml │ │ │ ├── targets/ │ │ │ │ ├── calc-server.js │ │ │ │ ├── gh_215_target.js │ │ │ │ ├── http-file-upload-server.js │ │ │ │ └── targetServer.js │ │ │ ├── tinyproxy.conf │ │ │ └── unit/ │ │ │ ├── before_after_hooks.test.js │ │ │ ├── create-bom.test.js │ │ │ ├── dist.test.js │ │ │ ├── fargate-bom.test.js │ │ │ ├── processor.js │ │ │ ├── ssms-basic.test.js │ │ │ ├── ssms-buckets.test.js │ │ │ ├── ssms-multi-process.test.js │ │ │ └── ssms-worker.js │ │ ├── types.d.ts │ │ └── util.js │ ├── artillery-engine-playwright/ │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ ├── fargate.aws.js │ │ ├── fixtures/ │ │ │ ├── processor.js │ │ │ ├── processor.ts │ │ │ ├── pw-acceptance-ts.yml │ │ │ ├── pw-acceptance.yml │ │ │ └── pw-url-normalization.yml │ │ └── index.test.js │ ├── artillery-engine-posthog/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── example.yml │ │ │ ├── example_js_logic.yml │ │ │ └── logic.js │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ └── index.js │ ├── artillery-plugin-apdex/ │ │ ├── LICENSE │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ ├── fixtures/ │ │ │ ├── processor.js │ │ │ └── scenario.yml │ │ └── index.spec.js │ ├── artillery-plugin-ensure/ │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ ├── test/ │ │ │ ├── fixtures/ │ │ │ │ ├── processor.js │ │ │ │ ├── scenario-custom-metrics.yml │ │ │ │ └── scenario.yml │ │ │ ├── index.spec.js │ │ │ └── utils.unit.js │ │ └── utils.js │ ├── artillery-plugin-expect/ │ │ ├── .circleci/ │ │ │ └── config.yml │ │ ├── .gitignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── expectations.js │ │ │ └── formatters.js │ │ ├── package.json │ │ └── test/ │ │ ├── cdn-test.yml │ │ ├── index.js │ │ ├── lib/ │ │ │ └── formatters.js │ │ ├── mock-pets-server.yaml │ │ ├── parallel.yml │ │ ├── pets-fail-test.yaml │ │ ├── pets-test.yaml │ │ ├── run.sh │ │ └── urls.csv │ ├── artillery-plugin-fake-data/ │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── index.js │ │ └── package.json │ ├── artillery-plugin-memory-inspector/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ ├── fixtures/ │ │ │ ├── myProcessor.js │ │ │ └── scenario.yml │ │ ├── index.spec.js │ │ ├── server/ │ │ │ └── server.js │ │ └── util.js │ ├── artillery-plugin-metrics-by-endpoint/ │ │ ├── README.md │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ ├── fixtures/ │ │ │ ├── scenario-parallel.yml │ │ │ ├── scenario-templated-url.yml │ │ │ └── scenario.yml │ │ ├── index.spec.js │ │ └── index.unit.js │ ├── artillery-plugin-publish-metrics/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── cloudwatch.js │ │ │ ├── datadog.js │ │ │ ├── dynatrace/ │ │ │ │ └── index.js │ │ │ ├── mixpanel.js │ │ │ ├── newrelic/ │ │ │ │ ├── README.md │ │ │ │ └── index.js │ │ │ ├── open-telemetry/ │ │ │ │ ├── exporters.js │ │ │ │ ├── file-span-exporter.js │ │ │ │ ├── index.js │ │ │ │ ├── metrics.js │ │ │ │ ├── outlier-detection-processor.js │ │ │ │ ├── tracing/ │ │ │ │ │ ├── base.js │ │ │ │ │ ├── http.js │ │ │ │ │ └── playwright.js │ │ │ │ └── translators/ │ │ │ │ ├── vendor-adot.js │ │ │ │ └── vendor-otel.js │ │ │ ├── prometheus.js │ │ │ ├── splunk/ │ │ │ │ ├── README.md │ │ │ │ └── index.js │ │ │ └── util.js │ │ ├── package.json │ │ └── test/ │ │ ├── config-agent.yaml │ │ ├── config-api.yaml │ │ ├── config-dynatrace.yaml │ │ ├── config-honeycomb.yaml │ │ ├── config-influxdb-statsd.yaml │ │ ├── config-mixpanel.yaml │ │ ├── config-newrelic-api.yaml │ │ ├── config-prometheus.yaml │ │ ├── config-splunk.yaml │ │ ├── config-statsd.yaml │ │ ├── index.js │ │ ├── scenario.yaml │ │ └── unit/ │ │ ├── adot-translators.js │ │ └── tracing.js │ ├── artillery-plugin-slack/ │ │ ├── LICENSE.txt │ │ ├── index.js │ │ └── package.json │ ├── commons/ │ │ ├── engine_util.js │ │ ├── index.js │ │ ├── jitter.js │ │ └── package.json │ ├── core/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── index.js │ │ ├── lib/ │ │ │ ├── engine_http.js │ │ │ ├── engine_socketio.js │ │ │ ├── engine_ws.js │ │ │ ├── is-idle-phase.js │ │ │ ├── phases.js │ │ │ ├── readers.js │ │ │ ├── runner.js │ │ │ ├── ssms.js │ │ │ └── weighted-pick.js │ │ ├── package.json │ │ └── test/ │ │ ├── acceptance/ │ │ │ ├── arrivals.test.js │ │ │ ├── basic-auth.test.js │ │ │ ├── capture-ws.test.js │ │ │ ├── capture.test.js │ │ │ ├── conditional-requests.test.js │ │ │ ├── config-variables.test.js │ │ │ ├── cookies-http.test.js │ │ │ ├── cookies-socketio.test.js │ │ │ ├── headers.test.js │ │ │ ├── loop.test.js │ │ │ ├── misc/ │ │ │ │ ├── helper.js │ │ │ │ ├── http.test.js │ │ │ │ ├── large-payload.test.js │ │ │ │ ├── multiple-phases.test.js │ │ │ │ ├── socketio-with-args.test.js │ │ │ │ ├── socketio-with-http.test.js │ │ │ │ ├── socketio.test.js │ │ │ │ ├── ws-proxy.test.js │ │ │ │ └── ws.test.js │ │ │ ├── multiple-payloads.test.js │ │ │ ├── multiple-runners.test.js │ │ │ ├── parallel.test.js │ │ │ ├── probability.test.js │ │ │ ├── think-time.test.js │ │ │ ├── tls.test.js │ │ │ ├── ws/ │ │ │ │ ├── scripts/ │ │ │ │ │ ├── subprotocols.json │ │ │ │ │ └── ws-tls.json │ │ │ │ ├── ws-subprotocols.test.js │ │ │ │ └── ws-tls.test.js │ │ │ └── ws-engine.test.js │ │ ├── plugins/ │ │ │ ├── normal_plugin/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ └── packaged_plugin/ │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── quarantine/ │ │ │ ├── concurrent-requests.test.js │ │ │ ├── test_config_plugin_package.js │ │ │ ├── test_engine_http.js │ │ │ └── test_environments.js │ │ ├── scripts/ │ │ │ ├── after_test.json │ │ │ ├── all_features.json │ │ │ ├── arrival_phases.json │ │ │ ├── arrival_phases_time_format.json │ │ │ ├── artillery_plugin.json │ │ │ ├── before_test.json │ │ │ ├── captures-regexp.json │ │ │ ├── concurrent_requests_arrival_count.json │ │ │ ├── concurrent_requests_arrival_rate.json │ │ │ ├── concurrent_requests_multiple_phases.json │ │ │ ├── concurrent_requests_ramp_to.json │ │ │ ├── config_variables.json │ │ │ ├── cookies.json │ │ │ ├── cookies_malformed_response.json │ │ │ ├── cookies_socketio.json │ │ │ ├── data/ │ │ │ │ ├── pets.csv │ │ │ │ └── urls.csv │ │ │ ├── defaults_cookies.json │ │ │ ├── express_socketio.json │ │ │ ├── generators_http.json │ │ │ ├── hello.json │ │ │ ├── hello_basic_auth.json │ │ │ ├── hello_environments.json │ │ │ ├── hello_socketio.json │ │ │ ├── hello_socketio_with_args.json │ │ │ ├── hello_ws.json │ │ │ ├── iftrue.json │ │ │ ├── large_payload.json │ │ │ ├── loop.json │ │ │ ├── loop_infinite.json │ │ │ ├── loop_nested_range.json │ │ │ ├── loop_range.json │ │ │ ├── multiple_payloads.json │ │ │ ├── multiple_phases.json │ │ │ ├── namespaces_socketio.json │ │ │ ├── no_defaults_cookies.json │ │ │ ├── parallel.json │ │ │ ├── plugin_packaged_inner.json │ │ │ ├── plugin_packaged_inner_override_outter.json │ │ │ ├── plugin_packaged_outer.json │ │ │ ├── plugin_statsd.json │ │ │ ├── probability.json │ │ │ ├── single_payload.json │ │ │ ├── thinks_http.json │ │ │ ├── tls-lax.json │ │ │ ├── tls-strict.json │ │ │ └── ws_proxy.json │ │ ├── targets/ │ │ │ ├── certs/ │ │ │ │ ├── private-key.pem │ │ │ │ └── public-cert.pem │ │ │ ├── express_socketio.js │ │ │ ├── simple.js │ │ │ ├── simple_socketio.js │ │ │ ├── simple_tls.js │ │ │ ├── simple_ws.js │ │ │ ├── socketio_args.js │ │ │ ├── ws_proxy.js │ │ │ └── ws_tls.js │ │ └── unit/ │ │ ├── context_functions.test.js │ │ ├── engine_http.test.js │ │ ├── engine_socketio.test.js │ │ ├── engine_ws.test.js │ │ ├── interpolation.test.js │ │ ├── large-json-payload-669kb.json │ │ ├── large-json-payload-7.2mb.json │ │ ├── phases.test.js │ │ ├── readers.test.js │ │ ├── templates.test.js │ │ └── util.test.js │ ├── skytrace/ │ │ ├── .editorconfig │ │ ├── LICENSE │ │ ├── README.md │ │ ├── asciiart-flow.yml │ │ ├── bin/ │ │ │ ├── dev │ │ │ ├── dev.cmd │ │ │ ├── run │ │ │ └── run.cmd │ │ ├── package.json │ │ ├── src/ │ │ │ ├── commands/ │ │ │ │ ├── ping.ts │ │ │ │ └── run.ts │ │ │ ├── index.ts │ │ │ └── telemetry.ts │ │ └── tsconfig.json │ └── types/ │ ├── .gitignore │ ├── LICENSE │ ├── definitions.ts │ ├── generate-schema.js │ ├── package.json │ ├── plugins/ │ │ └── expect.ts │ ├── schema/ │ │ ├── config/ │ │ │ └── phases.js │ │ ├── config.js │ │ ├── engines/ │ │ │ ├── common.js │ │ │ ├── http.js │ │ │ ├── playwright.js │ │ │ ├── socketio.js │ │ │ └── websocket.js │ │ ├── index.js │ │ ├── joi.helpers.js │ │ ├── plugins/ │ │ │ ├── apdex.js │ │ │ ├── ensure.js │ │ │ ├── expect.js │ │ │ ├── fake-data.js │ │ │ ├── metrics-by-endpoint.js │ │ │ ├── publish-metrics.js │ │ │ └── slack.js │ │ └── scenario.js │ ├── test/ │ │ ├── config.phases.test.ts │ │ ├── engine.arbitrary.test.ts │ │ ├── engine.http.test.ts │ │ ├── engine.socketio.test.ts │ │ ├── engine.websocket.test.ts │ │ ├── examples.test.ts │ │ ├── helpers.ts │ │ ├── plugin.expect.test.ts │ │ ├── simple.test.ts │ │ └── tsconfig.json │ ├── tsconfig.json │ └── tsconfig.schema.json ├── socket.yml └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .artilleryrc ================================================ { "logFilenameFormat": "[artillery_report_]YMMDD_HHmmSS[.json]" } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- Version info: ``` ``` Running this command: ``` ``` I expected to see this happen: *explanation* Instead, this happened: *explanation* Files being used: ``` ``` ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" exclude-paths: - "examples/**" ================================================ FILE: .github/pull_request_template.md ================================================ ## Description ## Pre-merge checklist **This is for use by the Artillery team. Please leave this in if you're contributing to Artillery.** - [ ] Does this require an update to the docs? - [ ] Does this require a changelog entry? ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '18 19 * * 3' jobs: analyze: name: Analyze runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 ================================================ FILE: .github/workflows/create-release-pr.yml ================================================ name: Create Release PR on: workflow_dispatch: permissions: contents: write pull-requests: write jobs: create_release_pr: runs-on: blacksmith-4vcpu-ubuntu-2404 env: # these are the packages we auto release (e.g. standalone pkgs like artillery/skytrace/types, and included dependencies like plugins) # some non-included dependencies are not released automatically (e.g. posthog, memory-inspector) # this list should be kept in sync with npm-publish-all-packages.yml PACKAGES_TO_RELEASE: "\ artillery-engine-playwright,\ artillery-plugin-apdex,\ artillery-plugin-ensure,\ artillery-plugin-expect,\ artillery-plugin-fake-data,\ artillery-plugin-metrics-by-endpoint,\ artillery-plugin-publish-metrics,\ artillery-plugin-slack,\ commons,\ core,\ artillery" steps: - name: Checkout code uses: actions/checkout@v3 - name: Replace package folder name with actual name from package.json run: | for package in $(echo $PACKAGES_TO_RELEASE | tr "," "\n"); do PACKAGE_NAME=$(node -e "console.log(require('./packages/$package/package.json').name)") PACKAGES_TO_RELEASE=${PACKAGES_TO_RELEASE/$package/$PACKAGE_NAME} echo "PACKAGES_TO_RELEASE=$PACKAGES_TO_RELEASE" >> "$GITHUB_ENV" done # all packages receive minor version bump, except for artillery which receives a patch version bump as convention - name: Update package versions run: | for package in $(echo ${{ env.PACKAGES_TO_RELEASE }} | tr "," "\n"); do if [ "$package" = "artillery" ]; then npm version patch --workspace $package else npm version minor --workspace $package fi done - name: Get new Artillery version run: | ARTILLERY_VERSION=$(node -e "console.log(require('./packages/artillery/package.json').version)") echo "ARTILLERY_VERSION=$ARTILLERY_VERSION" >> "$GITHUB_ENV" - name: Create branch run: | export BRANCH_NAME=release/artillery-v${{ env.ARTILLERY_VERSION }}-$(date '+%Y-%m-%d-%H-%M-%S') echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV" git checkout -b $BRANCH_NAME git push --set-upstream origin $BRANCH_NAME - name: Install dependencies run: npm ci - name: Add changes to commit run: git add . - name: Commit changes run: | git config --global user.name "${{ github.actor }}" git config --global user.email "${{ github.actor }}@users.noreply.github.com" RELEASE_COMMIT_MSG="ci: release v${{ env.ARTILLERY_VERSION }} artillery" >> "$GITHUB_ENV" git commit -m "ci: release v${{ env.ARTILLERY_VERSION }} artillery" git push - name: create pull request run: gh pr create -B main -H ${{ env.BRANCH_NAME }} --body 'Release v${{ env.ARTILLERY_VERSION }}. Created by Github action' --fill env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/docker-ecs-worker-image.yml ================================================ name: Build & publish ECS/Fargate worker image to ECR on: workflow_dispatch: inputs: SHOULD_BUILD_ARM64: description: 'Whether to build the ARM64 image.' type: boolean default: false workflow_call: inputs: COMMIT_SHA: description: 'Branch ref to checkout. Needed for pull_request_target to be able to pull correct ref.' type: string required: true USE_COMMIT_SHA_IN_VERSION: description: 'Whether to use the commit sha in building the pkg version of the image.' type: boolean SHOULD_BUILD_ARM64: description: 'Whether to build the ARM64 image.' type: boolean default: false secrets: ECR_WORKER_IMAGE_PUSH_ROLE_ARN: description: 'ARN of the IAM role to assume to push the image to ECR.' required: true permissions: id-token: write contents: read jobs: build_docker_image_amd64: runs-on: blacksmith-4vcpu-ubuntu-2404 env: # Set by the caller workflow, defaults to github.sha when not passed (e.g. workflow_dispatch against a branch) WORKER_VERSION: ${{ inputs.COMMIT_SHA || github.sha }} strategy: matrix: registry: [ public, private ] steps: - uses: actions/checkout@v3 with: ref: ${{ env.WORKER_VERSION }} fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Replace package version if: ${{ inputs.USE_COMMIT_SHA_IN_VERSION || false }} run: node .github/workflows/scripts/replace-package-versions.js env: COMMIT_SHA: ${{ env.WORKER_VERSION }} REPLACE_MAIN_VERSION_ONLY: true # we don't need to replace dependencies, as docker image builds using workspaces - name: Get Artillery version # we only want to tag with an actual version from pkg.json outside of PRs and manual dispatches # NOTE: can't check for refs/head/main because of pull_request_target used in some workflows if: github.event.pull_request == null && github.event_name != 'workflow_dispatch' run: | echo "WORKER_VERSION=$(node -e 'console.log(require("./packages/artillery/package.json").version)')" >> $GITHUB_ENV - name: Show git ref run: | echo GITHUB REF ${{ github.ref }} echo GITHUB PR HEAD SHA ${{ github.event.pull_request.head.sha }} echo GITHUB SHA ${{ github.sha }} echo WORKER_VERSION ENV ${{ env.WORKER_VERSION }} - name: Configure AWS Credentials (Public ECR) if: matrix.registry == 'public' uses: aws-actions/configure-aws-credentials@v1 with: aws-region: us-east-1 audience: sts.amazonaws.com role-to-assume: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} role-session-name: OIDCSession mask-aws-account-id: true - name: Login to Amazon (Public ECR) if: matrix.registry == 'public' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v1 with: registry-type: public - name: Build the Docker image (Public ECR) if: matrix.registry == 'public' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-x86_64 run: | docker build . --platform linux/amd64 --build-arg="WORKER_VERSION=${{ env.WORKER_VERSION }}" --tag public.ecr.aws/d8a4z9o5/artillery-worker:${{ env.DOCKER_TAG }} -f ./packages/artillery/lib/platform/aws-ecs/worker/Dockerfile - name: Push Docker image (Public ECR) if: matrix.registry == 'public' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-x86_64 run: | docker push public.ecr.aws/d8a4z9o5/artillery-worker:${{ env.DOCKER_TAG }} - name: Configure AWS Credentials (Private ECR) if: matrix.registry == 'private' uses: aws-actions/configure-aws-credentials@v1 with: aws-region: eu-west-1 audience: sts.amazonaws.com role-to-assume: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} role-session-name: OIDCSession mask-aws-account-id: true - name: Login to Amazon (Private ECR) if: matrix.registry == 'private' id: login-ecr-private uses: aws-actions/amazon-ecr-login@v1 - name: Build the Docker image (Private ECR) if: matrix.registry == 'private' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-x86_64 run: | docker build . --platform linux/amd64 --build-arg="WORKER_VERSION=${{ env.WORKER_VERSION }}" --tag 248481025674.dkr.ecr.eu-west-1.amazonaws.com/artillery-worker:${{ env.DOCKER_TAG }} -f ./packages/artillery/lib/platform/aws-ecs/worker/Dockerfile - name: Push Docker image (Private ECR) if: matrix.registry == 'private' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-x86_64 run: | docker push 248481025674.dkr.ecr.eu-west-1.amazonaws.com/artillery-worker:${{ env.DOCKER_TAG }} build_docker_image_arm64: runs-on: blacksmith-4vcpu-ubuntu-2404 if: ${{ inputs.SHOULD_BUILD_ARM64 }} env: # Set by the caller workflow, defaults to github.sha when not passed (e.g. workflow_dispatch against a branch) WORKER_VERSION: ${{ inputs.COMMIT_SHA || github.sha }} strategy: matrix: registry: [ public, private ] steps: - uses: actions/checkout@v3 with: ref: ${{ env.WORKER_VERSION }} fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Replace package version if: ${{ inputs.USE_COMMIT_SHA_IN_VERSION || false }} run: node .github/workflows/scripts/replace-package-versions.js env: COMMIT_SHA: ${{ env.WORKER_VERSION }} REPLACE_MAIN_VERSION_ONLY: true # we don't need to replace dependencies, as docker image builds using workspaces - name: Get Artillery version # we only want to tag with an actual version from pkg.json outside of PRs and manual dispatches # NOTE: can't check for refs/head/main because of pull_request_target used in some workflows if: github.event.pull_request == null && github.event_name != 'workflow_dispatch' run: | echo "WORKER_VERSION=$(node -e 'console.log(require("./packages/artillery/package.json").version)')" >> $GITHUB_ENV - name: Show git ref run: | echo GITHUB REF ${{ github.ref }} echo GITHUB PR HEAD SHA ${{ github.event.pull_request.head.sha }} echo GITHUB SHA ${{ github.sha }} echo WORKER_VERSION ENV ${{ env.WORKER_VERSION }} - name: Configure AWS Credentials (Public ECR) if: matrix.registry == 'public' uses: aws-actions/configure-aws-credentials@v1 with: aws-region: us-east-1 audience: sts.amazonaws.com role-to-assume: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} role-session-name: OIDCSession mask-aws-account-id: true - name: Login to Amazon (Public ECR) if: matrix.registry == 'public' id: login-ecr-public uses: aws-actions/amazon-ecr-login@v1 with: registry-type: public - name: Build the Docker image (Public ECR) if: matrix.registry == 'public' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-arm64 run: | docker build . --platform linux/arm64 --build-arg="WORKER_VERSION=${{ env.WORKER_VERSION }}" --tag public.ecr.aws/d8a4z9o5/artillery-worker:${{ env.DOCKER_TAG }} -f ./packages/artillery/lib/platform/aws-ecs/worker/Dockerfile - name: Push Docker image (Public ECR) if: matrix.registry == 'public' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-arm64 run: | docker push public.ecr.aws/d8a4z9o5/artillery-worker:${{ env.DOCKER_TAG }} - name: Configure AWS Credentials (Private ECR) if: matrix.registry == 'private' uses: aws-actions/configure-aws-credentials@v1 with: aws-region: eu-west-1 audience: sts.amazonaws.com role-to-assume: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} role-session-name: OIDCSession mask-aws-account-id: true - name: Login to Amazon (Private ECR) if: matrix.registry == 'private' id: login-ecr-private uses: aws-actions/amazon-ecr-login@v1 - name: Build the Docker image (Private ECR) if: matrix.registry == 'private' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-arm64 run: | docker build . --platform linux/arm64 --build-arg="WORKER_VERSION=${{ env.WORKER_VERSION }}" --tag 248481025674.dkr.ecr.eu-west-1.amazonaws.com/artillery-worker:${{ env.DOCKER_TAG }} -f ./packages/artillery/lib/platform/aws-ecs/worker/Dockerfile - name: Push Docker image (Private ECR) if: matrix.registry == 'private' env: DOCKER_TAG: ${{ env.WORKER_VERSION }}-arm64 run: | docker push 248481025674.dkr.ecr.eu-west-1.amazonaws.com/artillery-worker:${{ env.DOCKER_TAG }} ================================================ FILE: .github/workflows/docker-publish-artillery.yml ================================================ name: Publish Docker image for Artillery on: workflow_dispatch: # this will override the latest image, so only trigger when you want to publish a new version inputs: COMMIT_SHA: description: 'Commit SHA' required: true type: string workflow_call: inputs: COMMIT_SHA: description: 'Commit SHA' required: true type: string secrets: DOCKER_USERNAME: description: 'Docker Hub username' required: true DOCKER_PASSWORD: description: 'Docker Hub password' required: true jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Check out the repo uses: actions/checkout@v3 with: ref: ${{ inputs.COMMIT_SHA || null }} fetch-depth: 0 - name: Get Artillery version run: | echo "ARTILLERY_VERSION=$(node -e 'console.log(require("./packages/artillery/package.json").version)')" >> $GITHUB_ENV - name: Log in to Docker Hub uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: images: artilleryio/artillery tags: | type=semver,pattern={{version}},value=${{env.ARTILLERY_VERSION}} type=raw,value=latest - name: Build and push Docker image uses: useblacksmith/build-push-action@v2 with: context: ./packages/artillery push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/examples.yml ================================================ name: Examples on: push: branches: [main] pull_request: workflow_dispatch: env: ARTILLERY_BINARY_PATH: ${{ github.workspace }}/packages/artillery/bin/run CLI_TAGS: repo:${{ github.repository }},actor:${{ github.actor }},type:smoke,ci:true CLI_NOTE: Running from the Official Artillery Github Action! 😀 jobs: browser-load-test: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 10 env: CWD: ./examples/browser-load-testing-playwright steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: "22.x" - name: Install dependencies run: npm install # Uses the version from packages/artillery-engine-playwright - name: Install Playwright Browsers run: npm exec --workspace artillery-engine-playwright -- playwright install chromium --with-deps - name: Run test run: | $ARTILLERY_BINARY_PATH run browser-load-test.ts --record --tags ${{ env.CLI_TAGS }},group:browser-load-test --note "${{ env.CLI_NOTE }}" working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} http-metrics-by-endpoint: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 10 env: CWD: ./examples/http-metrics-by-endpoint defaults: run: working-directory: ${{ env.CWD }} steps: - name: Checkout uses: actions/checkout@v3 - name: Install run: npm ci - name: Test uses: artilleryio/action-cli@v1 with: command: run ./endpoint-metrics.yml --record --tags ${{ env.CLI_TAGS }},group:http-metrics-by-endpoint --note ${{ env.CLI_NOTE }} working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} multiple-scenarios-spec: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 10 env: CWD: ./examples/multiple-scenario-specs defaults: run: working-directory: ${{ env.CWD }} steps: - name: Checkout uses: actions/checkout@v3 - name: Install run: npm ci - name: Run armadillo scenario uses: artilleryio/action-cli@v1 with: command: run --config ./common-config.yml ./scenarios/armadillo.yml --record --tags ${{ env.CLI_TAGS }},group:multiple-scenario-specs --note ${{ env.CLI_NOTE }} working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} - name: Run dino scenario uses: artilleryio/action-cli@v1 with: command: run --config ./common-config.yml ./scenarios/dino.yml --record --tags ${{ env.CLI_TAGS }},group:multiple-scenario-specs --note ${{ env.CLI_NOTE }} working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} using-data-from-csv: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 env: CWD: ./examples/using-data-from-csv defaults: run: working-directory: ${{ env.CWD }} steps: - name: Checkout uses: actions/checkout@v3 - name: Install run: npm ci - name: Test uses: artilleryio/action-cli@v1 with: command: run ./website-test.yml --record --tags ${{ env.CLI_TAGS }},group:using-data-from-csv --note ${{ env.CLI_NOTE }} working-directory: ${{ env.CWD }} env: ARTILLERY_BINARY_PATH: ${{ env.ARTILLERY_BINARY_PATH }} ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} ================================================ FILE: .github/workflows/npm-publish-all-packages-canary.yml ================================================ name: Publish packages to NPM (canary) on: push: branches: - main paths: - 'packages/artillery/**' - 'packages/artillery-engine-playwright/**' - 'packages/artillery-engine-posthog/**' - 'packages/artillery-plugin-apdex/**' - 'packages/artillery-plugin-ensure/**' - 'packages/artillery-plugin-expect/**' - 'packages/artillery-plugin-metrics-by-endpoint/**' - 'packages/artillery-plugin-publish-metrics/**' - 'packages/artillery-plugin-fake-data/**' - 'packages/artillery-plugin-slack/**' - 'packages/commons/**' - 'packages/core/**' - 'packages/skytrace/**' - 'packages/artillery-plugin-memory-inspector/**' jobs: publish-fargate-worker-image: if: "!contains( github.event.head_commit.message, 'ci: release v')" uses: ./.github/workflows/docker-ecs-worker-image.yml permissions: contents: read id-token: write with: COMMIT_SHA: ${{ github.sha }} USE_COMMIT_SHA_IN_VERSION: true SHOULD_BUILD_ARM64: false secrets: ECR_WORKER_IMAGE_PUSH_ROLE_ARN: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} build: runs-on: blacksmith-4vcpu-ubuntu-2404 if: "!contains( github.event.head_commit.message, 'ci: release v')" needs: publish-fargate-worker-image permissions: contents: read packages: write env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} outputs: ARTILLERY_VERSION: ${{ steps.get-artillery-version.outputs.ARTILLERY_VERSION }} steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' scope: '@artilleryio' - run: node .github/workflows/scripts/replace-package-versions.js env: COMMIT_SHA: ${{ github.sha }} # It must be published in this specific order to account for order of dependencies (e.g. artillery depends on commons, core, etc), in case failures happen in publishing. - run: npm -w @artilleryio/int-commons publish --tag canary - run: npm -w @artilleryio/int-core publish --tag canary - run: npm -w artillery-plugin-expect publish --tag canary - run: npm -w artillery-plugin-publish-metrics publish --tag canary - run: npm -w artillery-plugin-metrics-by-endpoint publish --tag canary - run: npm -w artillery-plugin-ensure publish --tag canary - run: npm -w artillery-plugin-apdex publish --tag canary - run: npm -w artillery-engine-posthog publish --tag canary - run: npm -w artillery-engine-playwright publish --tag canary - run: npm -w artillery-plugin-fake-data publish --tag canary - run: npm -w artillery-plugin-slack publish --tag canary - run: npm -w artillery publish --tag canary - id: get-artillery-version run: | ARTILLERY_VERSION=$(node -e "console.log(require('./packages/artillery/package.json').version)") echo "ARTILLERY_VERSION=$ARTILLERY_VERSION" >> $GITHUB_OUTPUT # Skytrace is a Typescript Package and needs to install -> build -> publish - run: npm install -w skytrace --ignore-scripts - run: npm run build -w skytrace - run: npm -w skytrace publish --tag canary - run: npm -w artillery-plugin-memory-inspector publish --tag canary run-distributed-tests: uses: ./.github/workflows/run-distributed-tests.yml needs: build with: ARTILLERY_VERSION_OVERRIDE: ${{ needs.build.outputs.ARTILLERY_VERSION }} HAS_ARM64_BUILD: false permissions: contents: read id-token: write secrets: ARTILLERY_CLOUD_ENDPOINT_TEST: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY_TEST: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} DD_TESTS_API_KEY: ${{ secrets.DD_TESTS_API_KEY }} DD_TESTS_APP_KEY: ${{ secrets.DD_TESTS_APP_KEY }} AWS_TEST_EXECUTION_ROLE_ARN_TEST5: ${{ secrets.AWS_TEST_EXECUTION_ROLE_ARN_TEST5 }} publish-cloudformation-templates-canary-to-s3: uses: ./.github/workflows/s3-publish-cf-templates.yml needs: run-distributed-tests with: canary: true permissions: contents: read id-token: write secrets: AWS_ASSET_UPLOAD_ROLE_ARN: ${{ secrets.AWS_ASSET_UPLOAD_ROLE_ARN }} ================================================ FILE: .github/workflows/npm-publish-all-packages.yml ================================================ name: Publish packages to NPM on: workflow_dispatch: push: branches: - main paths: #If there are changes to package.json and ci:release v is in commit msg, it's a release - 'packages/artillery/package.json' jobs: publish-fargate-worker-image: if: > github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'ci: release v') uses: ./.github/workflows/docker-ecs-worker-image.yml permissions: contents: read id-token: write with: COMMIT_SHA: ${{ github.sha }} SHOULD_BUILD_ARM64: true secrets: ECR_WORKER_IMAGE_PUSH_ROLE_ARN: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} publish-packages-to-npm: runs-on: blacksmith-4vcpu-ubuntu-2404 if: > github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'ci: release v') needs: publish-fargate-worker-image permissions: contents: read packages: write outputs: ARTILLERY_VERSION: ${{ steps.get-artillery-version.outputs.ARTILLERY_VERSION }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # this list should be kept in sync with create-release-pr.yml PACKAGES_TO_RELEASE: "\ artillery-engine-playwright,\ artillery-plugin-apdex,\ artillery-plugin-ensure,\ artillery-plugin-expect,\ artillery-plugin-fake-data,\ artillery-plugin-slack,\ artillery-plugin-metrics-by-endpoint,\ artillery-plugin-publish-metrics,\ commons,\ core,\ artillery" steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' scope: '@artilleryio' - run: node .github/workflows/scripts/replace-package-versions.js # It must be published in this specific order to account for order of dependencies (e.g. artillery depends on commons, core, etc), in case failures happen in publishing. - run: npm -w @artilleryio/int-commons publish - run: npm -w @artilleryio/int-core publish - run: npm -w artillery-plugin-expect publish - run: npm -w artillery-plugin-publish-metrics publish - run: npm -w artillery-plugin-metrics-by-endpoint publish - run: npm -w artillery-plugin-ensure publish - run: npm -w artillery-plugin-apdex publish - run: npm -w artillery-engine-playwright publish - run: npm -w artillery-plugin-fake-data publish - run: npm -w artillery-plugin-slack publish - run: npm -w artillery publish - id: get-artillery-version run: | ARTILLERY_VERSION=$(node -e "console.log(require('./packages/artillery/package.json').version)") echo "ARTILLERY_VERSION=$ARTILLERY_VERSION" >> $GITHUB_OUTPUT # # Skytrace is a Typescript Package and needs to install -> build -> publish # - run: npm install -w skytrace --ignore-scripts # - run: npm run build -w skytrace # - run: npm -w skytrace publish publish-official-docker-image: uses: ./.github/workflows/docker-publish-artillery.yml if: > github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'ci: release v') needs: publish-packages-to-npm with: COMMIT_SHA: ${{ github.sha }} secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run-distributed-tests: uses: ./.github/workflows/run-distributed-tests.yml needs: publish-packages-to-npm with: ARTILLERY_VERSION_OVERRIDE: ${{ needs.publish-packages-to-npm.outputs.ARTILLERY_VERSION }} HAS_ARM64_BUILD: true permissions: contents: read id-token: write secrets: ARTILLERY_CLOUD_ENDPOINT_TEST: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY_TEST: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} DD_TESTS_API_KEY: ${{ secrets.DD_TESTS_API_KEY }} DD_TESTS_APP_KEY: ${{ secrets.DD_TESTS_APP_KEY }} AWS_TEST_EXECUTION_ROLE_ARN_TEST5: ${{ secrets.AWS_TEST_EXECUTION_ROLE_ARN_TEST5 }} publish-cloudformation-templates-to-s3: uses: ./.github/workflows/s3-publish-cf-templates.yml needs: run-distributed-tests with: canary: true permissions: contents: read id-token: write secrets: AWS_ASSET_UPLOAD_ROLE_ARN: ${{ secrets.AWS_ASSET_UPLOAD_ROLE_ARN }} ================================================ FILE: .github/workflows/npm-publish-artillery-engine-posthog.yml ================================================ name: Publish artillery-engine-posthog to npm on: push: branches: - main paths: - packages/artillery-engine-posthog/package.json jobs: build: if: "contains(github.event.head_commit.message, 'ci: release v')" runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' scope: '@artilleryio' - run: npm -w artillery-engine-posthog publish --tag latest env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/npm-publish-artillery-plugin-memory-inspector.yml ================================================ name: Publish artillery-plugin-memory-inspector to npm on: push: branches: - main paths: - packages/artillery-plugin-memory-inspector/package.json jobs: build: if: "contains(github.event.head_commit.message, 'ci: release v')" runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' scope: '@artilleryio' - run: npm -w artillery-plugin-memory-inspector publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/npm-publish-artillery-types.yml ================================================ name: Publish @artilleryio/types to npm on: push: branches: - main paths: - packages/types/package.json jobs: build: if: "contains(github.event.head_commit.message, 'ci: release v')" runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read packages: write steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' scope: '@artilleryio' - run: node .github/workflows/scripts/replace-package-versions.js - run: npm ci - run: npm -w '@artilleryio/types' publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/npm-publish-specific-package.yml ================================================ name: Publish specific package to NPM on: workflow_dispatch: inputs: CHANNEL: description: 'Channel to publish to. Can be "latest" or "canary".' type: choice options: - 'latest' - 'canary' default: 'canary' PACKAGE_FOLDER_NAME: description: 'Name of the package to publish (folder package).' required: true jobs: publish-fargate-worker-image: uses: ./.github/workflows/docker-ecs-worker-image.yml if: ${{ inputs.PACKAGE_FOLDER_NAME == 'artillery' }} permissions: contents: read id-token: write with: COMMIT_SHA: ${{ github.sha }} SHOULD_BUILD_ARM64: true secrets: ECR_WORKER_IMAGE_PUSH_ROLE_ARN: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} publish-packages-to-npm: runs-on: blacksmith-4vcpu-ubuntu-2404 needs: publish-fargate-worker-image permissions: contents: read packages: write env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' scope: '@artilleryio' - run: node .github/workflows/scripts/replace-package-versions.js if: ${{ inputs.CHANNEL == 'latest'}} - run: node .github/workflows/scripts/replace-package-versions.js env: COMMIT_SHA: ${{ github.sha }} if: ${{ inputs.CHANNEL == 'canary'}} - name: Get corresponding package name from package.json run: | PACKAGE_NAME=$(node -e "console.log(require('./packages/${{ inputs.PACKAGE_FOLDER_NAME }}/package.json').name)") echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_ENV" - run: npm install -w skytrace --ignore-scripts && npm run build -w skytrace if: ${{ inputs.PACKAGE_FOLDER_NAME == 'skytrace'}} - run: npm -w ${{ env.PACKAGE_NAME }} publish --tag ${{ inputs.CHANNEL }} publish-official-docker-image: uses: ./.github/workflows/docker-publish-artillery.yml if: ${{ inputs.PACKAGE_FOLDER_NAME == 'artillery'}} needs: publish-packages-to-npm with: COMMIT_SHA: ${{ github.sha }} secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} ================================================ FILE: .github/workflows/run-aws-tests-on-pr.yml ================================================ name: Run AWS tests (on PR) on: pull_request_target: branches: [main] #opened, reopened and synchronize will cause the workflow to fail on forks due to permissions #once labeled, that will then be overridden by the is-collaborator job types: [opened, labeled, synchronize, reopened] jobs: is-collaborator: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Get User Permission id: checkAccess uses: actions-cool/check-user-permission@cd622002ff25c2311d2e7fb82107c0d24be83f9b with: require: write username: ${{ github.actor }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check User Permission if: steps.checkAccess.outputs.require-result == 'false' run: | echo "${{ github.actor }} does not have permissions on this repo." echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}" exit 1 publish-branch-image: if: contains( github.event.pull_request.labels.*.name, 'run-aws-tests' ) needs: is-collaborator uses: ./.github/workflows/docker-ecs-worker-image.yml permissions: contents: read id-token: write secrets: ECR_WORKER_IMAGE_PUSH_ROLE_ARN: ${{ secrets.ECR_WORKER_IMAGE_PUSH_ROLE_ARN }} with: COMMIT_SHA: ${{ github.event.pull_request.head.sha || null }} # this should only be run with this ref if is-collaborator has been run and passed run-distributed-tests: needs: publish-branch-image uses: ./.github/workflows/run-distributed-tests.yml with: COMMIT_SHA: ${{ github.event.pull_request.head.sha || null }} permissions: contents: read id-token: write secrets: ARTILLERY_CLOUD_ENDPOINT_TEST: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY_TEST: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} DD_TESTS_API_KEY: ${{ secrets.DD_TESTS_API_KEY }} DD_TESTS_APP_KEY: ${{ secrets.DD_TESTS_APP_KEY }} AWS_TEST_EXECUTION_ROLE_ARN_TEST5: ${{ secrets.AWS_TEST_EXECUTION_ROLE_ARN_TEST5 }} ================================================ FILE: .github/workflows/run-distributed-tests.yml ================================================ name: Run distributed tests on: workflow_call: inputs: COMMIT_SHA: type: string ARTILLERY_VERSION_OVERRIDE: type: string HAS_ARM64_BUILD: type: boolean default: false secrets: ARTILLERY_CLOUD_ENDPOINT_TEST: required: true description: 'The endpoint for the Artillery Cloud API' ARTILLERY_CLOUD_API_KEY_TEST: required: true description: 'The api key for the Artillery Cloud API' DD_TESTS_API_KEY: required: true description: 'The api key for the Datadog API' DD_TESTS_APP_KEY: required: true description: 'The app key for the Datadog API' AWS_TEST_EXECUTION_ROLE_ARN_TEST5: required: true description: 'The role to assume for the AWS tests' permissions: contents: read id-token: write jobs: generate-test-matrix: runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} steps: - uses: actions/checkout@v3 with: ref: ${{ inputs.COMMIT_SHA || null }} - name: Use Node.js uses: actions/setup-node@v3 with: node-version: 22.x - id: generate-matrix run: | RESULT=$(node .github/workflows/scripts/get-tests-in-package-location.js) echo $RESULT echo "matrix=$RESULT" >> $GITHUB_OUTPUT run-tests: needs: generate-test-matrix timeout-minutes: 20 runs-on: blacksmith-4vcpu-ubuntu-2404 strategy: fail-fast: false matrix: testName: ${{fromJson(needs.generate-test-matrix.outputs.matrix).names}} permissions: contents: read id-token: write env: ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} DD_TESTS_API_KEY: ${{ secrets.DD_TESTS_API_KEY }} DD_TESTS_APP_KEY: ${{ secrets.DD_TESTS_APP_KEY }} GITHUB_REPO: ${{ github.repository }} GITHUB_ACTOR: ${{ github.actor }} HAS_ARM64_BUILD: ${{ inputs.HAS_ARM64_BUILD }} steps: - uses: actions/checkout@v3 with: ref: ${{ inputs.COMMIT_SHA || null }} # in a PR we make a collaborator check, otherwise this would override pull_request_target - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 env: SHOW_STACK_TRACE: true with: aws-region: eu-west-1 role-to-assume: ${{ secrets.AWS_TEST_EXECUTION_ROLE_ARN_TEST5 }} role-session-name: OIDCSession mask-aws-account-id: true - name: Use Node.js uses: actions/setup-node@v2 with: node-version: 22.x - run: .github/workflows/scripts/npm-command-retry.sh install - run: npm run build - name: Install Specific Artillery Version if needed if: ${{ inputs.ARTILLERY_VERSION_OVERRIDE || false }} run: mkdir __artillery__ && cd __artillery__ && npm init -y && ../.github/workflows/scripts/npm-command-retry.sh install artillery@${{ inputs.ARTILLERY_VERSION_OVERRIDE }} - name: Set A9_PATH if: ${{ inputs.ARTILLERY_VERSION_OVERRIDE || false }} run: echo "A9_PATH=${{ github.workspace }}/__artillery__/node_modules/.bin/artillery" >> $GITHUB_ENV - name: Set ECR Image Version if needed if: ${{ inputs.COMMIT_SHA }} run: | echo "ECR_IMAGE_VERSION=${{ inputs.COMMIT_SHA }}" >> $GITHUB_ENV echo "LAMBDA_IMAGE_VERSION=${{ inputs.COMMIT_SHA }}" >> $GITHUB_ENV # runs the single test file from `package` workspace in the `file`, as defined in the matrix output - run: npm run test:aws:ci --workspace ${{fromJson(needs.generate-test-matrix.outputs.matrix).namesToFiles[matrix.testName].packageName }} -- --files ${{ fromJson(needs.generate-test-matrix.outputs.matrix).namesToFiles[matrix.testName].file }} env: FORCE_COLOR: 1 run-tests-windows: needs: generate-test-matrix timeout-minutes: 20 runs-on: blacksmith-4vcpu-ubuntu-2404 continue-on-error: true permissions: contents: read id-token: write env: ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} DD_TESTS_API_KEY: ${{ secrets.DD_TESTS_API_KEY }} DD_TESTS_APP_KEY: ${{ secrets.DD_TESTS_APP_KEY }} GITHUB_REPO: ${{ github.repository }} GITHUB_ACTOR: ${{ github.actor }} HAS_ARM64_BUILD: ${{ inputs.HAS_ARM64_BUILD }} steps: - uses: actions/checkout@v3 with: ref: ${{ inputs.COMMIT_SHA || null }} # in a PR we make a collaborator check, otherwise this would override pull_request_target - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 env: SHOW_STACK_TRACE: true with: aws-region: eu-west-1 role-to-assume: ${{ secrets.AWS_TEST_EXECUTION_ROLE_ARN_TEST5 }} role-session-name: OIDCSession mask-aws-account-id: true - name: Use Node.js uses: actions/setup-node@v2 with: node-version: 22.x - run: .github/workflows/scripts/npm-command-retry.sh install - run: npm run build - name: Install Specific Artillery Version if needed if: ${{ inputs.ARTILLERY_VERSION_OVERRIDE || false }} run: mkdir __artillery__ && cd __artillery__ && npm init -y && ../.github/workflows/scripts/npm-command-retry.sh install artillery@${{ inputs.ARTILLERY_VERSION_OVERRIDE }} - name: Set A9_PATH if: ${{ inputs.ARTILLERY_VERSION_OVERRIDE || false }} run: echo "A9_PATH=${{ github.workspace }}/__artillery__/node_modules/.bin/artillery" >> $GITHUB_ENV - name: Set ECR Image Version if needed if: ${{ inputs.COMMIT_SHA }} run: | echo "ECR_IMAGE_VERSION=${{ inputs.COMMIT_SHA }}" >> $GITHUB_ENV echo "LAMBDA_IMAGE_VERSION=${{ inputs.COMMIT_SHA }}" >> $GITHUB_ENV - run: npm run test:aws:windows --workspace artillery env: FORCE_COLOR: 1 ================================================ FILE: .github/workflows/run-tests-windows.yml ================================================ name: Run Windows tests on: workflow_dispatch: inputs: ECR_IMAGE_VERSION: description: 'ECR image version' jobs: test: timeout-minutes: 60 runs-on: windows-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v3 - name: Use Node.js 22.x uses: actions/setup-node@v3 with: node-version: 22.x - run: npm install - run: npm run build - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 env: SHOW_STACK_TRACE: true with: aws-region: eu-west-1 role-to-assume: ${{ secrets.AWS_TEST_EXECUTION_ROLE_ARN_TEST5 }} role-session-name: OIDCSession mask-aws-account-id: true - name: Run local windows tests run: npm run test:windows --workspace artillery env: FORCE_COLOR: 1 - name: Run AWS windows tests run: npm run test:aws:windows --workspace artillery env: FORCE_COLOR: 1 ARTILLERY_CLOUD_ENDPOINT: ${{ secrets.ARTILLERY_CLOUD_ENDPOINT_TEST }} ARTILLERY_CLOUD_API_KEY: ${{ secrets.ARTILLERY_CLOUD_API_KEY_TEST }} GITHUB_REPO: ${{ github.repository }} GITHUB_ACTOR: ${{ github.actor }} ECR_IMAGE_VERSION: ${{ inputs.ECR_IMAGE_VERSION || github.sha}} - name: Notify about failures if: failure() && github.ref == 'refs/heads/main' uses: 8398a7/action-slack@v3.15.1 with: status: ${{ job.status }} fields: repo,message,commit,author,eventName,job,took,pullRequest env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: Run tests on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: generate-matrix-with-packages: runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 22.x - id: generate-matrix run: | RESULT=$(node .github/workflows/scripts/get-all-packages-by-name.js) echo "matrix=$RESULT" >> $GITHUB_OUTPUT test: timeout-minutes: 30 runs-on: blacksmith-4vcpu-ubuntu-2404 needs: generate-matrix-with-packages permissions: contents: read strategy: matrix: node-version: [22.x, 24.x] package: ${{fromJson(needs.generate-matrix-with-packages.outputs.matrix)}} fail-fast: false steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm run build - name: Install Playwright Browsers if: matrix.package == 'artillery-engine-playwright' run: npm exec --workspace artillery-engine-playwright -- playwright install chromium --with-deps - run: npm run test --workspace ${{ matrix.package }} env: FORCE_COLOR: 1 - name: Notify about failures if: failure() && github.ref == 'refs/heads/main' uses: 8398a7/action-slack@v3.15.1 with: status: ${{ job.status }} fields: repo,message,commit,author,eventName,job,took,pullRequest env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} test-windows: timeout-minutes: 60 runs-on: windows-latest needs: generate-matrix-with-packages continue-on-error: true permissions: contents: read id-token: write steps: - uses: actions/checkout@v3 - name: Use Node.js 22.x uses: actions/setup-node@v3 with: node-version: 22.x - run: npm install - run: npm run build - name: Run windows tests and capture exit code continue-on-error: true run: | npm run test:windows --workspace artillery echo "HAS_PASSED=$?" >> $env:GITHUB_ENV env: FORCE_COLOR: 1 - name: Notify about failures if: env.HAS_PASSED == 'False' uses: 8398a7/action-slack@v3.15.1 with: status: failure fields: repo,message,commit,author,eventName,job,took,pullRequest env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} ================================================ FILE: .github/workflows/s3-publish-cf-templates.yml ================================================ name: Publish CloudFormation templates to AWS S3 on: workflow_call: inputs: canary: type: boolean default: false description: 'Whether to deploy the canary versions of the templates' secrets: AWS_ASSET_UPLOAD_ROLE_ARN: description: 'ARN of the IAM role to assume to upload assets to S3' required: true workflow_dispatch: inputs: canary: type: boolean default: false description: 'Whether to deploy the canary versions of the templates' env: CF_LAMBDA_TEMPLATE: ${{ inputs.canary && 'aws-iam-lambda-cf-template-canary.yml' || 'aws-iam-lambda-cf-template.yml' }} CF_FARGATE_TEMPLATE: ${{ inputs.canary && 'aws-iam-fargate-cf-template-canary.yml' || 'aws-iam-fargate-cf-template.yml' }} GH_OIDC_LAMBDA_TEMPLATE: ${{ inputs.canary && 'gh-oidc-lambda-canary.yml' || 'gh-oidc-lambda.yml' }} GH_OIDC_FARGATE_TEMPLATE: ${{ inputs.canary && 'gh-oidc-fargate-canary.yml' || 'gh-oidc-fargate.yml' }} jobs: put-cloudformation-templates: runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: id-token: write contents: read steps: - name: Checkout code uses: actions/checkout@v2 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 env: SHOW_STACK_TRACE: true with: aws-region: us-east-1 role-to-assume: ${{ secrets.AWS_ASSET_UPLOAD_ROLE_ARN }} role-session-name: OIDCSession mask-aws-account-id: true - name: Update IAM CloudFormation templates run: | aws s3 cp --acl public-read ./packages/artillery/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml s3://artilleryio-cf-templates/${{ env.CF_FARGATE_TEMPLATE }} aws s3 cp --acl public-read ./packages/artillery/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml s3://artilleryio-cf-templates/${{ env.CF_LAMBDA_TEMPLATE }} aws s3 cp --acl public-read ./packages/artillery/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml s3://artilleryio-cf-templates/${{ env.GH_OIDC_LAMBDA_TEMPLATE }} aws s3 cp --acl public-read ./packages/artillery/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml s3://artilleryio-cf-templates/${{ env.GH_OIDC_FARGATE_TEMPLATE }} ================================================ FILE: .github/workflows/scripts/get-all-packages-by-name.js ================================================ const fs = require('node:fs'); const packageNames = []; fs.readdirSync('packages').forEach((pkg) => { if (fs.statSync(`packages/${pkg}`).isDirectory()) { const pkgJson = fs.readFileSync(`packages/${pkg}/package.json`, 'utf8'); packageNames.push(JSON.parse(pkgJson).name); } }); console.log(JSON.stringify(packageNames)); ================================================ FILE: .github/workflows/scripts/get-tests-in-package-location.js ================================================ const fs = require('node:fs'); const path = require('node:path'); /** * This script is used to discover all the tests in different test directories that match a specific suffix * and generate a JSON file that can be used to run the tests in parallel leveraging Github Actions */ const testLocations = [ { location: 'test/cloud-e2e/fargate', packageName: 'artillery', suffix: '.test.js' }, { location: 'test/cloud-e2e/lambda', packageName: 'artillery', suffix: '.test.js' }, { location: 'test', packageName: 'artillery-engine-playwright', suffix: '.aws.js' } ]; const tests = { names: [], namesToFiles: {} }; const addTest = (fileName, baseLocation, packageName, suffix) => { if (!fileName.endsWith(suffix)) { return; } const testName = fileName.replace(suffix, ''); const jobName = `${packageName}/${testName}`; tests.names.push(jobName); tests.namesToFiles[jobName] = { file: `${baseLocation}/${fileName}`, packageName: packageName }; }; // Recursively scan a directory to find files, and add tests to the tests object function scanDirectory(location, baseLocation, packageName, suffix) { fs.readdirSync(location).forEach((file) => { const absolute = path.join(location, file); if (fs.statSync(absolute).isDirectory()) { scanDirectory(absolute, baseLocation, packageName, suffix); } else { addTest(file, baseLocation, packageName, suffix); } }); } // Scan all the test locations for (const { packageName, location, suffix } of testLocations) { const fullLocation = `packages/${packageName}/${location}`; scanDirectory(fullLocation, location, packageName, suffix); } // Output the tests object as a JSON string to be used by Github Actions console.log(JSON.stringify(tests)); ================================================ FILE: .github/workflows/scripts/npm-command-retry.sh ================================================ #!/bin/bash # This is necessary because npm commands can fail intermittently due to network issues # The script retries an npm command up to 5 times with a 2-second delay between each attempt run_npm_command() { local max_retries=5 local retry_count=0 local sleep_time=2 local command="$@" while [ $retry_count -lt $max_retries ]; do $command && break retry_count=$((retry_count + 1)) echo "Command attempt $retry_count failed. Retrying in $sleep_time seconds..." sleep $sleep_time done if [ $retry_count -eq $max_retries ]; then echo "Command failed after $max_retries attempts." exit 1 else echo "Command succeeded." fi } if [ $# -eq 0 ]; then echo "No npm command provided." exit 1 else command="npm $@" fi run_npm_command "$command" ================================================ FILE: .github/workflows/scripts/replace-package-versions.js ================================================ const fs = require('node:fs'); const path = require('node:path'); const packagesDir = '../../../packages'; const commitSha = process.env.COMMIT_SHA; const getNewVersion = (version) => { if (!commitSha || commitSha === 'null') { return version; } const shortSha = commitSha.slice(0, 7); return `${version}-${shortSha}`; }; const versionMapping = {}; /** * This script iterates through every folder in ./packages and replaces their package.version with VERSION-COMMIT_SHA. * It then replaces the versions of all dependencies that are in this repo with the new VERSION-COMMIT_SHA of the corresponding package. * It is only used by the npm-publish-all-packages-canary.yml script, for the purposes of releasing a canary version of every package scoped to the latest commit to main. */ const updatePackageVersions = () => { const packageFolders = fs .readdirSync(path.join(__dirname, packagesDir), { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); packageFolders.forEach((folder) => { const packageJsonRelativePath = `${packagesDir}/${folder}/package.json`; const packageJsonFullPath = path.join(__dirname, packageJsonRelativePath); if (!fs.existsSync(packageJsonFullPath)) { throw new Error( `Path ${packageJsonRelativePath} does not exist! Please ensure that it is a package!` ); } const packageData = fs.readFileSync(packageJsonFullPath); const packageJson = JSON.parse(packageData); packageJson.version = getNewVersion(packageJson.version); versionMapping[packageJson.name] = { content: packageJson, path: packageJsonFullPath }; }); for (const pkg of Object.values(versionMapping)) { if (!process.env.REPLACE_MAIN_VERSION_ONLY) { updateDependencies(pkg); } saveUpdatedPackage(pkg); } }; const updateDependencies = (pkg) => { const { content: { dependencies } } = pkg; for (const packageNameToReplace of Object.keys(versionMapping)) { if (dependencies?.[packageNameToReplace]) { //replace the dependency we care about in this package with its corrected canary version dependencies[packageNameToReplace] = versionMapping[packageNameToReplace].content.version; console.log( `Updated dependency ${packageNameToReplace} in ${pkg.content.name} to ${dependencies[packageNameToReplace]}` ); } } }; const saveUpdatedPackage = (pkg) => { fs.writeFileSync(pkg.path, JSON.stringify(pkg.content, null, 2)); }; updatePackageVersions(); ================================================ FILE: .gitignore ================================================ .idea *.iml npm-debug.log dump.rdb node_modules components build .tap results.xml config.json .DS_Store */.DS_Store */*/.DS_Store ._* */._* */*/._* coverage.* lib-cov *scratch* artillery_report* coverage/* .vagrant/ .vscode .turbo build/** dist/** .next/** ================================================ FILE: .npmignore ================================================ test/ .idea *.iml npm-debug.log dump.rdb node_modules components build results.tap results.xml config.json .DS_Store */.DS_Store */*/.DS_Store ._* */._* */*/._* coverage.* lib-cov *scratch* coverage/* node_modules examples packages/ ================================================ FILE: .npmrc ================================================ ignore-scripts=true ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at team@artillery.io. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Artillery.io Contributors Guide ## Need to get in touch? All project discussions should happen in the [issue tracker](https://github.com/artilleryio/artillery/issues) or via [Discussions](https://github.com/artilleryio/artillery/discussions). If you are a first-time contributor and want some help getting started, feel free to get in touch over email: * Hassy Veldstra - [h@artillery.io](mailto:h@artillery.io?subject=Artillery Contribution Help) ## Guide for Contributions * We use the usual Fork+Pull model (more info here: [https://help.github.com/articles/using-pull-requests/](https://help.github.com/articles/using-pull-requests/)] * Pull requests that modify or add behavior should have tests, whether it's a new feature or a bug fix. If you're unsure how to structure a test, we can help. * We love PRs that fix bugs. * Do not add a new feature without discussing it via [Discussions](https://github.com/artilleryio/artillery/discussions) first. We've had to decline feature suggestions submitted via PRs in the past because they duplicate existing functionality, have limited utility to the wider user base, or carry too much maintenance burden. We don't want you to spend your time on something that we will not accept. * One logical change per commit please. We'll ask you to rebase PRs containing commits that change several unrelated things. * The smaller a PR is the better. Smaller PRs are much easier to review and provide feedback on. Always lean towards smaller PRs. * Before you write more than a few lines of code, please make sure: * If it's a new feature proposal - that it has been discussed and accepted * Let others know that you are working on the issue * Commit messages should follow this style (we use the [commitlint conventional](https://github.com/marionebl/commitlint/tree/master/%40commitlint/config-conventional) config): ``` feat: A brief one-liner < 50 chars, use the imperative mood Followed by further explanation if needed, this should be wrapped at around 72 characters. Most commits should reference an existing issue, such as #101 above. ``` Some reading on good commit messages: [http://chris.beams.io/posts/git-commit/](http://chris.beams.io/posts/git-commit/) * Once your first PR has been merged, please add yourself to `package.json` for the relevant module and open another PR. ## Licensing By sending a patch you certify that you have the rights to and agree for your contribution to be distributed under the terms of [MPL2](https://www.mozilla.org/en-US/MPL/2.0/). You will also need to sign a CLA before your first PR is merged. A GitHub bot will guide you through that after a new PR is opened. ================================================ FILE: LICENSE-BSL.txt ================================================ Business Source License ======================= License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. “Business Source License” is a trademark of MariaDB Corporation Ab. Parameters Licensor: Artillery Software Inc Licensed Works: Azure-related code in Artillery. The Licensed Work is (c) 2024 Artillery Software Inc Additional Use Grant: You may make use of the Licensed Work strictly for evaluation and/or non-production use only. Your use does not include offering the Licensed Work to third parties on a hosted or embedded basis. Change Date: Four years from the date the Licensed Work is published Change License: MPL 2.0 For information about commercial licensing arrangements for the Licensed Work, please contact sales@artillery.io. Notice Business Source License 1.1 Terms The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. ================================================ FILE: LICENSE.txt ================================================ Mozilla Public License, version 2.0 1. Definitions 1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 1.3. “Contribution” means Covered Software of a particular Contributor. 1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. “Incompatible With Secondary Licenses” means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. “Executable Form” means any form of the work other than Source Code Form. 1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. “License” means this document. 1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. “Modifications” means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. “Source Code Form” means the form of the work preferred for making modifications. 1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================

Artillery

Docs | Discussions | @artilleryio

npm

## Features - **Test at cloud scale.** Cloud-native distributed load testing at scale, **out-of-the box and for free**. - Scale out your load tests on top of AWS Lambda or AWS Fargate. No DevOps needed, zero infrastructure to set up or manage. - **Test with Playwright**. Load test with real headless browsers. - **Batteries-included.** 20+ integrations for monitoring, observability, and CICD. - **Test anything**. HTTP, WebSocket, Socket.io, gRPC, Kinesis, and more. - **Powerful workload modeling**. Emulate complex user behavior with request chains, multiple steps, transactions, and more. - **Extensible & hackable**. Artillery has a plugin API to allow extending and customization. ## License * Most of the code in this repository is licensed under the terms of the [MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/) license. * Some Azure-specific modules are licensed under the terms of the [BSL license](https://mariadb.com/bsl-faq-adopting/). See [LICENSE-BSL.txt](./LICENSE-BSL.txt) for details. You may use Artillery on Azure for evaluation and proof-of-concept purposes, but commercial and/or production usage requires a commercial license. → [Learn more](./packages/artillery#readme) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions The following versions of Artillery are currently being supported with security updates: | Version | Supported | | ------- | ------------------ | | 2.x.x | :white_check_mark: | | 1.7.x | :x: | | < 1.7 | :x: | ## Reporting a Vulnerability Please see the latest version of our security policy at https://www.artillery.io/security-policy ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", "formatter": { "enabled": false }, "linter": { "enabled": true, "rules": { "suspicious": { "noThenProperty": "off", "noExplicitAny": "off" } } }, "files": { "includes": [ "**", "!**/packages/types", "!**/packages/artillery-engine-playwright/test/**/*.ts", "!**/examples/browser-playwright-reuse-typescript/**/*.ts", "!**/node_modules", "!**/dist", "!**/build" ] } } ================================================ FILE: commitlint.config.js ================================================ const config = require('@commitlint/config-conventional'); const types = config.rules['type-enum'][2].concat(['dep']); module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', types] } }; ================================================ FILE: examples/README.md ================================================

artillery-examples

# Artillery Examples This repo contains examples of how to use various features in Artillery. Every example is self-contained and can be run as-is without external dependencies (other than those in `package.json`). ## Test scripts ### Core features - [using-data-from-csv](./using-data-from-csv) - using data from an external CSV file in vuser scenarios - [scenario-weights](./scenario-weights) - set weights to change how often Artillery runs a scenario - [script-overrides](./script-overrides) - override parts of the script such as load phases dynamically at runtime - [multiple-scenario-specs](./multiple-scenario-specs) - organizing your Artillery test codebase into separate scenario files - [automated-checks](./automated-checks) - setting up automated checks with `ensure` and `apdex` plugins ### How-tos - [refresh-auth-token](./refresh-auth-token/) - how to refresh an auth token used by a VU as the test is running ### End-to-end examples - [socket-io](./socket-io) - testing a Socket.io service - [websockets](./websockets) - testing a WebSocket service - [graphql-api-server](./graphql-api-server) - testing a GraphQL API server - [browser-load-testing-playwright](./browser-load-testing-playwright) - load testing with real browsers - [functional testing](./functional-testing-with-expect-plugin) - use `artillery-plugin-expect` to run both load and functional tests - [CSV-driven functional testing](./table-driven-functional-tests) - define functional tests with a CSV file ### HTTP-specific examples - [http-set-custom-header](./http-set-custom-header) - set an HTTP header in a `beforeRequest` hook - [using-cookies](./using-cookies) - using cookies with HTTP services - [file-uploads](./file-uploads) - HTTP file uploads with Artillery Pro ### Plugins and extensions - [track-custom-metrics](./track-custom-metrics) - track custom metrics (counters and histograms) - [artillery-plugin-hello-world](./artillery-plugin-hello-world) - a "hello world" plugin ## Running Artillery in CI/CD - [cicd examples](./cicd) - using Artillery with Github Actions, Gitlab CI, Azure DevOps, CircleCI and more ## Starter kits - [starter-kit](./starter-kit) - @cfryerdev's Artillery starter kit - an example of how a few different bits fit together ## Testing on Kubernetes - [k8s-testing-with-kubectl-artillery-](./k8s-testing-with-kubectl-artillery) # Contributing Would you like to share an example showing how to use a feature in Artillery with the community? Send us a PR 💜 # License All code in this repo is licensed under the terms of the [MPL2 license](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). ================================================ FILE: examples/artillery-engine-example/.gitignore ================================================ .idea *.iml npm-debug.log dump.rdb node_modules components build results.tap results.xml config.json .DS_Store */.DS_Store */*/.DS_Store ._* */._* */*/._* coverage.* lib-cov *scratch* artillery_report* coverage/* .vagrant/ .vscode ================================================ FILE: examples/artillery-engine-example/LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: examples/artillery-engine-example/README.md ================================================ # Artillery Engine Example This repo contains the code for a simple "hello world" Artillery engine that shows how Artillery's engine API works, and can serve as a starting point for a custom engine. ## Usage - Install dependencies with `npm install` - Set up parent folder as `NODE_PATH` so this engine is loaded by Artillery: `export NODE_PATH=$(pwd)/..` - Run the example script using this engine: `artillery run example.yaml` ### License [MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/) ================================================ FILE: examples/artillery-engine-example/example.yaml ================================================ config: target: "system-under-test-endpoint" example: mandatoryString: "a configuration setting for our engine" phases: - arrivalRate: 1 duration: 1 engines: example: {} scenarios: - name: "custom_example_engine_scenario" engine: example flow: - doSomething: id: 123 - doSomething: id: 456 ================================================ FILE: examples/artillery-engine-example/index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const A = require('async'); const debug = require('debug')('engine:example'); // Simple example engine that recieves a prop and prints it when a 'doSomething' // action is found. // Serves as a modifiable example to build on top of for new engines class ExampleEngine { // Artillery initializes each engine with the following arguments: // // - script is the entire script object, with .config and .scenarios properties // - events is an EventEmitter we can use to subscribe to events from Artillery, and // to report custom metrics // - helpers is a collection of utility functions constructor(script, ee, helpers) { this.script = script; this.ee = ee; this.helpers = helpers; // This would typically be the endpoint we're testing this.target = script.config.target; const opts = { ...this.script.config.example }; // We can add custom validations on those props if (!opts.mandatoryString) { throw new Error('mandatoryString setting must be set'); } } // For each scenario in the script using this engine, Artillery calls this function // to create a VU function createScenario(scenarioSpec, ee) { const tasks = scenarioSpec.flow.map((rs) => this.step(rs, ee)); return function scenario(initialContext, callback) { ee.emit('started'); function vuInit(callback) { // we can run custom VU-specific init code here return callback(null, initialContext); } const steps = [vuInit].concat(tasks); A.waterfall(steps, function done(err, context) { if (err) { debug(err); } return callback(err, context); }); }; } // This is a convenience function where we delegate common actions like loop, log, and think, // and handle actions which are custom for our engine, i.e. the "doSomething" action in this case step(rs, ee) { const self = this; if (rs.loop) { const steps = rs.loop.map((loopStep) => this.step(loopStep, ee)); return this.helpers.createLoopWithCount(rs.count || -1, steps, {}); } if (rs.log) { return function log(context, callback) { return process.nextTick(() => { callback(null, context); }); }; } if (rs.think) { return this.helpers.createThink(rs, self.config?.defaults?.think || {}); } if (rs.function) { return (context, callback) => { const func = self.script.config.processor[rs.function]; if (!func) { return process.nextTick(() => { callback(null, context); }); } return func(context, ee, () => callback(null, context)); }; } // // This is our custom action: // if (rs.doSomething) { return function example(context, callback) { console.log( 'doSomething action with id:', self.helpers.template(rs.doSomething.id, context, true) ); console.log('target is:', self.target); // Emit a metric to count the number of example actions performed: ee.emit('counter', 'example.action_count', 1); return callback(null, context); }; } // // Ignore any unrecognized actions: // return function doNothing(context, callback) { return callback(null, context); }; } } module.exports = ExampleEngine; ================================================ FILE: examples/artillery-engine-example/package.json ================================================ { "name": "artillery-engine-example", "version": "0.0.1", "description": "Engine template/example for Artillery", "main": "index.js", "scripts": { "test": "node test/index.js" }, "keywords": [ "artillery", "engine", "load" ], "author": "Juan Gil ", "license": "MPL-2.0", "devDependencies": { "tap": "^18.6.1", "tape": "^5.6.1" }, "dependencies": { "async": "^3.2.4", "debug": "^4.3.4" } } ================================================ FILE: examples/artillery-engine-example/test/index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { test } = require('tap'); const EventEmitter = require('node:events'); const ExampleEngine = require('..'); const script = { config: { target: 'my-endpoint', example: { mandatoryString: 'hello-world' } }, scenarios: [ { name: 'test scenario', engine: 'example', flow: [ { doSomething: { id: 123 } } ] } ] }; test('Engine interface', async (t) => { const events = new EventEmitter(); const engine = new ExampleEngine(script, events, {}); const scenario = engine.createScenario(script.scenarios[0], events); t.match(engine.script, script, 'Engine constructor sets script'); t.type(scenario, 'function', 'Engine.createScenario returns a function'); }); ================================================ FILE: examples/artillery-plugin-hello-world/README.md ================================================ # artillery-hello-world-plugin This is a "hello world" plugin for Artillery which shows: - Artillery's plugin interface - How a barebones plugin is constructed - How to inspect test script properties in a plugin - How to attach custom hooks to scenarios in a plugin to do something interesting ## Run the example scenario 👋 By default Artillery will look in Node.js package path for the plugin package (which is expected to have an `artillery-plugin-` prefix, so the package for the `hello-world` plugin is expected to be named `artillery-plugin-hello-world`). We can add an extra look-up location with `ARTILLERY_PLUGIN_PATH`. This is useful when developing plugins. From the folder where this README is located, run: ```sh ARTILLERY_PLUGIN_PATH=`pwd`/.. DEBUG=plugin:hello-world artillery run test.yml ``` And we should see our greeting, debug messages from the plugin, and the counter in Artillery's output: ![greeting](./images/screenshot1.png) ![custom counter](./images/screenshot2.png) ## Learn more 📖 Check out these plugins for ideas of how to do more things: - https://github.com/artilleryio/artillery-plugin-publish-metrics - https://github.com/artilleryio/artillery-plugin-hls - https://github.com/artilleryio/artillery-plugin-fuzzer - https://github.com/artilleryio/artillery-plugin-expect Artillery's extension APIs: https://artillery.io/docs/guides/guides/extension-apis Blog post on creating a custom plugin: https://artillery.io/blog/extend-artillery-by-creating-your-own-plugins ## Write a plugin (and let us know!) ⚡ Artillery's plugin interface + the power of Node.js (there's an npm package for everything!) make it easy to extend Artillery with new functionality. If you write a new plugin, let us know! - Github Discussion board: https://github.com/artilleryio/artillery/discussions 💜 ================================================ FILE: examples/artillery-plugin-hello-world/index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('plugin:hello-world'); module.exports.Plugin = ArtilleryHelloWorldPlugin; function ArtilleryHelloWorldPlugin(script, events) { // This is the entirety of the test script - config and // scenarios this.script = script; // This is an EventEmitter, we can subscribe to: // 'stats' - fired when a new batch of metrics is available // 'done' - fired when all VUs are done // We can also use this EventEmitter to emit custom // metrics: // https://artillery.io/docs/guides/guides/extending.html#Tracking-custom-metrics this.events = events; // We can read our plugin's configuration: const pluginConfig = script.config.plugins['hello-world']; this.greeting = pluginConfig.greeting || 'hello, world'; // But we could also read anything else defined in the test // script, e.g.: debug('target is:', script.config.target); // // Let's attach a beforeRequest hook to all scenarios // which will print a greeting before a request is made // // Create processor object if needed to hold our custom function: script.config.processor = script.config.processor || {}; // Add our custom function: script.config.processor.pluginHelloWorldBeforeRequestHook = ( _req, _vuContext, events, next ) => { // This a beforeRequest handler function: // https://artillery.io/docs/guides/guides/http-reference.html#beforeRequest console.log(this.greeting); // print greeting events.emit('counter', 'greeting_count', 1); // increase custom counter return next(); // the hook is done, go on to the next one (or let Artillery make the request) }; // Attach the function to every scenario as a scenario-level hook: script.scenarios.forEach((scenario) => { scenario.beforeRequest = scenario.beforeRequest || []; scenario.beforeRequest.push('pluginHelloWorldBeforeRequestHook'); }); return this; } // Artillery will call this before it exits to give plugins // a chance to clean up, e.g. by flushing any in-flight data, // writing something to disk etc. ArtilleryHelloWorldPlugin.prototype.cleanup = (done) => { debug('cleaning up'); done(null); }; ================================================ FILE: examples/artillery-plugin-hello-world/package.json ================================================ { "name": "artillery-plugin-hello-world", "version": "1.0.0", "description": "This is a \"hello world\" plugin for Artillery which shows:", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "MPL-2.0", "dependencies": { "debug": "^4.3.1" } } ================================================ FILE: examples/artillery-plugin-hello-world/test.yml ================================================ config: target: "http://asciiart.artillery.io:8080" phases: - arrivalRate: 1 duration: 10 plugins: hello-world: greeting: "Hello world! 👋" scenarios: - flow: - get: url: "/" ================================================ FILE: examples/automated-checks/README.md ================================================ # Using automated checks This is an example Artillery load test that includes: 1. A load configuration with 3 distinct phases that create a burst of traffic after a warm up period 2. Configuration for [`apdex`](https://docs.art/reference/extensions/apdex) and [`ensure`](https://docs.art/reference/extensions/ensure) plugins to set up automated scoring and checking of performance results from the test 3. Use of `metrics-by-endpoint` plugin to enable reporting of metrics for each individual URL in the test Run the script with: ``` artillery run test-with-automated-checks.yml ``` ================================================ FILE: examples/automated-checks/load-test-with-automated-checks.yml ================================================ # This an example Artillery load test that includes: # - A load configuration with 3 distinct phases that create # a burst of traffic after a warm up period # - How to use built-in "apdex" and "ensure" plugins to set up # automated scoring and checking of performance results from the test # - Using metrics-by-endpoint plugin to enable reporting of metrics # for each individual URL in the test config: # This is a test server run by team Artillery # It's designed to be highly scalable and withstand # traffic spikes of millions of requests per second target: http://asciiart.artillery.io:8080 phases: - duration: 60 arrivalRate: 1 rampTo: 5 name: Warm up phase - duration: 60 arrivalRate: 5 rampTo: 10 name: Ramp up load - duration: 30 arrivalRate: 10 rampTo: 30 name: Spike phase # Load a couple of useful plugins # https://docs.art/reference/extensions plugins: ensure: {} apdex: {} metrics-by-endpoint: {} # Set a threshold of 25ms for calculating Apdex scores # https://docs.art/reference/extensions/apdex apdex: threshold: 100 # Configure automated checks # https://docs.art/reference/extensions/ensure ensure: thresholds: - http.response_time.p99: 100 - http.response_time.p95: 75 scenarios: - flow: - loop: - get: url: "/dino" - get: url: "/pony" - get: url: "/armadillo" count: 100 ================================================ FILE: examples/browser-load-testing-playwright/README.md ================================================ # Load testing and smoke testing with headless browsers Artillery can run Playwright scripts as performance tests. This example shows you how to run a simple load test, a smoke test, and how to track custom metrics for part of the flow. - [Why load test with headless browsers?](https://www.artillery.io/docs/playwright#why-load-test-with-headless-browsers) > [!TIP] > Artillery's uses YAML as its default configuration format, but Playwright tests can be written as TypeScript. The examples below are shown as both TypeScript-only, and YAML + TypeScript. ## Example 1: A simple load test Run a simple load test using a plain Playwright script (recorded with `playwright codegen` - no Artillery-specific changes required): ```sh # Run the TypeScript example: npx artillery run browser-load-test.ts ``` ```sh # The same example configured with a separate YAML config file: npx artillery run browser-load-test.yml ``` That's it! Artillery will create headless Chrome browsers that will run Playwright scenarios you provide. ## Example 2: A smoke test This example shows how we can implement a smoke test (or a synthetic check) using a headless browser. We make use of Artillery's [CSV payload](https://artillery.io/docs/guides/guides/test-script-reference.html#Payload-files) feature to specify the URLs we want to check, and [custom metric API](https://artillery.io/docs/guides/guides/extending.html#Tracking-custom-metrics) to track custom metrics. For every row in the CSV file, we'll load the URL from the first column, and check that the page contains the text specified in the second column. The test will load each page specified in the CSV file, and check that it contains the text ```sh # Run the TypeScript example: npx artillery run browser-smoke-test.ts ``` ```sh # The same example configured with a separate YAML config file: npx artillery run browser-smoke-test.yml ``` ## Example 3: Tracking custom metrics for part of the flow A common usage scenario is reporting performance metrics only for one part of a test flow. For example, you may be testing an ecommerce app with the following steps: 1. Go to the homepage 2. Search for a product 3. Navigate to product page 4. Add product to cart 5. Login 6. Complete checkout: - Enter a discount code - Update billing info - Check out You may want to report performance metrics only for part 6 of the flow. Artillery lets you do that with its [custom metrics API](https://www.artillery.io/docs/guides/guides/extension-apis#tracking-custom-metrics). See the example in [./advanced-custom-metric-for-subflow.yml](./advanced-custom-metric-for-subflow.yml) and specifically the `multistepWithCustomMetrics()` test in [flows.js](./flows.js) for details. ## Creating Playwright scripts You can use the built-in `playwright codegen` tool to generate test scripts quickly by performing user actions in the real browser. That's how the code in `flows.js` in this example was created. It's just a Playwright script, there's nothing Artillery specific about it. **Speed up test creation time by 10x.** ## Front-end AND back-end metrics Artillery will emit both backend and browser-level performance metrics when running this test, so that you can see both how long resources such as static assets took to load, as well as page-level metrics, such as how long it took for pages to become interactive. ``` vusers.created_by_name.Dev account signup: .................. 10 vusers.created.total: ....................................... 10 vusers.completed: ........................................... 10 vusers.session_length: min: ...................................................... 3884.2 max: ...................................................... 13846.2 median: ................................................... 12711.5 p95: ...................................................... 12968.3 p99: ...................................................... 12968.3 browser.page_domcontentloaded: ........................... 20 browser.response_time: min: ...................................................... 0 max: ...................................................... 1778.8 median: ................................................... 37.7 p95: ...................................................... 3828.5 p99: ...................................................... 3828.5 browser.page_domcontentloaded.dominteractive: min: ...................................................... 297 max: ...................................................... 2247 median: ................................................... 1002.4 p95: ...................................................... 1939.5 p99: ...................................................... 1939.5 browser.page_domcontentloaded.dominteractive.https://artillery.io/: min: ...................................................... 427 max: ...................................................... 2247 median: ................................................... 1130.2 p95: ...................................................... 1939.5 p99: ...................................................... 1939.5 browser.page_domcontentloaded.dominteractive.https://artillery.io/pro/: min: ...................................................... 297 max: ...................................................... 1927 median: ................................................... 596 p95: ...................................................... 1380.5 p99: ...................................................... 1380.5 ``` ## Scaling browser tests Running headless browsers in parallel will quickly exhaust CPU and memory of a single machine. Artillery has built-in support for cloud-native distributed load testing on AWS Fargate or Azure Container Instances. See our guide for [Distributed load testing](https://www.artillery.io/docs/load-testing-at-scale) for more information. ================================================ FILE: examples/browser-load-testing-playwright/browser-load-test.ts ================================================ export const config = { target: 'https://www.artillery.io', phases: [ { arrivalRate: 1, duration: 10 } ], engines: { playwright: { trace: true } } }; export const before = { engine: 'playwright', testFunction: async function beforeFunctionHook(_page, userContext, _events) { // Any scenario variables we add via userContext.vars in this before hook will be available in every VU userContext.vars.testStartTime = new Date(); } }; export const scenarios = [ { engine: 'playwright', name: 'check_out_core_concepts_scenario', testFunction: async function checkOutArtilleryCoreConceptsFlow( page, _userContext, _events, test ) { await test.step('Go to Artillery', async () => { const requestPromise = page.waitForRequest('https://artillery.io/'); await page.goto('https://artillery.io/'); const _req = await requestPromise; }); await test.step('Go to docs', async () => { await page.getByRole('link', { name: 'Docs' }).first().click(); await page.waitForURL('https://www.artillery.io/docs'); }); await test.step('Go to core concepts', async () => { await page .getByRole('link', { name: 'Start a new GitHub Discussion' }) .click(); await page.waitForURL( 'https://github.com/artilleryio/artillery/discussions' ); }); } } ]; ================================================ FILE: examples/browser-load-testing-playwright/browser-load-test.yml ================================================ config: target: "https://www.artillery.io" phases: - arrivalRate: 1 duration: 10 engines: playwright: {} processor: ./flows.js scenarios: - name: "check_out_core_concepts_scenario" engine: playwright flowFunction: "checkOutArtilleryCoreConceptsFlow" ================================================ FILE: examples/browser-load-testing-playwright/browser-smoke-test.ts ================================================ import { checkPage } from './flows'; export const config = { target: 'https://www.artillery.io', phases: [ { arrivalCount: 1, duration: 1 } ], payload: { path: './pages.csv', fields: ['url', 'title'], loadAll: true, name: 'pageChecks' }, engines: { playwright: {} } }; export const scenarios = [ { name: 'smoke_test_page', engine: 'playwright', testFunction: checkPage } ]; ================================================ FILE: examples/browser-load-testing-playwright/browser-smoke-test.yml ================================================ config: target: "https://www.artillery.io" payload: - path: ./pages.csv fields: - "url" - "title" loadAll: true name: pageChecks engines: playwright: {} processor: ./flows.js scenarios: - name: smoke_test_page engine: playwright flowFunction: checkPage ================================================ FILE: examples/browser-load-testing-playwright/browser-test-with-steps.yml ================================================ config: target: "https://www.artillery.io" phases: - arrivalRate: 1 duration: 10 engines: playwright: {} processor: ./flows.js scenarios: - name: flow_with_multiple_steps engine: playwright flowFunction: "multistepWithCustomMetrics" ================================================ FILE: examples/browser-load-testing-playwright/flows.js ================================================ // // The code in this function was generated with // playwright codegen // https://playwright.dev/docs/codegen // async function checkOutArtilleryCoreConceptsFlow( page, _userContext, _events, test ) { await test.step('Go to Artillery', async () => { const requestPromise = page.waitForRequest('https://artillery.io/'); await page.goto('https://artillery.io/'); const _req = await requestPromise; }); await test.step('Go to docs', async () => { await page.getByRole('link', { name: 'Docs' }).first().click(); await page.waitForURL('https://www.artillery.io/docs'); }); await test.step('Go to core concepts', async () => { await page .getByRole('link', { name: 'Review core concepts' }) .click(); await page.waitForURL( 'https://www.artillery.io/docs/get-started/core-concepts' ); }); } // // A simple smoke test using a headless browser: // async function checkPage(page, userContext, events) { // The pageChecks variable is created via the config.payload // section in the YML config file for (const { url, title } of userContext.vars.pageChecks) { const response = await page.goto(url); if (response.status() !== 200) { events.emit('counter', `user.status_check_failed.${url}`, 1); } else { events.emit('counter', `user.status_check_ok.${url}`, 1); } const isElementVisible = await page.getByText(title).isVisible(); if (!isElementVisible) { events.emit('counter', `user.element_check_failed.${title}`, 1); } await page.reload(); } } async function multistepWithCustomMetrics(page, _userContext, _events, test) { //1. we get the convenience step() helper from the test object. //More information: https://www.artillery.io/docs/reference/engines/playwright#teststep-argument const { step } = test; //2. We can now wrap parts of our Playwright script in step() calls await step('go_to_artillery_io', async () => { await page.goto('https://www.artillery.io'); }); await step('go_to_cloud_page', async () => { await page.goto('https://www.artillery.io/cloud'); }); await step('go_to_docs', async () => { await page.goto('https://www.artillery.io/docs'); }); // 3. latency metrics will be emitted automatically throughout the test for each step. // For more information on custom metrics, please see: https://www.artillery.io/docs/guides/guides/extension-apis#tracking-custom-metrics } module.exports = { checkOutArtilleryCoreConceptsFlow, checkPage, multistepWithCustomMetrics }; ================================================ FILE: examples/browser-load-testing-playwright/pages.csv ================================================ https://www.artillery.io/,trademark of Artillery Software Inc https://www.artillery.io/docs,Get started https://www.artillery.io/changelog,Feature updates and improvements to Artillery ================================================ FILE: examples/browser-playwright-reuse-authentication/README.md ================================================ # Reuse Authentication in Playwright tests Playwright allows you to use `sessionStorage` to reuse authentication in your tests. This can be especially useful in Load Testing, where you might be interested in testing other parts of the application at scale, without having to exercise the login backend (which is sometimes third-party) with every VU. This example shows you how to do that. ## Pre-requisites Before running the example, install Artillery: ```sh npm install artillery ``` ## Example The example leverages [`storageState`] similarly to [Playwright documentation](https://playwright.dev/docs/auth#basic-shared-account-in-all-tests). It's simple to set this up with Artillery, but there are some small differences: * You set the `storageState` path in [`config.engines.playwright.contextOptions`](https://www.artillery.io/docs/reference/engines/playwright#configuration), instead of a Playwright config file. Note that the [`$dirname` utility](https://www.artillery.io/docs/reference/test-script#test-level-variables) is needed to resolve the full path for Playwright. * A [before hook](https://www.artillery.io/docs/reference/test-script#before-and-after-sections) will run the setup function (`loginUserAndSaveStorage` in this example), rather than referencing it as a Playwright project. * You will need to create the storageState JSON (`storage.json` in this example) file first as an empty object (`{}`), in the same directory where you run the test from. This is because the first time Artillery runs, it will run the `before` hook, and the file referenced in `config` won't be available. That's it! To run the fully configured example, run: ```sh npx artillery run scenario.yml ``` *Note: this example runs with headless disabled, so you can easily observe the login being reused.* ## Running the example in Fargate Want to run 1,000 browsers at the same time? 10,000? more? Run your load tests on AWS Fargate with [built-in support in Artillery](https://www.artillery.io/docs/load-testing-at-scale/aws-fargate). Just make sure to tell Artillery to include the `storage.json` file. For example: ```yaml config: ... includeFiles: - ./storage.json ``` This ensures the file is bundled to Fargate workers correctly. You will also need to make sure the test is running using headless mode. Then, run the test: ```sh npx artillery run:fargate scenario.yml --count 2 ``` *Note: `before` hooks run once per Fargate worker, so the authentication step will run as many times as the `--count` you set.* ## Playwright Version Compatibility It's important to note that Artillery uses specific versions of Playwright, which are listed in our [documentation](https://www.artillery.io/docs/reference/engines/playwright#playwright-compatibility). The `@playwright/test` version installed in your package.json should ideally match the version Artillery is currently using. ================================================ FILE: examples/browser-playwright-reuse-authentication/flow.js ================================================ const { expect } = require('@playwright/test'); const fs = require('node:fs'); async function loginUserAndSaveStorage(page, context) { // NOTE: we use the $dirname utility so Playwright can resolve the full path const storageState = JSON.parse( fs.readFileSync(`${context.vars.$dirname}/storage.json`, 'utf8') ); if (Object.keys(storageState).length > 0) { console.log('Already logged in. Skipping login.'); return; } //1. navigate to page and assert that we are not logged in await page.goto(context.vars.target); await expect(page.getByText('Authentication example')).toBeVisible(); //2. click login button and make sure we are redirected to `/login` await page.getByRole('link', { name: 'Login' }).click(); await page.waitForURL('**/login'); //3. fill in your github username and click login button await page.getByLabel('username').fill(context.vars.githubUsername); await page.getByRole('button', { name: 'Login' }).click(); //4. ensure we are redirected to profile page and logged in await page.waitForURL('**/profile-sg'); await expect(page.getByText('Your GitHub profile')).toBeVisible(); //5. save iron session cookie to storage.json // NOTE: we use the $dirname utility so Playwright can resolve the full path await page .context() .storageState({ path: `${context.vars.$dirname}/storage.json` }); } async function goToProfilePageAndLogout(page, context, _events, test) { const { step } = test; const profileHeaderText = 'Profile (Static Generation, recommended)'; await step('go_to_page', async () => { await page.goto(context.vars.target); await expect(page.getByText(profileHeaderText)).toBeVisible(); }); await step('go_to_profile_page', async () => { await page.getByRole('link', { name: profileHeaderText }).click(); await page.waitForURL('**/profile-sg'); await expect(page.getByText('Your Github Profile')).toBeVisible(); }); await step('logout', async () => { await page.getByRole('link', { name: 'Logout' }).click(); await page.waitForURL('**/login'); }); } module.exports = { loginUserAndSaveStorage, goToProfilePageAndLogout }; ================================================ FILE: examples/browser-playwright-reuse-authentication/package.json ================================================ { "name": "browser-playwright-reuse-authentication", "version": "1.0.0", "description": "Playwright allows you to use `sessionStorage` to reuse authentication in your tests. This can be especially useful in Load Testing, where you might be interested in testing other parts of the application at scale, without having to exercise the login backend (which is sometimes third-party) with every VU.", "main": "flow.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@playwright/test": "1.45.3" } } ================================================ FILE: examples/browser-playwright-reuse-authentication/scenario.yml ================================================ config: target: https://iron-session-example.vercel.app/ phases: - arrivalRate: 1 duration: 10 engines: playwright: launchOptions: headless: false contextOptions: # NOTE: we use the $dirname utility so Playwright can resolve the full path storageState: "{{ $dirname }}/storage.json" processor: ./flow.js variables: githubUsername: "bernardobridge" # NOTE: add this if you want to run the test in fargate. make sure to remove headless:false too # includeFiles: # - ./storage.json before: engine: playwright flowFunction: loginUserAndSaveStorage scenarios: - name: go_to_profile_page_and_logout engine: playwright flowFunction: goToProfilePageAndLogout ================================================ FILE: examples/browser-playwright-reuse-authentication/storage.json ================================================ {} ================================================ FILE: examples/browser-playwright-reuse-typescript/README.md ================================================ # Reusing Typescript Playwright code as Artillery code This example shows you how you can reuse a pure Playwright codebase written in Typescript as Artillery tests. The `e2e/` folder contains the Playwright test (`e2e/tests/get-issues.spec.ts`). The logic in that test has been abstracted to a helper in `e2e/helpers/index.ts`. The `performance` folder contains the Artillery/Playwright test. Using the same helper, we can construct an Artillery test by importing it in our processor file (`./performance/processor.ts`) and calling it as the `testFunction` in our test (`./performance/search-for-ts-doc.yml`). The `target` used matches the `baseURL` from the playwright config in `e2e/playwright.config.ts`. ## Running the tests First, run `npm install`. To run the pure Playwright example: `cd e2e && npx playwright run` To run the same test as an Artillery test: `cd performance && npx artillery run search-for-ts-doc.yml` ## Using a Page Object Model In this example we didn't use a [Page Object Model](https://playwright.dev/docs/pom). However, similar concepts can be applied. You can have a centralised Page Object Model with methods for most UI actions, or even specific user flows, and then just call those as appropriate in both Playwright and Artillery tests. ## Playwright Version Compatibility It's important to note that Artillery uses specific versions of Playwright, which are listed in our [documentation](https://www.artillery.io/docs/reference/engines/playwright#playwright-compatibility). Your regular Playwright tests must use features that are compatible with the versions used by Artillery. The `@playwright/test` version installed in your package.json should ideally match the version Artillery is currently using. ================================================ FILE: examples/browser-playwright-reuse-typescript/e2e/.gitignore ================================================ playwright-report/ test-results/ ================================================ FILE: examples/browser-playwright-reuse-typescript/e2e/helpers/index.ts ================================================ import { type Page, expect } from '@playwright/test'; export const goToDocsAndSearch = async (page: Page, step) => { await step('go_to_artillery_io', async () => { await page.goto('/'); }); await step('go_to_docs', async () => { await page.getByRole('link', { name: 'Docs' }).first().click(); await expect(page).toHaveURL('/docs'); await expect(page.getByText('Get started')).toBeVisible(); }); await step('search_for_ts_doc_and_goto', async () => { await page .getByRole('searchbox', { name: 'Search documentation…' }) .click(); await page.keyboard.type('typescript', { delay: 100 }); await page .getByRole('link', { name: 'processor - load custom code' }) .click(); await expect(page.getByText('processor - load custom code')).toBeVisible(); }); }; ================================================ FILE: examples/browser-playwright-reuse-typescript/e2e/playwright.config.ts ================================================ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: 'tests', reporter: 'html', use: { baseURL: 'https://www.artillery.io/' }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } } ] }); ================================================ FILE: examples/browser-playwright-reuse-typescript/e2e/tests/get-issues.spec.ts ================================================ import { goToDocsAndSearch } from '../helpers'; import { test } from '@playwright/test'; test('search and go to doc page', async ({ page }) => { await goToDocsAndSearch(page, test.step); }); ================================================ FILE: examples/browser-playwright-reuse-typescript/package.json ================================================ { "name": "browser-playwright-reuse-typescript", "version": "1.0.0", "description": "This example shows you how you can reuse a pure Playwright codebase written in Typescript as Artillery tests.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@playwright/test": "1.45.3" } } ================================================ FILE: examples/browser-playwright-reuse-typescript/performance/processor.ts ================================================ import { goToDocsAndSearch } from '../e2e/helpers'; export async function playwrightTest(page, vuContext, events, test) { const { step } = test; await goToDocsAndSearch(page, step); } ================================================ FILE: examples/browser-playwright-reuse-typescript/performance/search-for-ts-doc.yml ================================================ config: target: "https://www.artillery.io/" phases: - duration: 1 arrivalRate: 1 name: "Phase 1" processor: "./processor.ts" engines: playwright: {} scenarios: - engine: playwright testFunction: "playwrightTest" ================================================ FILE: examples/cicd/README.md ================================================ # Artillery CI/CD Examples This repo contains examples of how to integrate [Artillery](https://artillery.io/) with different continuous integration and continuous delivery services. ## CI/CD examples - [github-actions](./github-actions) - a GitHub Actions workflow for running Artillery load tests - [azure-devops](./azure-devops) - an Azure Pipelines setup for running Artillery load tests - [circleci](./circleci) - a CircleCI workflow for running Artillery load tests - [gitlab-ci-cd](./gitlab-ci-cd) - a GitLab CI/CD setup for running Artillery load tests - [jenkins](./jenkins) - a Jenkins Pipeline for running Artillery load tests - [aws-codebuild](./aws-codebuild) - an AWS CodeBuild buildspec for running Artillery load tests # Contributing Would you like to share an example showing how to integrate Artillery with a CI/CD service not covered here? Send us a PR 💜 ================================================ FILE: examples/cicd/aws-codebuild/README.md ================================================ # Load Testing With Artillery and AWS CodeBuild This repo contains an example for running [Artillery](https://artillery.io/) load tests on AWS CodeBuild. For more details, read the ["Integrating Artillery with AWS CodeBuild"](https://artillery.io/docs/guides/integration-guides/aws-codebuild.html) section in the Artillery documentation. ## Artillery test script The [example Artillery script](tests/performance/socket-io.yml) will test a running Socket.IO server. You can run the test script and see it in action: https://repl.artillery.io/?s=4ae41a53-1fa7-4256-9d1c-2a80202c1ca2&hR=true ## AWS CodeBuild buildspec The [included AWS CodeBuild buildspec configuration file](buildspec.yml) is set up to run the load test, generate an HTML report, and store the artifact in an S3 bucket for later retrieval. You can also schedule the load test to run on a recurring schedule using Amazon EventBridge, as explained in the Artillery documentation. ================================================ FILE: examples/cicd/aws-codebuild/buildspec.yml ================================================ version: 0.2 phases: install: commands: - npm install -g artillery@latest pre_build: commands: - mkdir reports build: commands: - artillery run --output reports/report.json tests/performance/socket-io.yml artifacts: files: - 'reports/*' name: artifacts/$CODEBUILD_BUILD_NUMBER ================================================ FILE: examples/cicd/aws-codebuild/tests/performance/socket-io.yml ================================================ config: target: "http://lab.artillery.io" # As an example, we'll only run a single virtual user in this # test script. For real-world load testing, you'll want to # adjust your load phases according to your needs. phases: - duration: 1 arrivalRate: 1 ensure: maxErrorRate: 1 max: 500 scenarios: - name: "emit_an_event" engine: "socketio" flow: - emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" ================================================ FILE: examples/cicd/azure-devops/README.md ================================================ # Load Testing With Artillery and Azure DevOps This repo contains an example for running [Artillery](https://artillery.io/) load tests on [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/) using [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/). For more details, read the ["Integrating Artillery with Azure DevOps"](https://artillery.io/docs/guides/integration-guides/azure-devops.html) section in the Artillery documentation. ## Artillery test script The [example Artillery script](tests/performance/socket-io.yml) will test a running Socket.IO server. You can run the test script and see it in action: https://repl.artillery.io/?s=4ae41a53-1fa7-4256-9d1c-2a80202c1ca2&hR=true ## Azure Pipelines setup The [included Azure Pipelines YAML file](azure-pipelines.yml) will trigger the load test after any code push to the `main` branch of the repository, and is set up to run on a schedule every day at 12:00 AM (UTC). The setup will also generate an HTML report and store the artifact for later retrieval. ================================================ FILE: examples/cicd/azure-devops/azure-pipelines.yml ================================================ trigger: - main schedules: - cron: "0 0 * * *" displayName: 'Midnight (UTC) performance test' branches: include: - main pool: vmImage: ubuntu-latest steps: - task: NodeTool@0 inputs: versionSpec: '12.x' displayName: 'Install Node.js v12.x' - script: npm install -g artillery@latest displayName: 'Install Artillery' - script: mkdir $(System.DefaultWorkingDirectory)/reports displayName: 'Make reports directory' - script: artillery run --output reports/report.json tests/performance/socket-io.yml displayName: 'Execute load tests' - publish: $(System.DefaultWorkingDirectory)/reports artifact: artillery-test-report ================================================ FILE: examples/cicd/azure-devops/tests/performance/socket-io.yml ================================================ config: target: "http://lab.artillery.io" # As an example, we'll only run a single virtual user in this # test script. For real-world load testing, you'll want to # adjust your load phases according to your needs. phases: - duration: 1 arrivalRate: 1 ensure: maxErrorRate: 1 max: 500 scenarios: - name: "emit_an_event" engine: "socketio" flow: - emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" ================================================ FILE: examples/cicd/circleci/.circleci/config.yml ================================================ version: 2.1 jobs: artillery: docker: - image: artilleryio/artillery:latest steps: - checkout - run: name: Make reports directory command: mkdir reports - run: name: Execute load tests command: /home/node/artillery/bin/artillery run --output reports/report.json tests/performance/socket-io.yml - store_artifacts: path: reports workflows: load-tests: jobs: - artillery: filters: branches: only: main nightly: jobs: - artillery triggers: - schedule: cron: "0 0 * * *" filters: branches: only: - main ================================================ FILE: examples/cicd/circleci/README.md ================================================ # Load Testing With Artillery and CircleCI This repo contains an example for running [Artillery](https://artillery.io/) load tests on CircleCI. For more details, read the ["Integrating Artillery with CircleCI"](https://artillery.io/docs/guides/integration-guides/circleci.html) section in the Artillery documentation. ## Artillery test script The [example Artillery script](tests/performance/socket-io.yml) will test a running Socket.IO server. You can run the test script and see it in action: https://repl.artillery.io/?s=4ae41a53-1fa7-4256-9d1c-2a80202c1ca2&hR=true ## CircleCI workflow The [included CircleCI configuration file](.circleci/config.yml) will trigger the load test after any code push to the `main` branch of the repository, and is set up to run on a schedule every day at 12:00 AM (UTC) against the `main` branch. The workflow will also generate an HTML report and store the artifact for later retrieval. ================================================ FILE: examples/cicd/circleci/tests/performance/socket-io.yml ================================================ config: target: "http://lab.artillery.io" # As an example, we'll only run a single virtual user in this # test script. For real-world load testing, you'll want to # adjust your load phases according to your needs. phases: - duration: 1 arrivalRate: 1 ensure: maxErrorRate: 1 max: 500 scenarios: - name: "emit_an_event" engine: "socketio" flow: - emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" ================================================ FILE: examples/cicd/github-actions/.github/workflows/load-test.yml ================================================ name: Artillery Socket.IO Load Test on: push: branches: - main schedule: - cron: '0 0 * * *' jobs: artillery: runs-on: ubuntu-latest container: artilleryio/artillery:latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Make reports directory run: mkdir reports - name: Execute load tests run: /home/node/artillery/bin/run run --output reports/report.json tests/performance/socket-io.yml - name: Generate HTML report run: /home/node/artillery/bin/run report --output reports/report.html reports/report.json - name: Archive test report uses: actions/upload-artifact@v2 with: name: artillery-test-report path: reports/* ================================================ FILE: examples/cicd/github-actions/README.md ================================================ # Load Testing With Artillery and GitHub Actions This repo contains an example for running [Artillery](https://artillery.io/) load tests on GitHub Actions. For more details, read the ["Integrating Artillery with GitHub Actions"](https://artillery.io/docs/guides/integration-guides/github-actions.html) section in the Artillery documentation. ## Artillery test script The [example Artillery script](tests/performance/socket-io.yml) will test a running Socket.IO server. You can run the test script and see it in action: https://repl.artillery.io/?s=4ae41a53-1fa7-4256-9d1c-2a80202c1ca2&hR=true ## GitHub Actions workflow The [included GitHub Actions workflow](.github/workflows/load-test.yml) will trigger the load test after any code push to the `main` branch of the repository, and is set up to run on a schedule every day at 12:00 AM (UTC). The workflow will also generate an HTML report and store the artifact for later retrieval. ================================================ FILE: examples/cicd/github-actions/tests/performance/socket-io.yml ================================================ config: target: "http://lab.artillery.io" # As an example, we'll only run a single virtual user in this # test script. For real-world load testing, you'll want to # adjust your load phases according to your needs. phases: - duration: 1 arrivalRate: 1 ensure: maxErrorRate: 1 max: 500 scenarios: - name: "emit_an_event" engine: "socketio" flow: - emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" ================================================ FILE: examples/cicd/gitlab-ci-cd/.gitlab-ci.yml ================================================ artillery: image: name: artilleryio/artillery:latest entrypoint: [""] script: | mkdir reports /home/node/artillery/bin/artillery run --output reports/report.json tests/performance/socket-io.yml artifacts: paths: - reports ================================================ FILE: examples/cicd/gitlab-ci-cd/README.md ================================================ # Load Testing With Artillery and GitLab CI/CD This repo contains an example for running [Artillery](https://artillery.io/) load tests on GitLab CI/CD. For more details, read the ["Integrating Artillery with GitLab CI/CD"](https://artillery.io/docs/guides/integration-guides/gitlab-ci-cd.html) section in the Artillery documentation. ## Artillery test script The [example Artillery script](tests/performance/socket-io.yml) will test a running Socket.IO server. You can run the test script and see it in action: https://repl.artillery.io/?s=4ae41a53-1fa7-4256-9d1c-2a80202c1ca2&hR=true ## GitHub Actions workflow The [included GitLab CI/CD configuration file](.gitlab-ci.yml) will trigger the load test after any code push to the repository, generate an HTML report and store the artifact for later retrieval. ================================================ FILE: examples/cicd/gitlab-ci-cd/tests/performance/socket-io.yml ================================================ config: target: "http://lab.artillery.io" # As an example, we'll only run a single virtual user in this # test script. For real-world load testing, you'll want to # adjust your load phases according to your needs. phases: - duration: 1 arrivalRate: 1 ensure: maxErrorRate: 1 max: 500 scenarios: - name: "emit_an_event" engine: "socketio" flow: - emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" ================================================ FILE: examples/cicd/jenkins/Jenkinsfile ================================================ pipeline { agent { docker { image 'artilleryio/artillery:latest' args '-u root:root -i --entrypoint=' } } triggers { cron('0 0 * * *') } stages { stage('Load Test') { steps { sh 'mkdir reports' sh '/home/node/artillery/bin/artillery run --output reports/report.json tests/performance/socket-io.yml' } } } post { success { archiveArtifacts 'reports/*' } } } ================================================ FILE: examples/cicd/jenkins/README.md ================================================ # Load Testing With Artillery and Jenkins This repo contains an example for running [Artillery](https://artillery.io/) load tests on Jenkins. For more details, read the ["Integrating Artillery with Jenkins"](https://artillery.io/docs/guides/integration-guides/jenkins.html) section in the Artillery documentation. ## Artillery test script The [example Artillery script](tests/performance/socket-io.yml) will test a running Socket.IO server. You can run the test script and see it in action: https://repl.artillery.io/?s=4ae41a53-1fa7-4256-9d1c-2a80202c1ca2&hR=true ## Jenkins Pipline The [included Jenkins Pipeline configuration](Jenkinsfile) is set up to run the load test on a schedule every day at 12:00 AM (based on the Jenkins server timezone). The Pipeline will also generate an HTML report and store the artifact for later retrieval. ================================================ FILE: examples/cicd/jenkins/tests/performance/socket-io.yml ================================================ config: target: "http://lab.artillery.io" # As an example, we'll only run a single virtual user in this # test script. For real-world load testing, you'll want to # adjust your load phases according to your needs. phases: - duration: 1 arrivalRate: 1 ensure: maxErrorRate: 1 max: 500 scenarios: - name: "emit_an_event" engine: "socketio" flow: - emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" ================================================ FILE: examples/functional-testing-with-expect-plugin/.gitignore ================================================ node_modules ================================================ FILE: examples/functional-testing-with-expect-plugin/README.md ================================================ # Functional testing on Artillery This example shows you how to run both load and functional tests with a single Artillery test script using the `artillery-plugin-expect` plugin. ## Running the API server This example includes an Express.js application running an HTTP API using an in-memory SQLite 3 database. First, install the server dependencies: ```shell npm install ``` After installing the dependencies, start the API server: ```shell node app.js ``` This command will start a server listening at http://localhost:3000/. ## Running Artillery tests This directory contains a test script (`functional-load-tests.yml`) which defines two environments: - `load` - this defines a load phase that generates 25 virtual users per second for 10 minutes. - `functional` - this enables the `artillery-plugin-expect` plugin. We don't want to enable it in the `load` phase as it would generate a lot of console output Once the API server is up and running, you can run either load tests or functional tests using the same test script, using the `--environment` flag. To run load tests: ```shell npx artillery run --environment load functional-load-tests.yml ``` To run functional tests: ```shell npx artillery run --environment functional functional-load-tests.yml ``` ================================================ FILE: examples/functional-testing-with-expect-plugin/app.js ================================================ const express = require('express'); const app = express(); const port = 3000; const sqlite3 = require('sqlite3').verbose(); const db = new sqlite3.Database(':memory:'); app.use(express.json()); app.post('/users', (req, res) => { if (req.body.username === '') { res.status(422).send({ error: 'username is missing' }); return; } db.run( 'INSERT INTO users (username) VALUES (?)', [req.body.username], function (err) { if (err === null) { res.status(201).send({ id: this.lastID, username: req.body.username }); } else { res.status(500).send(err); } } ); }); app.get('/users/:id', (req, res) => { db.get('SELECT * FROM users WHERE id = ?', [req.params.id], (err, row) => { if (err !== null) { res.status(500).send(err); return; } if (row === undefined) { res.status(404).send({ error: 'User not found' }); } else { res.status(200).send(row); } }); }); app.delete('/users/:id', (req, res) => { db.run('DELETE FROM users WHERE id = ?', [req.params.id], function (err) { if (err !== null) { res.status(500).send(err); return; } if (this.changes === 0) { res.status(404).send({ error: 'User not found' }); } else { res.sendStatus(204); } }); }); app.listen(port, () => { db.run(`CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT NOT NULL UNIQUE )`); console.log(`App listening at http://localhost:${port}`); }); ================================================ FILE: examples/functional-testing-with-expect-plugin/functional-load-tests.yml ================================================ config: target: "http://localhost:3000" environments: load: phases: - duration: 10min arrivalRate: 25 functional: # We don't need to specify a phase here. Artillery will # launch a single VU when there is no phase definition. plugins: expect: {} scenarios: - flow: - post: url: "/users" json: username: "new-user" capture: - json: "$.id" as: id expect: - statusCode: 201 - contentType: json - hasProperty: username - equals: - "new-user" - get: url: "/users/{{ id }}" expect: - statusCode: 200 - contentType: json - hasProperty: username - equals: - "new-user" - delete: url: "/users/{{ id }}" expect: - statusCode: 204 ================================================ FILE: examples/functional-testing-with-expect-plugin/package.json ================================================ { "name": "load-and-functional-tests", "version": "1.0.0", "description": "Running load and functional tests from a single Artillery test script", "main": "app.js", "scripts": { "test:load": "artillery run --environment load functional-load-tests.yml", "test:functional": "artillery run --environment functional functional-load-tests.yml" }, "author": "Dennis Martinez", "license": "ISC", "dependencies": { "express": "^4.17.1", "sqlite3": "^5.0.2" } } ================================================ FILE: examples/generating-vu-tokens/README.md ================================================ # generating-vu-tokens A common use-case when testing stateful APIs with Artillery is to generate a token that VUs can use for authentication (e.g. to use with an [OAuth2 Credentials Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow)). This example shows the basic structure of how that can be done with Artillery: - We can use a `before` block and a custom JS function to generate a token that will be shared by all VUs in the test (`sharedToken`) - We can use a `function` step with a custom JS function to generate a token unique to each VU (`vuToken`) - We can use a value stored in a variable (`sharedToken` or `vuToken`) in a request header for authentication (e.g. as a [Bearer token](https://oauth.net/2/bearer-tokens/#:~:text=Bearer%20Tokens%20are%20the%20predominant,such%20as%20JSON%20Web%20Tokens.)) Run the script with: ``` artillery run auth-with-token.yml ``` The output will look similar to the screenshot below. The value of `sharedToken` generated in the `before` block will be the same for every VU, whereas the value of VU-specific token in `vuToken` will differ between VUs. ![auth tokens artillery](./screenshot.png) To turn this into a working test-case for your API, replace the example implementation of one (or both) of the custom JS functions to implement the token generation logic you need, and tweak the way in which those tokens are sent as appropriate for your API. ================================================ FILE: examples/generating-vu-tokens/auth-with-token.yml ================================================ config: target: http://asciiart.artillery.io:8080 processor: "./helpers.js" phases: - arrivalCount: 10 duration: 1 before: flow: - function: "generateSharedToken" scenarios: - flow: - function: "generateVUToken" - log: "VU id: {{ $uuid }}" - log: " shared token is: {{ sharedToken }}" - log: " VU-specific token is: {{ vuToken }}" - get: headers: x-auth-one: "{{ sharedToken }}" x-auth-two: "{{ vuToken }}" url: "/" ================================================ FILE: examples/generating-vu-tokens/helpers.js ================================================ module.exports = { generateSharedToken, generateVUToken }; function generateSharedToken(context, _events, done) { context.vars.sharedToken = `shared-token-${Date.now()}`; return done(); } function generateVUToken(context, _events, done) { context.vars.vuToken = `vu-token-${Date.now()}`; return done(); } ================================================ FILE: examples/graphql-api-server/.gitignore ================================================ node_modules/ prisma/dev.db prisma/dev.db-journal ================================================ FILE: examples/graphql-api-server/README.md ================================================ # Load testing an GraphQL service with Artillery graphql-load-testing-gh This example shows you how to run load tests on a GraphQL API using Artillery. 📖 See the companion blog post: [Using Artillery to load test GraphQL APIs](https://artillery.io/blog/using-artillery-to-load-test-graphql-apis/). ## Running the GraphQL server This example runs a GraphQL server using [Apollo Server](https://www.apollographql.com/docs/apollo-server/) and [Prisma](https://www.prisma.io/) with a SQLite 3 database for data persistence. First, install the server dependencies: ```shell npm install ``` Next, create the SQLite database and set up the required database tables by running the initial Prisma database migration: ```shell npx prisma migrate dev ``` After installing the dependencies and setting up the database, start the GraphQL server: ```shell node app.js ``` This command will start the GraphQL API server listening at http://localhost:4000/. Once the server is up and running, you can explore the server using the [Apollo Sandbox](https://studio.apollographql.com/sandbox/). ## Running Artillery test This directory contains a test script (`graphql.yml`) which demonstrates how to use Artillery scenarios against a GraphQL server. The test script contains a scenario executing various queries and mutations on the GraphQL server. Once the GraphQL server is up and running, execute the test script: ``` npx artillery run graphql.yml ``` ================================================ FILE: examples/graphql-api-server/app.js ================================================ const { ApolloServer, gql } = require('apollo-server'); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const typeDefs = gql` input UserInput { username: String email: String } type User { id: ID! username: String email: String } type Query { users: [User] user(id: ID!): User userByUsername(username: String!): User userByEmail(username: String!): User } type Mutation { createUser(input: UserInput): User updateUser(id: ID!, input: UserInput): User deleteUser(id: ID!): User } `; const resolvers = { Query: { users: async () => { return await prisma.user.findMany(); }, user: async (_, { id }) => { return await prisma.user.findUnique({ where: { id: parseInt(id, 10) } }); }, userByEmail: async (_, { email }) => { return await prisma.user.findUnique({ where: { email } }); }, userByUsername: async (_, { username }) => { return await prisma.user.findUnique({ where: { username } }); } }, Mutation: { createUser: async (_, { input }) => { return await prisma.user.create({ data: input }); }, updateUser: async (_, { id, input }) => { return await prisma.user.update({ where: { id: parseInt(id, 10) }, data: input }); }, deleteUser: async (_, { id }) => { return await prisma.user.delete({ where: { id: parseInt(id, 10) } }); } } }; const server = new ApolloServer({ typeDefs, resolvers }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); }); ================================================ FILE: examples/graphql-api-server/graphql.yaml ================================================ config: target: "http://localhost:4000/" phases: - duration: 60 arrivalRate: 25 scenarios: - name: "crud_from_db_scenario" flow: - post: url: "/" json: query: | mutation CreateUserMutation($createUserInput: UserInput) { createUser(input: $createUserInput) { id } } variables: createUserInput: username: "{{ $randomString() }}" email: "user-{{ $randomString() }}@artillery.io" capture: json: "$.data.createUser.id" as: "userId" - post: url: "/" json: query: | query UserQuery($userId: ID!) { user(id: $userId) { username email } } variables: userId: "{{ userId }}" - post: url: "/" json: query: | mutation UpdateUserMutation($userId: ID!, $updateUserInput: UserInput) { updateUser(id: $userId, input: $updateUserInput) { username email } } variables: userId: "{{ userId }}" updateUserInput: email: "user-{{ $randomString() }}@artillery.io" - post: url: "/" json: query: | mutation DeleteUserMutation($userId: ID!) { deleteUser(id: $userId) { id } } variables: userId: "{{ userId }}" ================================================ FILE: examples/graphql-api-server/package.json ================================================ { "name": "graphql-api-server", "version": "1.0.0", "description": "Load testing a GraphQL API using Artillery", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Dennis Martinez", "license": "ISC", "dependencies": { "@prisma/client": "^3.1.1", "apollo-server": "^3.3.0", "graphql": "^15.6.0" }, "devDependencies": { "prisma": "^3.1.1" } } ================================================ FILE: examples/graphql-api-server/prisma/migrations/20211005051218_init/migration.sql ================================================ -- CreateTable CREATE TABLE "User" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "username" TEXT NOT NULL, "email" TEXT NOT NULL ); -- CreateIndex CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); -- CreateIndex CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); ================================================ FILE: examples/graphql-api-server/prisma/migrations/migration_lock.toml ================================================ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) provider = "sqlite" ================================================ FILE: examples/graphql-api-server/prisma/schema.prisma ================================================ datasource db { provider = "sqlite" url = "file:./dev.db" } generator client { provider = "prisma-client-js" } model User { id Int @id @default(autoincrement()) username String @unique email String @unique } ================================================ FILE: examples/http-file-uploads/.gitignore ================================================ node_modules uploads/* !uploads/.keep ================================================ FILE: examples/http-file-uploads/README.md ================================================ # HTTP file uploads This example shows you how to perform HTTP file uploads from an Artillery test script. ## Running the HTTP server This example includes an Express.js application running an HTTP server. First, install the server dependencies: ```shell npm install ``` After installing the dependencies, start the HTTP server: ```shell npm run app:start ``` This command will start an HTTP server listening at http://localhost:3000/. ## Running Artillery tests This directory contains a test script (`file-uploads.yml`) which demonstrates how you can upload files in your scenarios. Once the HTTP server is up and running, execute the test script: ``` artillery run file-uploads.yml ``` ## Cleaning up uploaded files During the test run, Artillery will upload files to the HTTP server, which get stored in the `uploads` directory. For convenience, you can clean the directory by executing the following command: ``` npm run uploads:clean ``` ================================================ FILE: examples/http-file-uploads/app.js ================================================ const express = require('express'); const app = express(); const upload = require('multer')({ dest: 'uploads/', preservePath: true }); const port = 3000; app.post('/upload', upload.single('document'), (req, res) => { const { originalname, mimetype, size } = req.file; res.json({ originalname, mimetype, size }); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); }); ================================================ FILE: examples/http-file-uploads/file-uploads.yml ================================================ config: target: "http://localhost:3000" phases: - duration: 10min arrivalRate: 25 # To randomize the files to upload during the test scenario, # set up variables with the names of the files to use. These # files are placed in the `/files` directory. variables: filename: - "artillery-logo.jpg" - "artillery-installation.pdf" - "sre-fundamental-rules.png" scenarios: - flow: # The HTTP server has an endpoint (POST /upload) that accepts files # through the `document` field. - post: url: "/upload" formData: document: # The `fromFile` attribute tells Artillery to upload the # specified file. If the file cannot be read, this scenario # will report an ENOENT error. fromFile: "./files/{{ filename }}" ================================================ FILE: examples/http-file-uploads/package.json ================================================ { "name": "file-uploads", "version": "1.0.0", "description": "How to load-test HTTP file uploads with Artillery Pro", "main": "app.js", "scripts": { "test": "artillery run file-uploads.yml", "app:start": "node app.js", "uploads:clean": "del-cli 'uploads/*' '!uploads/.keep'" }, "author": "Dennis Martinez", "license": "ISC", "dependencies": { "del-cli": "^4.0.1", "express": "^4.17.1", "multer": "^1.4.2" } } ================================================ FILE: examples/http-file-uploads/uploads/.keep ================================================ ================================================ FILE: examples/http-metrics-by-endpoint/endpoint-metrics.yml ================================================ config: target: http://asciiart.artillery.io:8080 phases: - duration: 20 arrivalRate: 1 plugins: metrics-by-endpoint: {} scenarios: - flow: - get: url: "/dino" - get: url: "/armadillo" - get: url: "/pony" ================================================ FILE: examples/http-set-custom-header/README.md ================================================ # Set custom HTTP header via custom JS function This example shows how an HTTP header can be set via a hook function. --- To test, run `nc` in server mode with: ```shell nc -l 31337 ``` and then run Artillery: ```shell artillery run set-header.yml ``` You should see custom header `X-Dino` being set: ![Artillery custom HTTP header](./custom-header-artillery.png) Tada! 🎉 ================================================ FILE: examples/http-set-custom-header/helpers.js ================================================ const https = require('node:https'); // Set the value of a header to a custom value, which // in this example comes from an HTTP call to another // API function setCustomHeader(req, _userContext, _ee, next) { let data = ''; https .get('https://api.artillery.io/v1/dino', (res) => { res.on('data', (d) => { data += d; }); res.on('end', () => { // Extract a string composed of letters + spaces + punctuation: const val = data.match(/^<([A-Za-z!\s]+)/m)[1].trim(); // Use that as the value of our custom header: req.headers['x-dino'] = val; return next(); }); }) .on('error', (e) => { return next(e); }); } module.exports = { setCustomHeader }; ================================================ FILE: examples/http-set-custom-header/set-header.yml ================================================ config: target: "http://localhost:31337" processor: "./helpers.js" phases: - duration: 1 arrivalCount: 1 scenarios: - name: set_custom_header flow: - get: url: "/" qs: foo: bar headers: content-type: application/json accept: application/json json: {} beforeRequest: setCustomHeader ================================================ FILE: examples/http-socketio-server/.gitignore ================================================ node_modules/ ================================================ FILE: examples/http-socketio-server/README.md ================================================ # HTTP and Socket.IO server ## Running the server First, install the server dependencies: ```shell npm install ``` After installing the dependencies, start the server: ```shell npm start ``` ##### HTTP `GET /movies` returns a list of all the movies ```json [ { "id": 1, "releaseDate": "Dec 18 1985", "director": "Terry Gilliam", "title": "Brazil", "genre": "Black Comedy", "imdbRating": 8, "runningTimeMin": 136 }, { "id": 2, "releaseDate": "Feb 16 1996", "director": "Harold Becker", "title": "City Hall", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 111 } ... ] ``` `GET /movies/:id` returns a single movie by its id ```json { "id": 35, "releaseDate": "Oct 01 1999", "director": "David O. Russell", "title": "Three Kings", "genre": "Action", "imdbRating": 7.3, "runningTimeMin": 115 } ``` ##### socket.io The socket.io server listens to `echo` events and emits `echoResponse` events to the client, passing the received payload back ================================================ FILE: examples/http-socketio-server/app.js ================================================ const http = require('node:http'); const app = require('./http'); const socketio = require('./socketio'); const { log } = console; const PORT = process.env.PORT || 3000; const server = http.createServer(app); socketio(server); server.listen(PORT, () => { log(`Server listening on port ${PORT}`); }); ================================================ FILE: examples/http-socketio-server/data/movies.json ================================================ [ { "id": 1, "releaseDate": "Dec 18 1985", "director": "Terry Gilliam", "title": "Brazil", "genre": "Black Comedy", "imdbRating": 8, "runningTimeMin": 136 }, { "id": 2, "releaseDate": "Feb 16 1996", "director": "Harold Becker", "title": "City Hall", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 111 }, { "id": 3, "releaseDate": "Jul 12 1996", "director": "Edward Zwick", "title": "Courage Under Fire", "genre": "Drama", "imdbRating": 6.6, "runningTimeMin": 115 }, { "id": 4, "releaseDate": "May 31 1996", "director": "Rob Cohen", "title": "Dragonheart", "genre": "Adventure", "imdbRating": 6.2, "runningTimeMin": 108 }, { "id": 5, "releaseDate": "Jan 19 1996", "director": "Robert Rodriguez", "title": "From Dusk Till Dawn", "genre": "Horror", "imdbRating": 7.1, "runningTimeMin": 107 }, { "id": 6, "releaseDate": "Mar 08 1996", "director": "Joel Coen", "title": "Fargo", "genre": "Thriller/Suspense", "imdbRating": 8.3, "runningTimeMin": 87 }, { "id": 7, "releaseDate": "Oct 11 1996", "director": "Stephen Hopkins", "title": "The Ghost and the Darkness", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 109 }, { "id": 8, "releaseDate": "Feb 16 1996", "director": "Dennis Dugan", "title": "Happy Gilmore", "genre": "Comedy", "imdbRating": 6.9, "runningTimeMin": 92 }, { "id": 9, "releaseDate": "Jul 02 1996", "director": "Roland Emmerich", "title": "Independence Day", "genre": "Adventure", "imdbRating": 6.5, "runningTimeMin": 145 }, { "id": 10, "releaseDate": "Jun 13 1980", "director": "Michael Ritchie", "title": "The Island", "genre": "Adventure", "imdbRating": 6.9, "runningTimeMin": 138 }, { "id": 11, "releaseDate": "Jul 26 1996", "director": "Bobby Farrelly", "title": "Kingpin", "genre": "Comedy", "imdbRating": 6.7, "runningTimeMin": 113 }, { "id": 12, "releaseDate": "Oct 11 1996", "director": "Renny Harlin", "title": "The Long Kiss Goodnight", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 120 }, { "id": 13, "releaseDate": "Jul 18 1986", "director": "James Cameron", "title": "Aliens", "genre": "Action", "imdbRating": 7.5, "runningTimeMin": 137 }, { "id": 14, "releaseDate": "Dec 13 1996", "director": "Tim Burton", "title": "Mars Attacks!", "genre": "Comedy", "imdbRating": 6.3, "runningTimeMin": 110 }, { "id": 15, "releaseDate": "Nov 15 1996", "director": "Barbra Streisand", "title": "The Mirror Has Two Faces", "genre": "Romantic Comedy", "imdbRating": 6, "runningTimeMin": 127 }, { "id": 16, "releaseDate": "May 21 1996", "director": "Brian De Palma", "title": "Mission: Impossible", "genre": "Action", "imdbRating": 6.9, "runningTimeMin": 110 }, { "id": 17, "releaseDate": "Nov 15 1996", "director": "Anthony Minghella", "title": "The English Patient", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 160 }, { "id": 18, "releaseDate": "Jul 05 1996", "director": "Jon Turteltaub", "title": "Phenomenon", "genre": "Drama", "imdbRating": 6.3, "runningTimeMin": 124 }, { "id": 19, "releaseDate": "Nov 08 1996", "director": "Ron Howard", "title": "Ransom", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 121 }, { "id": 20, "releaseDate": "Nov 01 1996", "director": "Baz Luhrmann", "title": "Romeo+Juliet", "genre": "Drama", "imdbRating": 6.5, "runningTimeMin": 120 }, { "id": 21, "releaseDate": "Jun 07 1996", "director": "Michael Bay", "title": "The Rock", "genre": "Action", "imdbRating": 7.2, "runningTimeMin": 136 }, { "id": 22, "releaseDate": "Nov 22 1996", "director": "Scott Hicks", "title": "Shine", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 105 }, { "id": 23, "releaseDate": "Nov 06 1996", "director": "F. Gary Gray", "title": "Set It Off", "genre": "Drama", "imdbRating": 6.3, "runningTimeMin": 120 }, { "id": 24, "releaseDate": "Oct 18 1996", "director": "Barry Levinson", "title": "Sleepers", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 105 }, { "id": 25, "releaseDate": "Aug 06 2004", "director": "Ryan Little", "title": "Saints and Soldiers", "genre": "Drama", "imdbRating": 7, "runningTimeMin": 90 }, { "id": 26, "releaseDate": "Aug 16 1996", "director": "Ron Shelton", "title": "Tin Cup", "genre": "Romantic Comedy", "imdbRating": 6.1, "runningTimeMin": 105 }, { "id": 27, "releaseDate": "Jul 19 1996", "director": "Danny Boyle", "title": "Trainspotting", "genre": "Drama", "imdbRating": 8.2, "runningTimeMin": 94 }, { "id": 28, "releaseDate": "Jul 24 1996", "director": "Joel Schumacher", "title": "A Time to Kill", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 150 }, { "id": 29, "releaseDate": "Oct 04 1996", "director": "Tom Hanks", "title": "That Thing You Do!", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 110 }, { "id": 30, "releaseDate": "May 10 1996", "director": "Jan De Bont", "title": "Twister", "genre": "Action", "imdbRating": 6, "runningTimeMin": 117 }, { "id": 31, "releaseDate": "Apr 23 2004", "director": "Gary Winick", "title": "13 Going On 30", "genre": "Comedy", "imdbRating": 6.1, "runningTimeMin": 98 }, { "id": 32, "releaseDate": "Nov 13 2009", "director": "Roland Emmerich", "title": 2012, "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 158 }, { "id": 33, "releaseDate": "Jun 27 2003", "director": "Danny Boyle", "title": "28 Days Later...", "genre": "Horror", "imdbRating": 7.6, "runningTimeMin": 113 }, { "id": 34, "releaseDate": "Mar 09 2007", "director": "Zack Snyder", "title": 300, "genre": "Action", "imdbRating": 7.8, "runningTimeMin": 117 }, { "id": 35, "releaseDate": "Oct 01 1999", "director": "David O. Russell", "title": "Three Kings", "genre": "Action", "imdbRating": 7.3, "runningTimeMin": 115 }, { "id": 36, "releaseDate": "Sep 02 2007", "director": "James Mangold", "title": "3:10 to Yuma", "genre": "Western", "imdbRating": 7.9, "runningTimeMin": 117 }, { "id": 37, "releaseDate": "Aug 19 2005", "director": "Judd Apatow", "title": "The 40 Year-old Virgin", "genre": "Comedy", "imdbRating": 7.5, "runningTimeMin": 111 }, { "id": 38, "releaseDate": "Aug 12 2005", "director": "John Singleton", "title": "Four Brothers", "genre": "Drama", "imdbRating": 6.8, "runningTimeMin": 109 }, { "id": 39, "releaseDate": "Feb 13 2004", "director": "Peter Segal", "title": "50 First Dates", "genre": "Romantic Comedy", "imdbRating": 6.8, "runningTimeMin": 99 }, { "id": 40, "releaseDate": "Nov 08 2002", "director": "Curtis Hanson", "title": "8 Mile", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 110 }, { "id": 41, "releaseDate": "May 17 2002", "director": "Paul Weitz", "title": "About a Boy", "genre": "Romantic Comedy", "imdbRating": 7.4, "runningTimeMin": 101 }, { "id": 42, "releaseDate": "Nov 20 1998", "director": "John Lasseter", "title": "A Bug's Life", "genre": "Adventure", "imdbRating": 7.3, "runningTimeMin": 96 }, { "id": 43, "releaseDate": "Feb 14 1997", "director": "Clint Eastwood", "title": "Absolute Power", "genre": "Thriller/Suspense", "imdbRating": 6.5, "runningTimeMin": 120 }, { "id": 44, "releaseDate": "Nov 12 2004", "director": "Brett Ratner", "title": "After the Sunset", "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 97 }, { "id": 45, "releaseDate": "Nov 10 2006", "director": "Ridley Scott", "title": "A Good Year", "genre": "Drama", "imdbRating": 6.8, "runningTimeMin": 118 }, { "id": 46, "releaseDate": "Jul 25 1997", "director": "Wolfgang Petersen", "title": "Air Force One", "genre": "Action", "imdbRating": 6.3, "runningTimeMin": 124 }, { "id": 47, "releaseDate": "Sep 22 2006", "director": "Steven Zaillian", "title": "All the King's Men", "genre": "Drama", "imdbRating": 6, "runningTimeMin": 128 }, { "id": 48, "releaseDate": "Dec 25 2001", "director": "Michael Mann", "title": "Ali", "genre": "Drama", "imdbRating": 6.6, "runningTimeMin": 159 }, { "id": 49, "releaseDate": "Nov 26 1997", "director": "Jean-Pierre Jeunet", "title": "Alien: Resurrection", "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 108 }, { "id": 50, "releaseDate": "Sep 15 2000", "director": "Cameron Crowe", "title": "Almost Famous", "genre": "Comedy", "imdbRating": 8, "runningTimeMin": 123 }, { "id": 51, "releaseDate": "May 23 2003", "director": "Tom Shadyac", "title": "Bruce Almighty", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 101 }, { "id": 52, "releaseDate": "Apr 06 2001", "director": "Lee Tamahori", "title": "Along Came a Spider", "genre": "Thriller/Suspense", "imdbRating": 6.1, "runningTimeMin": 103 }, { "id": 53, "releaseDate": "Sep 15 1999", "director": "Sam Mendes", "title": "American Beauty", "genre": "Drama", "imdbRating": 8.6, "runningTimeMin": 118 }, { "id": 54, "releaseDate": "Nov 02 2001", "director": "Jean-Pierre Jeunet", "title": "Le Fabuleux destin d'AmÈlie Poulain", "genre": "Comedy", "imdbRating": 8.5, "runningTimeMin": 122 }, { "id": 55, "releaseDate": "Nov 02 2007", "director": "Ridley Scott", "title": "American Gangster", "genre": "Drama", "imdbRating": 7.9, "runningTimeMin": 157 }, { "id": 56, "releaseDate": "Dec 12 1997", "director": "Steven Spielberg", "title": "Amistad", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 152 }, { "id": 57, "releaseDate": "Jul 09 1999", "director": "Paul Weitz", "title": "American Pie", "genre": "Comedy", "imdbRating": 6.9, "runningTimeMin": 95 }, { "id": 58, "releaseDate": "Nov 14 1997", "director": "Don Bluth", "title": "Anastasia", "genre": "Musical", "imdbRating": 6.6, "runningTimeMin": 94 }, { "id": 59, "releaseDate": "Jul 09 2004", "director": "Adam McKay", "title": "Anchorman: The Legend of Ron Burgundy", "genre": "Comedy", "imdbRating": 7, "runningTimeMin": 104 }, { "id": 60, "releaseDate": "Apr 11 2003", "director": "Peter Segal", "title": "Anger Management", "genre": "Comedy", "imdbRating": 6.1, "runningTimeMin": 106 }, { "id": 61, "releaseDate": "Dec 17 1999", "director": "Andy Tennant", "title": "Anna and the King", "genre": "Drama", "imdbRating": 6.5, "runningTimeMin": 147 }, { "id": 62, "releaseDate": "Mar 05 1999", "director": "Harold Ramis", "title": "Analyze This", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 103 }, { "id": 63, "releaseDate": "Oct 02 1998", "director": "Tim Johnson", "title": "Antz", "genre": "Adventure", "imdbRating": 6.8, "runningTimeMin": 83 }, { "id": 64, "releaseDate": "Dec 08 2006", "director": "Mel Gibson", "title": "Apocalypto", "genre": "Adventure", "imdbRating": 7.9, "runningTimeMin": 136 }, { "id": 65, "releaseDate": "Dec 17 1997", "director": "Robert Duvall", "title": "The Apostle", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 148 }, { "id": 66, "releaseDate": "Jul 01 1998", "director": "Michael Bay", "title": "Armageddon", "genre": "Adventure", "imdbRating": 6.1, "runningTimeMin": 150 }, { "id": 67, "releaseDate": "Jul 07 2004", "director": "Antoine Fuqua", "title": "King Arthur", "genre": "Adventure", "imdbRating": 6.2, "runningTimeMin": 126 }, { "id": 68, "releaseDate": "Jun 29 2001", "director": "Steven Spielberg", "title": "Artificial Intelligence: AI", "genre": "Drama", "imdbRating": 6.9, "runningTimeMin": 146 }, { "id": 69, "releaseDate": "Oct 23 2009", "director": "David Bowers", "title": "Astro Boy", "genre": "Adventure", "imdbRating": 6.4, "runningTimeMin": 94 }, { "id": 70, "releaseDate": "Feb 23 2007", "director": "Michael Polish", "title": "The Astronaut Farmer", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 109 }, { "id": 71, "releaseDate": "Dec 24 1997", "director": "James L. Brooks", "title": "As Good as it Gets", "genre": "Romantic Comedy", "imdbRating": 7.8, "runningTimeMin": 138 }, { "id": 72, "releaseDate": "Dec 11 1998", "director": "Sam Raimi", "title": "A Simple Plan", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 121 }, { "id": 73, "releaseDate": "Jun 11 2010", "director": "Joe Carnahan", "title": "The A-Team", "genre": "Action", "imdbRating": 7.2, "runningTimeMin": 119 }, { "id": 74, "releaseDate": "Jun 08 2001", "director": "Gary Trousdale", "title": "Atlantis: The Lost Empire", "genre": "Adventure", "imdbRating": 6.4, "runningTimeMin": 96 }, { "id": 75, "releaseDate": "Dec 07 2007", "director": "Joe Wright", "title": "Atonement", "genre": "Drama", "imdbRating": 7.9, "runningTimeMin": 130 }, { "id": 76, "releaseDate": "Jun 10 1999", "director": "Jay Roach", "title": "Austin Powers: The Spy Who Shagged Me", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 95 }, { "id": 77, "releaseDate": "Jul 25 2002", "director": "Jay Roach", "title": "Austin Powers in Goldmember", "genre": "Comedy", "imdbRating": 6.2, "runningTimeMin": 94 }, { "id": 78, "releaseDate": "May 02 1997", "director": "Jay Roach", "title": "Austin Powers: International Man of Mystery", "genre": "Comedy", "imdbRating": 7.1, "runningTimeMin": 89 }, { "id": 79, "releaseDate": "Dec 17 2004", "director": "Martin Scorsese", "title": "The Aviator", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 170 }, { "id": 80, "releaseDate": "Jun 05 2009", "director": "Sam Mendes", "title": "Away We Go", "genre": "Comedy", "imdbRating": 7.3, "runningTimeMin": 98 }, { "id": 81, "releaseDate": "Nov 25 1998", "director": "George Miller", "title": "Babe: Pig in the City", "genre": "Adventure", "imdbRating": 6.1, "runningTimeMin": 75 }, { "id": 82, "releaseDate": "Jul 18 2003", "director": "Michael Bay", "title": "Bad Boys II", "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 147 }, { "id": 83, "releaseDate": "Mar 28 2003", "director": "John McTiernan", "title": "Basic", "genre": "Thriller/Suspense", "imdbRating": 6.3, "runningTimeMin": 98 }, { "id": 84, "releaseDate": "Jun 15 2005", "director": "Christopher Nolan", "title": "Batman Begins", "genre": "Action", "imdbRating": 8.3, "runningTimeMin": 140 }, { "id": 85, "releaseDate": "Jul 18 2008", "director": "Christopher Nolan", "title": "The Dark Knight", "genre": "Action", "imdbRating": 8.9, "runningTimeMin": 152 }, { "id": 86, "releaseDate": "Jun 27 2001", "director": "John Singleton", "title": "Baby Boy", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 130 }, { "id": 87, "releaseDate": "Dec 25 2008", "director": "David Fincher", "title": "The Curious Case of Benjamin Button", "genre": "Drama", "imdbRating": 8, "runningTimeMin": 167 }, { "id": 88, "releaseDate": "Oct 10 2008", "director": "Ridley Scott", "title": "Body of Lies", "genre": "Thriller/Suspense", "imdbRating": 7.2, "runningTimeMin": 129 }, { "id": 89, "releaseDate": "Dec 08 2006", "director": "Edward Zwick", "title": "Blood Diamond", "genre": "Action", "imdbRating": 8, "runningTimeMin": 143 }, { "id": 90, "releaseDate": "Dec 20 1996", "director": "Mike Judge", "title": "Beavis and Butt-head Do America", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 80 }, { "id": 91, "releaseDate": "Mar 12 2003", "director": "Gurinder Chadha", "title": "Bend it Like Beckham", "genre": "Drama", "imdbRating": 6.9, "runningTimeMin": 112 }, { "id": 92, "releaseDate": "Nov 23 2001", "director": "Todd Field", "title": "In the Bedroom", "genre": "Drama", "imdbRating": 7.5, "runningTimeMin": 130 }, { "id": 93, "releaseDate": "Nov 02 2007", "director": "Steve Hickner", "title": "Bee Movie", "genre": "Comedy", "imdbRating": 6.3, "runningTimeMin": 90 }, { "id": 94, "releaseDate": "Oct 29 1999", "director": "Spike Jonze", "title": "Being John Malkovich", "genre": "Black Comedy", "imdbRating": 7.9, "runningTimeMin": 112 }, { "id": 95, "releaseDate": "Nov 16 2007", "director": "Robert Zemeckis", "title": "Beowulf", "genre": "Adventure", "imdbRating": 6.6, "runningTimeMin": 115 }, { "id": 96, "releaseDate": "Dec 10 2003", "director": "Tim Burton", "title": "Big Fish", "genre": "Drama", "imdbRating": 8.1, "runningTimeMin": 125 }, { "id": 97, "releaseDate": "Mar 06 1998", "director": "Joel Coen", "title": "The Big Lebowski", "genre": "Comedy", "imdbRating": 8.2, "runningTimeMin": 127 }, { "id": 98, "releaseDate": "Dec 28 2001", "director": "Ridley Scott", "title": "Black Hawk Down", "genre": "Action", "imdbRating": 7.7, "runningTimeMin": 144 }, { "id": 99, "releaseDate": "Oct 13 2000", "director": "Stephen Daldry", "title": "Billy Elliot", "genre": "Drama", "imdbRating": 7.7, "runningTimeMin": 110 }, { "id": 100, "releaseDate": "Dec 17 1999", "director": "Chris Columbus", "title": "Bicentennial Man", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 132 }, { "id": 101, "releaseDate": "Aug 21 1998", "director": "Stephen Norrington", "title": "Blade", "genre": "Action", "imdbRating": 7, "runningTimeMin": 121 }, { "id": 102, "releaseDate": "Feb 12 1999", "director": "Hugh Wilson", "title": "Blast from the Past", "genre": "Comedy", "imdbRating": 6.4, "runningTimeMin": 111 }, { "id": 103, "releaseDate": "Aug 09 2002", "director": "Clint Eastwood", "title": "Blood Work", "genre": "Thriller/Suspense", "imdbRating": 6.3, "runningTimeMin": 110 }, { "id": 104, "releaseDate": "Jul 13 2001", "director": "Robert Luketic", "title": "Legally Blonde", "genre": "Comedy", "imdbRating": 6.2, "runningTimeMin": 97 }, { "id": 105, "releaseDate": "Apr 06 2001", "director": "Ted Demme", "title": "Blow", "genre": "Drama", "imdbRating": 7.4, "runningTimeMin": 123 }, { "id": 106, "releaseDate": "Dec 21 2001", "director": "Ron Howard", "title": "A Beautiful Mind", "genre": "Drama", "imdbRating": 8, "runningTimeMin": 135 }, { "id": 107, "releaseDate": "Oct 12 2001", "director": "Barry Levinson", "title": "Bandits", "genre": "Comedy", "imdbRating": 6.5, "runningTimeMin": 123 }, { "id": 108, "releaseDate": "Nov 17 2006", "director": "Emilio Estevez", "title": "Bobby", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 120 }, { "id": 109, "releaseDate": "Nov 05 1999", "director": "Phillip Noyce", "title": "The Bone Collector", "genre": "Thriller/Suspense", "imdbRating": 6.3, "runningTimeMin": 118 }, { "id": 110, "releaseDate": "Oct 10 1997", "director": "Paul Thomas Anderson", "title": "Boogie Nights", "genre": "Drama", "imdbRating": 7.9, "runningTimeMin": 152 }, { "id": 111, "releaseDate": "Nov 03 2006", "director": "Larry Charles", "title": "Borat", "genre": "Comedy", "imdbRating": 7.7, "runningTimeMin": 83 }, { "id": 112, "releaseDate": "Jun 14 2002", "director": "Doug Liman", "title": "The Bourne Identity", "genre": "Action", "imdbRating": 7.7, "runningTimeMin": 110 }, { "id": 113, "releaseDate": "Jul 23 2004", "director": "Paul Greengrass", "title": "The Bourne Supremacy", "genre": "Action", "imdbRating": 7.6, "runningTimeMin": 108 }, { "id": 114, "releaseDate": "Aug 03 2007", "director": "Paul Greengrass", "title": "The Bourne Ultimatum", "genre": "Action", "imdbRating": 8.2, "runningTimeMin": 114 }, { "id": 115, "releaseDate": "Oct 22 1999", "director": "Martin Scorsese", "title": "Bringing Out The Dead", "genre": "Black Comedy", "imdbRating": 6.8, "runningTimeMin": 120 }, { "id": 116, "releaseDate": "Dec 09 2005", "director": "Ang Lee", "title": "Brokeback Mountain", "genre": "Drama", "imdbRating": 7.8, "runningTimeMin": 134 }, { "id": 117, "releaseDate": "Sep 13 2002", "director": "Tim Story", "title": "Barbershop", "genre": "Comedy", "imdbRating": 6.2, "runningTimeMin": 103 }, { "id": 118, "releaseDate": "Sep 27 2000", "director": "Christopher Guest", "title": "Best in Show", "genre": "Comedy", "imdbRating": 7.4, "runningTimeMin": 90 }, { "id": 119, "releaseDate": "Nov 26 2003", "director": "Terry Zwigoff", "title": "Bad Santa", "genre": "Comedy", "imdbRating": 7.3, "runningTimeMin": 91 }, { "id": 120, "releaseDate": "Aug 21 2009", "director": "Quentin Tarantino", "title": "Inglourious Basterds", "genre": "Action", "imdbRating": 8.4, "runningTimeMin": 152 }, { "id": 121, "releaseDate": "May 15 1998", "director": "Warren Beatty", "title": "Bulworth", "genre": "Comedy", "imdbRating": 6.8, "runningTimeMin": 107 }, { "id": 122, "releaseDate": "Aug 13 1999", "director": "Frank Oz", "title": "Bowfinger", "genre": "Comedy", "imdbRating": 6.4, "runningTimeMin": 96 }, { "id": 123, "releaseDate": "Dec 22 2000", "director": "Robert Zemeckis", "title": "Cast Away", "genre": "Drama", "imdbRating": 7.5, "runningTimeMin": 144 }, { "id": 124, "releaseDate": "Sep 10 2004", "director": "David R. Ellis", "title": "Cellular", "genre": "Thriller/Suspense", "imdbRating": 6.5, "runningTimeMin": 94 }, { "id": 125, "releaseDate": "Oct 10 2008", "director": "Gil Kenan", "title": "City of Ember", "genre": "Adventure", "imdbRating": 6.4, "runningTimeMin": 94 }, { "id": 126, "releaseDate": "Jul 15 2005", "director": "Tim Burton", "title": "Charlie and the Chocolate Factory", "genre": "Comedy", "imdbRating": 7.1, "runningTimeMin": 115 }, { "id": 127, "releaseDate": "Oct 27 2006", "director": "Phillip Noyce", "title": "Catch a Fire", "genre": "Thriller/Suspense", "imdbRating": 6.8, "runningTimeMin": 101 }, { "id": 128, "releaseDate": "Dec 27 2002", "director": "Rob Marshall", "title": "Chicago", "genre": "Musical", "imdbRating": 7.2, "runningTimeMin": 113 }, { "id": 129, "releaseDate": "Jun 21 2000", "director": "Nick Park", "title": "Chicken Run", "genre": "Adventure", "imdbRating": 7.3, "runningTimeMin": 84 }, { "id": 130, "releaseDate": "Dec 25 2006", "director": "Alfonso Cuaron", "title": "Children of Men", "genre": "Thriller/Suspense", "imdbRating": 8.1, "runningTimeMin": 114 }, { "id": 131, "releaseDate": "Nov 16 2007", "director": "Mike Newell", "title": "Love in the Time of Cholera", "genre": "Drama", "imdbRating": 6.2, "runningTimeMin": 139 }, { "id": 132, "releaseDate": "Dec 15 2000", "director": "Lasse Hallstrom", "title": "Chocolat", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 122 }, { "id": 133, "releaseDate": "Dec 15 2006", "director": "Gary Winick", "title": "Charlotte's Web", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 98 }, { "id": 134, "releaseDate": "Jun 03 2005", "director": "Ron Howard", "title": "Cinderella Man", "genre": "Drama", "imdbRating": 8, "runningTimeMin": 144 }, { "id": 135, "releaseDate": "Apr 10 1998", "director": "Brad Silberling", "title": "City of Angels", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 117 }, { "id": 136, "releaseDate": "Dec 25 1998", "director": "Steven Zaillian", "title": "A Civil Action", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 112 }, { "id": 137, "releaseDate": "Dec 25 2003", "director": "Anthony Minghella", "title": "Cold Mountain", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 152 }, { "id": 138, "releaseDate": "Feb 18 2005", "director": "Francis Lawrence", "title": "Constantine", "genre": "Action", "imdbRating": 6.7, "runningTimeMin": 122 }, { "id": 139, "releaseDate": "Aug 06 2004", "director": "Michael Mann", "title": "Collateral", "genre": "Action", "imdbRating": 7.8, "runningTimeMin": 120 }, { "id": 140, "releaseDate": "Jun 06 1997", "director": "Simon West", "title": "Con Air", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 115 }, { "id": 141, "releaseDate": "Aug 08 1997", "director": "Richard Donner", "title": "Conspiracy Theory", "genre": "Thriller/Suspense", "imdbRating": 6.5, "runningTimeMin": 135 }, { "id": 142, "releaseDate": "Jul 11 1997", "director": "Robert Zemeckis", "title": "Contact", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 150 }, { "id": 143, "releaseDate": "Aug 15 1997", "director": "James Mangold", "title": "Cop Land", "genre": "Drama", "imdbRating": 6.9, "runningTimeMin": 105 }, { "id": 144, "releaseDate": "Dec 21 2006", "director": "Yimou Zhang", "title": "Man cheng jin dai huang jin jia", "genre": "Action", "imdbRating": 7, "runningTimeMin": 113 }, { "id": 145, "releaseDate": "May 06 2005", "director": "Paul Haggis", "title": "Crash", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 107 }, { "id": 146, "releaseDate": "Jan 25 2002", "director": "Kevin Reynolds", "title": "The Count of Monte Cristo", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 131 }, { "id": 147, "releaseDate": "Mar 05 1999", "director": "Roger Kumble", "title": "Cruel Intentions", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 95 }, { "id": 148, "releaseDate": "Apr 16 2010", "director": "James Ivory", "title": "The City of Your Final Destination", "genre": "Drama", "imdbRating": 6.6, "runningTimeMin": 114 }, { "id": 149, "releaseDate": "Sep 18 2009", "director": "Phil Lord", "title": "Cloudy with a Chance of Meatballs", "genre": "Comedy", "imdbRating": 7.2, "runningTimeMin": 90 }, { "id": 150, "releaseDate": "Mar 19 2004", "director": "Zack Snyder", "title": "Dawn of the Dead", "genre": "Horror", "imdbRating": 7.4, "runningTimeMin": 100 } ] ================================================ FILE: examples/http-socketio-server/http-socket.yml ================================================ config: target: "http://localhost:3000" phases: - duration: 30 arrivalRate: 5 before: flow: - post: url: /login json: username: doesnt password: matter scenarios: - engine: socketio flow: - get: url: /movies capture: json: "$[{{ $randomNumber(1, 150) }}]" #pick a random movie from array of 150 as: chosenMovie - get: url: /movies/{{ chosenMovie.id }} capture: json: "$.title" as: chosenMovieTitle - emit: channel: "echo" data: "{{ chosenMovieTitle }}" response: channel: "echoResponse" data: "{{ chosenMovieTitle }}" after: flow: - post: url: /logout ================================================ FILE: examples/http-socketio-server/http.js ================================================ const express = require('express'); const cookieParser = require('cookie-parser'); const app = express(); const response = require('./data/movies.json'); app.use(express.json()); app.use(cookieParser()); app.post('/login', (req, res) => { const { username, password } = req.body; if (username && password) { res.cookie('username', username); res.json({ success: true }); } else { res.status(422).json({ error: 'Username and password are required' }); } }); app.delete('/logout', (_, res) => { res.clearCookie('username'); res.sendStatus(204); }); app.get('/account', (req, res) => { res.json({ user: req.cookies }); }); app.get('/movies', (_, res) => { res.json(response); }); app.get('/movies/:id', (req, res) => { const id = parseInt(req.params.id, 10); res.json(response.filter((movie) => movie.id === id).pop()); }); module.exports = app; ================================================ FILE: examples/http-socketio-server/package.json ================================================ { "name": "test-endpoints", "version": "1.0.0", "description": "Artillery.io - HTTP and Socket.IO test server", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node app.js" }, "author": "", "license": "ISC", "dependencies": { "cookie-parser": "^1.4.5", "express": "^4.17.1", "socket.io": "^3.1.2" } } ================================================ FILE: examples/http-socketio-server/socketio.js ================================================ const socketio = require('socket.io'); const { log } = console; module.exports = (server) => { const io = socketio(server); io.on('connect', (client) => { function onEcho(m) { log('Echo message', m); client.emit('echoResponse', m); } function onDisconnect() { log(`Received: disconnect event from client: ${client.id}`); client.removeListener('echo', onEcho); client.removeListener('disconnect', onDisconnect); } client.on('disconnect', onDisconnect); client.on('echo', onEcho); }); io.on('connect_error', (err) => { log('connectError, ', err); }); return io; }; ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/.dockerignore ================================================ .eslintrc.js /node_modules ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/.gitignore ================================================ node_modules/ ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/Dockerfile ================================================ FROM node:16-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm --production install COPY . . EXPOSE 3001 CMD ["node", "app.js"] ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/Makefile ================================================ THIS_FILE := $(lastword $(MAKEFILE_LIST)) # VERSION defines the project version for the bundle. # Update this value when you upgrade the version of your project. # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) VERSION ?= latest IMAGE_REPO_OWNER ?= artilleryio IMAGE_PLATFORM ?= linux/amd64 COMMIT_TAG := $(shell git log -1 --pretty=%H) APP_IMAGE_NAME ?= movie-browser-test-endpoints # Image URL to use all building/pushing image targets APP_IMG ?= $(IMAGE_REPO_OWNER)/$(APP_IMAGE_NAME):$(VERSION) # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec all: help ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk commands is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Build API docker image docker-build: ## Build docker image with the api. docker build -f Dockerfile --platform "${IMAGE_PLATFORM}" -t "${APP_IMAGE_NAME}:${COMMIT_TAG}" . docker tag "${APP_IMAGE_NAME}:${COMMIT_TAG}" ${APP_IMG} docker-push: ## Push docker image with the api. docker push ${APP_IMG} ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/README.md ================================================ # Kubernetes testing with kubectl-artillery This example uses the [kubectl-artillery](https://github.com/artilleryio/kubectl-artillery) plugin to bootstrap Artillery tests on Kubernetes. The plugin can scaffold new Artillery test-scripts from running Kubernetes [Services](https://kubernetes.io/docs/concepts/services-networking/service/). For this to work, a Service will need to have access to the underlying [Pod's](https://kubernetes.io/docs/concepts/workloads/pods/) [liveness HTTP probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request) check endpoint. And, the plugin can also generate a Job that wraps a test-script to run Artillery test workers on Kubernetes. ## Trying it out We will be using the [Movie Browser HTTP test server](app.js) to demonstrate how this works. To make this easy we already have a [containerized version](https://github.com/orgs/artilleryio/packages/container/package/movie-browser-test-endpoints) ready to go. This is configured to run on Kubernetes as defined in this [YAML manifest](k8s-deploy.yaml). [KinD](https://kind.sigs.k8s.io/docs/user/quick-start/) is bundled to help you get and running with a Kubernetes cluster as soon as possible. It runs on [Docker](https://docs.docker.com/get-docker/), so be sure to install it if you're going to use a `KinD` cluster. ### Install the plugin [Follow the plugin installation instructions](https://github.com/artilleryio/kubectl-artillery#installation) to install the plugin for your target OS. ### Prepare your Kubernetes cluster Make sure you have a cluster to deploy to. If you don't, run this shell script to get up and running with a KinD cluster locally on your machine. ```shell ./hack/kind/kind-with-registry.sh ``` ### Deploy to Kubernetes Get the `Movie Browser HTTP test server` running on Kubernetes, ```shell kubectl apply -f k8s-deploy.yaml ``` Ensure the server is running. ```shell kubectl get all -l app=movie-browser # NAME READY STATUS RESTARTS AGE # pod/movie-browser-75f47f84f4-xcxkv 1/1 Running 2 5m25s # NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # service/movie-browser-service ClusterIP 10.96.105.65 80/TCP 5m25s # NAME READY UP-TO-DATE AVAILABLE AGE # deployment.apps/movie-browser 1/1 1 1 5m25s # NAME DESIRED CURRENT READY AGE # replicaset.apps/movie-browser-75f47f84f4 1 1 1 5m25s ``` ### Scaffold a test-script The `movie-browser-service` Kubernetes Service exposes an underlying Pod configured with an HTTP Liveness Probe. The `kubectl-artillery` plugin will use this to scaffold an Artillery test-script from the Service. ```shell kubectl artillery scaffold movie-browser-service # artillery-scripts/test-script_movie-browser-service.yaml generated ``` This produces this test-script, ```yaml config: target: http://movie-browser-service:80/ environments: functional: phases: - duration: 1 arrivalCount: 1 plugins: expect: {} scenarios: - flow: - get: url: http://movie-browser-service:80/healthz expect: - statusCode: 200 ``` The health check under test is an endpoint already provided by the `Movie Browser HTTP test server` in the [http.js](http.js) file. ```javascript app.get('/healthz', (req, res) => { if (response.length > 0) { res.status(200).send('Ok'); } else { res.status(500).send('Movie data is missing'); } }); ``` Feel free to update the test-script with any other tests you'd like to run. ### Generate a test Now that we have a test-script, `artillery-scripts/test-script_movie-browser-service.yaml`, we can generate a Kubernetes [Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/) that mounts the test-script as a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) all packaged with [Kustomize](https://kustomize.io). ```shell kubectl artillery generate movie-browser-service -s artillery-scripts/test-script_movie-browser-service.yaml # artillery-manifests/test-job.yaml generated # artillery-manifests/kustomization.yaml generated ``` This produces this Job manifest `artillery-manifests/test-job.yaml`, ```yaml apiVersion: batch/v1 kind: Job metadata: labels: artillery.io/component: test-worker-master artillery.io/part-of: artilleryio-test artillery.io/test-name: movie-browser-service name: movie-browser-service namespace: default ... ... ``` And Kustomize manifest, ```yaml kind: Kustomization apiVersion: kustomize.config.k8s.io/v1beta1 namespace: default resources: - test-job.yaml configMapGenerator: - name: movie-browser-service-test-script files: - test-script_movie-browser-service.yaml generatorOptions: labels: artillery.io/component: artilleryio-test-config artillery.io/part-of: artilleryio-test disableNameSuffixHash: true ``` Here we use Kustomize to create a ConfigMap, `movie-browser-service-test-script`, that will mount our test-script into our cluster. ### Run a test Deploying the generated manifests to our cluster will actually run Artillery test workers on Kubernetes. ```shell kubectl apply -k artillery-manifests # configmap/movie-browser-service-test-script created # job.batch/movie-browser-service created ``` Getting the job, `job.batch/movie-browser-service`, and test worker, `pod/movie-browser-service-9xnsn`, statuses. ```shell kubectl get all -l artillery.io/part-of=artilleryio-test # NAME READY STATUS RESTARTS AGE # pod/movie-browser-service-9xnsn 0/1 Completed 0 44s # NAME COMPLETIONS DURATION AGE # job.batch/movie-browser-service 1/1 24s 45s ``` And the test results can be viewed from the test worker's logs, ```shell kubectl logs pod/movie-browser-service-9xnsn # Phase started: unnamed (index: 0, duration: 1s) 12:17:50(+0000) # Phase completed: unnamed (index: 0, duration: 1s) 12:17:51(+0000) # -------------------------------------- # Metrics for period to: 12:18:00(+0000) (width: 0.12s) # -------------------------------------- # http.codes.200: ................................................................ 1 # http.request_rate: ............................................................. 1/sec # http.requests: ................................................................. 1 # http.response_time: # min: ......................................................................... 16 # max: ......................................................................... 16 # median: ...................................................................... 16 # p95: ......................................................................... 16 # p99: ......................................................................... 16 # http.responses: ................................................................ 1 # vusers.completed: .............................................................. 1 # vusers.created: ................................................................ 1 # vusers.created_by_name.0: ...................................................... 1 # vusers.failed: ................................................................. 0 # vusers.session_length: # min: ......................................................................... 119.3 # max: ......................................................................... 119.3 # median: ...................................................................... 120.3 # p95: ......................................................................... 120.3 # p99: ......................................................................... 120.3 # All VUs finished. Total time: 4 seconds # -------------------------------- # Summary report @ 12:17:52(+0000) # -------------------------------- # http.codes.200: ................................................................ 1 # http.request_rate: ............................................................. 1/sec # http.requests: ................................................................. 1 # http.response_time: # min: ......................................................................... 16 # max: ......................................................................... 16 # median: ...................................................................... 16 # p95: ......................................................................... 16 # p99: ......................................................................... 16 # http.responses: ................................................................ 1 # vusers.completed: .............................................................. 1 # vusers.created: ................................................................ 1 # vusers.created_by_name.0: ...................................................... 1 # vusers.failed: ................................................................. 0 # vusers.session_length: # min: ......................................................................... 119.3 # max: ......................................................................... 119.3 # median: ...................................................................... 120.3 # p95: ......................................................................... 120.3 # p99: ......................................................................... 120.3 ``` ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/app.js ================================================ const http = require('node:http'); const app = require('./http'); const { log } = console; const PORT = process.env.PORT || 3001; const server = http.createServer(app); server.listen(PORT, () => { log(`Server listening on port ${PORT}`); }); ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/data/movies.json ================================================ [ { "id": 1, "releaseDate": "Dec 18 1985", "director": "Terry Gilliam", "title": "Brazil", "genre": "Black Comedy", "imdbRating": 8, "runningTimeMin": 136 }, { "id": 2, "releaseDate": "Feb 16 1996", "director": "Harold Becker", "title": "City Hall", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 111 }, { "id": 3, "releaseDate": "Jul 12 1996", "director": "Edward Zwick", "title": "Courage Under Fire", "genre": "Drama", "imdbRating": 6.6, "runningTimeMin": 115 }, { "id": 4, "releaseDate": "May 31 1996", "director": "Rob Cohen", "title": "Dragonheart", "genre": "Adventure", "imdbRating": 6.2, "runningTimeMin": 108 }, { "id": 5, "releaseDate": "Jan 19 1996", "director": "Robert Rodriguez", "title": "From Dusk Till Dawn", "genre": "Horror", "imdbRating": 7.1, "runningTimeMin": 107 }, { "id": 6, "releaseDate": "Mar 08 1996", "director": "Joel Coen", "title": "Fargo", "genre": "Thriller/Suspense", "imdbRating": 8.3, "runningTimeMin": 87 }, { "id": 7, "releaseDate": "Oct 11 1996", "director": "Stephen Hopkins", "title": "The Ghost and the Darkness", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 109 }, { "id": 8, "releaseDate": "Feb 16 1996", "director": "Dennis Dugan", "title": "Happy Gilmore", "genre": "Comedy", "imdbRating": 6.9, "runningTimeMin": 92 }, { "id": 9, "releaseDate": "Jul 02 1996", "director": "Roland Emmerich", "title": "Independence Day", "genre": "Adventure", "imdbRating": 6.5, "runningTimeMin": 145 }, { "id": 10, "releaseDate": "Jun 13 1980", "director": "Michael Ritchie", "title": "The Island", "genre": "Adventure", "imdbRating": 6.9, "runningTimeMin": 138 }, { "id": 11, "releaseDate": "Jul 26 1996", "director": "Bobby Farrelly", "title": "Kingpin", "genre": "Comedy", "imdbRating": 6.7, "runningTimeMin": 113 }, { "id": 12, "releaseDate": "Oct 11 1996", "director": "Renny Harlin", "title": "The Long Kiss Goodnight", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 120 }, { "id": 13, "releaseDate": "Jul 18 1986", "director": "James Cameron", "title": "Aliens", "genre": "Action", "imdbRating": 7.5, "runningTimeMin": 137 }, { "id": 14, "releaseDate": "Dec 13 1996", "director": "Tim Burton", "title": "Mars Attacks!", "genre": "Comedy", "imdbRating": 6.3, "runningTimeMin": 110 }, { "id": 15, "releaseDate": "Nov 15 1996", "director": "Barbra Streisand", "title": "The Mirror Has Two Faces", "genre": "Romantic Comedy", "imdbRating": 6, "runningTimeMin": 127 }, { "id": 16, "releaseDate": "May 21 1996", "director": "Brian De Palma", "title": "Mission: Impossible", "genre": "Action", "imdbRating": 6.9, "runningTimeMin": 110 }, { "id": 17, "releaseDate": "Nov 15 1996", "director": "Anthony Minghella", "title": "The English Patient", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 160 }, { "id": 18, "releaseDate": "Jul 05 1996", "director": "Jon Turteltaub", "title": "Phenomenon", "genre": "Drama", "imdbRating": 6.3, "runningTimeMin": 124 }, { "id": 19, "releaseDate": "Nov 08 1996", "director": "Ron Howard", "title": "Ransom", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 121 }, { "id": 20, "releaseDate": "Nov 01 1996", "director": "Baz Luhrmann", "title": "Romeo+Juliet", "genre": "Drama", "imdbRating": 6.5, "runningTimeMin": 120 }, { "id": 21, "releaseDate": "Jun 07 1996", "director": "Michael Bay", "title": "The Rock", "genre": "Action", "imdbRating": 7.2, "runningTimeMin": 136 }, { "id": 22, "releaseDate": "Nov 22 1996", "director": "Scott Hicks", "title": "Shine", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 105 }, { "id": 23, "releaseDate": "Nov 06 1996", "director": "F. Gary Gray", "title": "Set It Off", "genre": "Drama", "imdbRating": 6.3, "runningTimeMin": 120 }, { "id": 24, "releaseDate": "Oct 18 1996", "director": "Barry Levinson", "title": "Sleepers", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 105 }, { "id": 25, "releaseDate": "Aug 06 2004", "director": "Ryan Little", "title": "Saints and Soldiers", "genre": "Drama", "imdbRating": 7, "runningTimeMin": 90 }, { "id": 26, "releaseDate": "Aug 16 1996", "director": "Ron Shelton", "title": "Tin Cup", "genre": "Romantic Comedy", "imdbRating": 6.1, "runningTimeMin": 105 }, { "id": 27, "releaseDate": "Jul 19 1996", "director": "Danny Boyle", "title": "Trainspotting", "genre": "Drama", "imdbRating": 8.2, "runningTimeMin": 94 }, { "id": 28, "releaseDate": "Jul 24 1996", "director": "Joel Schumacher", "title": "A Time to Kill", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 150 }, { "id": 29, "releaseDate": "Oct 04 1996", "director": "Tom Hanks", "title": "That Thing You Do!", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 110 }, { "id": 30, "releaseDate": "May 10 1996", "director": "Jan De Bont", "title": "Twister", "genre": "Action", "imdbRating": 6, "runningTimeMin": 117 }, { "id": 31, "releaseDate": "Apr 23 2004", "director": "Gary Winick", "title": "13 Going On 30", "genre": "Comedy", "imdbRating": 6.1, "runningTimeMin": 98 }, { "id": 32, "releaseDate": "Nov 13 2009", "director": "Roland Emmerich", "title": 2012, "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 158 }, { "id": 33, "releaseDate": "Jun 27 2003", "director": "Danny Boyle", "title": "28 Days Later...", "genre": "Horror", "imdbRating": 7.6, "runningTimeMin": 113 }, { "id": 34, "releaseDate": "Mar 09 2007", "director": "Zack Snyder", "title": 300, "genre": "Action", "imdbRating": 7.8, "runningTimeMin": 117 }, { "id": 35, "releaseDate": "Oct 01 1999", "director": "David O. Russell", "title": "Three Kings", "genre": "Action", "imdbRating": 7.3, "runningTimeMin": 115 }, { "id": 36, "releaseDate": "Sep 02 2007", "director": "James Mangold", "title": "3:10 to Yuma", "genre": "Western", "imdbRating": 7.9, "runningTimeMin": 117 }, { "id": 37, "releaseDate": "Aug 19 2005", "director": "Judd Apatow", "title": "The 40 Year-old Virgin", "genre": "Comedy", "imdbRating": 7.5, "runningTimeMin": 111 }, { "id": 38, "releaseDate": "Aug 12 2005", "director": "John Singleton", "title": "Four Brothers", "genre": "Drama", "imdbRating": 6.8, "runningTimeMin": 109 }, { "id": 39, "releaseDate": "Feb 13 2004", "director": "Peter Segal", "title": "50 First Dates", "genre": "Romantic Comedy", "imdbRating": 6.8, "runningTimeMin": 99 }, { "id": 40, "releaseDate": "Nov 08 2002", "director": "Curtis Hanson", "title": "8 Mile", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 110 }, { "id": 41, "releaseDate": "May 17 2002", "director": "Paul Weitz", "title": "About a Boy", "genre": "Romantic Comedy", "imdbRating": 7.4, "runningTimeMin": 101 }, { "id": 42, "releaseDate": "Nov 20 1998", "director": "John Lasseter", "title": "A Bug's Life", "genre": "Adventure", "imdbRating": 7.3, "runningTimeMin": 96 }, { "id": 43, "releaseDate": "Feb 14 1997", "director": "Clint Eastwood", "title": "Absolute Power", "genre": "Thriller/Suspense", "imdbRating": 6.5, "runningTimeMin": 120 }, { "id": 44, "releaseDate": "Nov 12 2004", "director": "Brett Ratner", "title": "After the Sunset", "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 97 }, { "id": 45, "releaseDate": "Nov 10 2006", "director": "Ridley Scott", "title": "A Good Year", "genre": "Drama", "imdbRating": 6.8, "runningTimeMin": 118 }, { "id": 46, "releaseDate": "Jul 25 1997", "director": "Wolfgang Petersen", "title": "Air Force One", "genre": "Action", "imdbRating": 6.3, "runningTimeMin": 124 }, { "id": 47, "releaseDate": "Sep 22 2006", "director": "Steven Zaillian", "title": "All the King's Men", "genre": "Drama", "imdbRating": 6, "runningTimeMin": 128 }, { "id": 48, "releaseDate": "Dec 25 2001", "director": "Michael Mann", "title": "Ali", "genre": "Drama", "imdbRating": 6.6, "runningTimeMin": 159 }, { "id": 49, "releaseDate": "Nov 26 1997", "director": "Jean-Pierre Jeunet", "title": "Alien: Resurrection", "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 108 }, { "id": 50, "releaseDate": "Sep 15 2000", "director": "Cameron Crowe", "title": "Almost Famous", "genre": "Comedy", "imdbRating": 8, "runningTimeMin": 123 }, { "id": 51, "releaseDate": "May 23 2003", "director": "Tom Shadyac", "title": "Bruce Almighty", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 101 }, { "id": 52, "releaseDate": "Apr 06 2001", "director": "Lee Tamahori", "title": "Along Came a Spider", "genre": "Thriller/Suspense", "imdbRating": 6.1, "runningTimeMin": 103 }, { "id": 53, "releaseDate": "Sep 15 1999", "director": "Sam Mendes", "title": "American Beauty", "genre": "Drama", "imdbRating": 8.6, "runningTimeMin": 118 }, { "id": 54, "releaseDate": "Nov 02 2001", "director": "Jean-Pierre Jeunet", "title": "Le Fabuleux destin d'AmÈlie Poulain", "genre": "Comedy", "imdbRating": 8.5, "runningTimeMin": 122 }, { "id": 55, "releaseDate": "Nov 02 2007", "director": "Ridley Scott", "title": "American Gangster", "genre": "Drama", "imdbRating": 7.9, "runningTimeMin": 157 }, { "id": 56, "releaseDate": "Dec 12 1997", "director": "Steven Spielberg", "title": "Amistad", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 152 }, { "id": 57, "releaseDate": "Jul 09 1999", "director": "Paul Weitz", "title": "American Pie", "genre": "Comedy", "imdbRating": 6.9, "runningTimeMin": 95 }, { "id": 58, "releaseDate": "Nov 14 1997", "director": "Don Bluth", "title": "Anastasia", "genre": "Musical", "imdbRating": 6.6, "runningTimeMin": 94 }, { "id": 59, "releaseDate": "Jul 09 2004", "director": "Adam McKay", "title": "Anchorman: The Legend of Ron Burgundy", "genre": "Comedy", "imdbRating": 7, "runningTimeMin": 104 }, { "id": 60, "releaseDate": "Apr 11 2003", "director": "Peter Segal", "title": "Anger Management", "genre": "Comedy", "imdbRating": 6.1, "runningTimeMin": 106 }, { "id": 61, "releaseDate": "Dec 17 1999", "director": "Andy Tennant", "title": "Anna and the King", "genre": "Drama", "imdbRating": 6.5, "runningTimeMin": 147 }, { "id": 62, "releaseDate": "Mar 05 1999", "director": "Harold Ramis", "title": "Analyze This", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 103 }, { "id": 63, "releaseDate": "Oct 02 1998", "director": "Tim Johnson", "title": "Antz", "genre": "Adventure", "imdbRating": 6.8, "runningTimeMin": 83 }, { "id": 64, "releaseDate": "Dec 08 2006", "director": "Mel Gibson", "title": "Apocalypto", "genre": "Adventure", "imdbRating": 7.9, "runningTimeMin": 136 }, { "id": 65, "releaseDate": "Dec 17 1997", "director": "Robert Duvall", "title": "The Apostle", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 148 }, { "id": 66, "releaseDate": "Jul 01 1998", "director": "Michael Bay", "title": "Armageddon", "genre": "Adventure", "imdbRating": 6.1, "runningTimeMin": 150 }, { "id": 67, "releaseDate": "Jul 07 2004", "director": "Antoine Fuqua", "title": "King Arthur", "genre": "Adventure", "imdbRating": 6.2, "runningTimeMin": 126 }, { "id": 68, "releaseDate": "Jun 29 2001", "director": "Steven Spielberg", "title": "Artificial Intelligence: AI", "genre": "Drama", "imdbRating": 6.9, "runningTimeMin": 146 }, { "id": 69, "releaseDate": "Oct 23 2009", "director": "David Bowers", "title": "Astro Boy", "genre": "Adventure", "imdbRating": 6.4, "runningTimeMin": 94 }, { "id": 70, "releaseDate": "Feb 23 2007", "director": "Michael Polish", "title": "The Astronaut Farmer", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 109 }, { "id": 71, "releaseDate": "Dec 24 1997", "director": "James L. Brooks", "title": "As Good as it Gets", "genre": "Romantic Comedy", "imdbRating": 7.8, "runningTimeMin": 138 }, { "id": 72, "releaseDate": "Dec 11 1998", "director": "Sam Raimi", "title": "A Simple Plan", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 121 }, { "id": 73, "releaseDate": "Jun 11 2010", "director": "Joe Carnahan", "title": "The A-Team", "genre": "Action", "imdbRating": 7.2, "runningTimeMin": 119 }, { "id": 74, "releaseDate": "Jun 08 2001", "director": "Gary Trousdale", "title": "Atlantis: The Lost Empire", "genre": "Adventure", "imdbRating": 6.4, "runningTimeMin": 96 }, { "id": 75, "releaseDate": "Dec 07 2007", "director": "Joe Wright", "title": "Atonement", "genre": "Drama", "imdbRating": 7.9, "runningTimeMin": 130 }, { "id": 76, "releaseDate": "Jun 10 1999", "director": "Jay Roach", "title": "Austin Powers: The Spy Who Shagged Me", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 95 }, { "id": 77, "releaseDate": "Jul 25 2002", "director": "Jay Roach", "title": "Austin Powers in Goldmember", "genre": "Comedy", "imdbRating": 6.2, "runningTimeMin": 94 }, { "id": 78, "releaseDate": "May 02 1997", "director": "Jay Roach", "title": "Austin Powers: International Man of Mystery", "genre": "Comedy", "imdbRating": 7.1, "runningTimeMin": 89 }, { "id": 79, "releaseDate": "Dec 17 2004", "director": "Martin Scorsese", "title": "The Aviator", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 170 }, { "id": 80, "releaseDate": "Jun 05 2009", "director": "Sam Mendes", "title": "Away We Go", "genre": "Comedy", "imdbRating": 7.3, "runningTimeMin": 98 }, { "id": 81, "releaseDate": "Nov 25 1998", "director": "George Miller", "title": "Babe: Pig in the City", "genre": "Adventure", "imdbRating": 6.1, "runningTimeMin": 75 }, { "id": 82, "releaseDate": "Jul 18 2003", "director": "Michael Bay", "title": "Bad Boys II", "genre": "Action", "imdbRating": 6.2, "runningTimeMin": 147 }, { "id": 83, "releaseDate": "Mar 28 2003", "director": "John McTiernan", "title": "Basic", "genre": "Thriller/Suspense", "imdbRating": 6.3, "runningTimeMin": 98 }, { "id": 84, "releaseDate": "Jun 15 2005", "director": "Christopher Nolan", "title": "Batman Begins", "genre": "Action", "imdbRating": 8.3, "runningTimeMin": 140 }, { "id": 85, "releaseDate": "Jul 18 2008", "director": "Christopher Nolan", "title": "The Dark Knight", "genre": "Action", "imdbRating": 8.9, "runningTimeMin": 152 }, { "id": 86, "releaseDate": "Jun 27 2001", "director": "John Singleton", "title": "Baby Boy", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 130 }, { "id": 87, "releaseDate": "Dec 25 2008", "director": "David Fincher", "title": "The Curious Case of Benjamin Button", "genre": "Drama", "imdbRating": 8, "runningTimeMin": 167 }, { "id": 88, "releaseDate": "Oct 10 2008", "director": "Ridley Scott", "title": "Body of Lies", "genre": "Thriller/Suspense", "imdbRating": 7.2, "runningTimeMin": 129 }, { "id": 89, "releaseDate": "Dec 08 2006", "director": "Edward Zwick", "title": "Blood Diamond", "genre": "Action", "imdbRating": 8, "runningTimeMin": 143 }, { "id": 90, "releaseDate": "Dec 20 1996", "director": "Mike Judge", "title": "Beavis and Butt-head Do America", "genre": "Comedy", "imdbRating": 6.6, "runningTimeMin": 80 }, { "id": 91, "releaseDate": "Mar 12 2003", "director": "Gurinder Chadha", "title": "Bend it Like Beckham", "genre": "Drama", "imdbRating": 6.9, "runningTimeMin": 112 }, { "id": 92, "releaseDate": "Nov 23 2001", "director": "Todd Field", "title": "In the Bedroom", "genre": "Drama", "imdbRating": 7.5, "runningTimeMin": 130 }, { "id": 93, "releaseDate": "Nov 02 2007", "director": "Steve Hickner", "title": "Bee Movie", "genre": "Comedy", "imdbRating": 6.3, "runningTimeMin": 90 }, { "id": 94, "releaseDate": "Oct 29 1999", "director": "Spike Jonze", "title": "Being John Malkovich", "genre": "Black Comedy", "imdbRating": 7.9, "runningTimeMin": 112 }, { "id": 95, "releaseDate": "Nov 16 2007", "director": "Robert Zemeckis", "title": "Beowulf", "genre": "Adventure", "imdbRating": 6.6, "runningTimeMin": 115 }, { "id": 96, "releaseDate": "Dec 10 2003", "director": "Tim Burton", "title": "Big Fish", "genre": "Drama", "imdbRating": 8.1, "runningTimeMin": 125 }, { "id": 97, "releaseDate": "Mar 06 1998", "director": "Joel Coen", "title": "The Big Lebowski", "genre": "Comedy", "imdbRating": 8.2, "runningTimeMin": 127 }, { "id": 98, "releaseDate": "Dec 28 2001", "director": "Ridley Scott", "title": "Black Hawk Down", "genre": "Action", "imdbRating": 7.7, "runningTimeMin": 144 }, { "id": 99, "releaseDate": "Oct 13 2000", "director": "Stephen Daldry", "title": "Billy Elliot", "genre": "Drama", "imdbRating": 7.7, "runningTimeMin": 110 }, { "id": 100, "releaseDate": "Dec 17 1999", "director": "Chris Columbus", "title": "Bicentennial Man", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 132 }, { "id": 101, "releaseDate": "Aug 21 1998", "director": "Stephen Norrington", "title": "Blade", "genre": "Action", "imdbRating": 7, "runningTimeMin": 121 }, { "id": 102, "releaseDate": "Feb 12 1999", "director": "Hugh Wilson", "title": "Blast from the Past", "genre": "Comedy", "imdbRating": 6.4, "runningTimeMin": 111 }, { "id": 103, "releaseDate": "Aug 09 2002", "director": "Clint Eastwood", "title": "Blood Work", "genre": "Thriller/Suspense", "imdbRating": 6.3, "runningTimeMin": 110 }, { "id": 104, "releaseDate": "Jul 13 2001", "director": "Robert Luketic", "title": "Legally Blonde", "genre": "Comedy", "imdbRating": 6.2, "runningTimeMin": 97 }, { "id": 105, "releaseDate": "Apr 06 2001", "director": "Ted Demme", "title": "Blow", "genre": "Drama", "imdbRating": 7.4, "runningTimeMin": 123 }, { "id": 106, "releaseDate": "Dec 21 2001", "director": "Ron Howard", "title": "A Beautiful Mind", "genre": "Drama", "imdbRating": 8, "runningTimeMin": 135 }, { "id": 107, "releaseDate": "Oct 12 2001", "director": "Barry Levinson", "title": "Bandits", "genre": "Comedy", "imdbRating": 6.5, "runningTimeMin": 123 }, { "id": 108, "releaseDate": "Nov 17 2006", "director": "Emilio Estevez", "title": "Bobby", "genre": "Drama", "imdbRating": 7.1, "runningTimeMin": 120 }, { "id": 109, "releaseDate": "Nov 05 1999", "director": "Phillip Noyce", "title": "The Bone Collector", "genre": "Thriller/Suspense", "imdbRating": 6.3, "runningTimeMin": 118 }, { "id": 110, "releaseDate": "Oct 10 1997", "director": "Paul Thomas Anderson", "title": "Boogie Nights", "genre": "Drama", "imdbRating": 7.9, "runningTimeMin": 152 }, { "id": 111, "releaseDate": "Nov 03 2006", "director": "Larry Charles", "title": "Borat", "genre": "Comedy", "imdbRating": 7.7, "runningTimeMin": 83 }, { "id": 112, "releaseDate": "Jun 14 2002", "director": "Doug Liman", "title": "The Bourne Identity", "genre": "Action", "imdbRating": 7.7, "runningTimeMin": 110 }, { "id": 113, "releaseDate": "Jul 23 2004", "director": "Paul Greengrass", "title": "The Bourne Supremacy", "genre": "Action", "imdbRating": 7.6, "runningTimeMin": 108 }, { "id": 114, "releaseDate": "Aug 03 2007", "director": "Paul Greengrass", "title": "The Bourne Ultimatum", "genre": "Action", "imdbRating": 8.2, "runningTimeMin": 114 }, { "id": 115, "releaseDate": "Oct 22 1999", "director": "Martin Scorsese", "title": "Bringing Out The Dead", "genre": "Black Comedy", "imdbRating": 6.8, "runningTimeMin": 120 }, { "id": 116, "releaseDate": "Dec 09 2005", "director": "Ang Lee", "title": "Brokeback Mountain", "genre": "Drama", "imdbRating": 7.8, "runningTimeMin": 134 }, { "id": 117, "releaseDate": "Sep 13 2002", "director": "Tim Story", "title": "Barbershop", "genre": "Comedy", "imdbRating": 6.2, "runningTimeMin": 103 }, { "id": 118, "releaseDate": "Sep 27 2000", "director": "Christopher Guest", "title": "Best in Show", "genre": "Comedy", "imdbRating": 7.4, "runningTimeMin": 90 }, { "id": 119, "releaseDate": "Nov 26 2003", "director": "Terry Zwigoff", "title": "Bad Santa", "genre": "Comedy", "imdbRating": 7.3, "runningTimeMin": 91 }, { "id": 120, "releaseDate": "Aug 21 2009", "director": "Quentin Tarantino", "title": "Inglourious Basterds", "genre": "Action", "imdbRating": 8.4, "runningTimeMin": 152 }, { "id": 121, "releaseDate": "May 15 1998", "director": "Warren Beatty", "title": "Bulworth", "genre": "Comedy", "imdbRating": 6.8, "runningTimeMin": 107 }, { "id": 122, "releaseDate": "Aug 13 1999", "director": "Frank Oz", "title": "Bowfinger", "genre": "Comedy", "imdbRating": 6.4, "runningTimeMin": 96 }, { "id": 123, "releaseDate": "Dec 22 2000", "director": "Robert Zemeckis", "title": "Cast Away", "genre": "Drama", "imdbRating": 7.5, "runningTimeMin": 144 }, { "id": 124, "releaseDate": "Sep 10 2004", "director": "David R. Ellis", "title": "Cellular", "genre": "Thriller/Suspense", "imdbRating": 6.5, "runningTimeMin": 94 }, { "id": 125, "releaseDate": "Oct 10 2008", "director": "Gil Kenan", "title": "City of Ember", "genre": "Adventure", "imdbRating": 6.4, "runningTimeMin": 94 }, { "id": 126, "releaseDate": "Jul 15 2005", "director": "Tim Burton", "title": "Charlie and the Chocolate Factory", "genre": "Comedy", "imdbRating": 7.1, "runningTimeMin": 115 }, { "id": 127, "releaseDate": "Oct 27 2006", "director": "Phillip Noyce", "title": "Catch a Fire", "genre": "Thriller/Suspense", "imdbRating": 6.8, "runningTimeMin": 101 }, { "id": 128, "releaseDate": "Dec 27 2002", "director": "Rob Marshall", "title": "Chicago", "genre": "Musical", "imdbRating": 7.2, "runningTimeMin": 113 }, { "id": 129, "releaseDate": "Jun 21 2000", "director": "Nick Park", "title": "Chicken Run", "genre": "Adventure", "imdbRating": 7.3, "runningTimeMin": 84 }, { "id": 130, "releaseDate": "Dec 25 2006", "director": "Alfonso Cuaron", "title": "Children of Men", "genre": "Thriller/Suspense", "imdbRating": 8.1, "runningTimeMin": 114 }, { "id": 131, "releaseDate": "Nov 16 2007", "director": "Mike Newell", "title": "Love in the Time of Cholera", "genre": "Drama", "imdbRating": 6.2, "runningTimeMin": 139 }, { "id": 132, "releaseDate": "Dec 15 2000", "director": "Lasse Hallstrom", "title": "Chocolat", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 122 }, { "id": 133, "releaseDate": "Dec 15 2006", "director": "Gary Winick", "title": "Charlotte's Web", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 98 }, { "id": 134, "releaseDate": "Jun 03 2005", "director": "Ron Howard", "title": "Cinderella Man", "genre": "Drama", "imdbRating": 8, "runningTimeMin": 144 }, { "id": 135, "releaseDate": "Apr 10 1998", "director": "Brad Silberling", "title": "City of Angels", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 117 }, { "id": 136, "releaseDate": "Dec 25 1998", "director": "Steven Zaillian", "title": "A Civil Action", "genre": "Drama", "imdbRating": 6.4, "runningTimeMin": 112 }, { "id": 137, "releaseDate": "Dec 25 2003", "director": "Anthony Minghella", "title": "Cold Mountain", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 152 }, { "id": 138, "releaseDate": "Feb 18 2005", "director": "Francis Lawrence", "title": "Constantine", "genre": "Action", "imdbRating": 6.7, "runningTimeMin": 122 }, { "id": 139, "releaseDate": "Aug 06 2004", "director": "Michael Mann", "title": "Collateral", "genre": "Action", "imdbRating": 7.8, "runningTimeMin": 120 }, { "id": 140, "releaseDate": "Jun 06 1997", "director": "Simon West", "title": "Con Air", "genre": "Action", "imdbRating": 6.6, "runningTimeMin": 115 }, { "id": 141, "releaseDate": "Aug 08 1997", "director": "Richard Donner", "title": "Conspiracy Theory", "genre": "Thriller/Suspense", "imdbRating": 6.5, "runningTimeMin": 135 }, { "id": 142, "releaseDate": "Jul 11 1997", "director": "Robert Zemeckis", "title": "Contact", "genre": "Drama", "imdbRating": 7.3, "runningTimeMin": 150 }, { "id": 143, "releaseDate": "Aug 15 1997", "director": "James Mangold", "title": "Cop Land", "genre": "Drama", "imdbRating": 6.9, "runningTimeMin": 105 }, { "id": 144, "releaseDate": "Dec 21 2006", "director": "Yimou Zhang", "title": "Man cheng jin dai huang jin jia", "genre": "Action", "imdbRating": 7, "runningTimeMin": 113 }, { "id": 145, "releaseDate": "May 06 2005", "director": "Paul Haggis", "title": "Crash", "genre": "Drama", "imdbRating": 6.1, "runningTimeMin": 107 }, { "id": 146, "releaseDate": "Jan 25 2002", "director": "Kevin Reynolds", "title": "The Count of Monte Cristo", "genre": "Drama", "imdbRating": 7.6, "runningTimeMin": 131 }, { "id": 147, "releaseDate": "Mar 05 1999", "director": "Roger Kumble", "title": "Cruel Intentions", "genre": "Drama", "imdbRating": 6.7, "runningTimeMin": 95 }, { "id": 148, "releaseDate": "Apr 16 2010", "director": "James Ivory", "title": "The City of Your Final Destination", "genre": "Drama", "imdbRating": 6.6, "runningTimeMin": 114 }, { "id": 149, "releaseDate": "Sep 18 2009", "director": "Phil Lord", "title": "Cloudy with a Chance of Meatballs", "genre": "Comedy", "imdbRating": 7.2, "runningTimeMin": 90 }, { "id": 150, "releaseDate": "Mar 19 2004", "director": "Zack Snyder", "title": "Dawn of the Dead", "genre": "Horror", "imdbRating": 7.4, "runningTimeMin": 100 } ] ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/hack/kind/kind-with-registry.sh ================================================ #!/bin/bash set -o errexit set -o posix # full directory name of the script no matter where it is being called from script_dir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" # create registry container unless it already exists reg_name='kind-registry' reg_port='5000' running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" if [ "${running}" != 'true' ]; then docker run \ -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \ registry:2 fi echo "Ran registry ${reg_name}:${reg_port}" # create data directory to mount volumes - ignored by git and docker mkdir -p ${script_dir}/data # create a cluster with the local registry enabled in containerd cat < { const { username, password } = req.body; if (username && password) { res.cookie('username', username); res.json({ success: true }); } else { res.status(422).json({ error: 'Username and password are required' }); } }); app.delete('/logout', (_, res) => { res.clearCookie('username'); res.sendStatus(204); }); app.get('/account', (req, res) => { res.json({ user: req.cookies }); }); app.get('/movies', (_, res) => { res.json(response); }); app.get('/movies/:id', (req, res) => { const id = parseInt(req.params.id, 10); res.json(response.filter((movie) => movie.id === id).pop()); }); app.get('/healthz', (_req, res) => { if (response.length > 0) { res.status(200).send('Ok'); } else { res.status(500).send('Movie data is missing'); } }); module.exports = app; ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/k8s-deploy.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: movie-browser labels: app: movie-browser spec: replicas: 1 selector: matchLabels: app: movie-browser template: metadata: labels: app: movie-browser spec: containers: - name: nginx image: ghcr.io/artilleryio/movie-browser-test-endpoints:latest ports: - containerPort: 3001 livenessProbe: initialDelaySeconds: 1 periodSeconds: 2 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 1 httpGet: path: /healthz httpHeaders: - name: Host value: myapplication1.com port: 3001 --- apiVersion: v1 kind: Service metadata: labels: app: movie-browser name: movie-browser-service namespace: default spec: ports: - name: nginx-http-port port: 80 targetPort: 3001 selector: app: movie-browser sessionAffinity: None type: ClusterIP ================================================ FILE: examples/k8s-testing-with-kubectl-artillery/package.json ================================================ { "name": "movie-browser-test-endpoints", "version": "1.0.0", "description": "Artillery.io - Movie Browser HTTP test server", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node app.js" }, "author": "", "license": "ISC", "dependencies": { "cookie-parser": "^1.4.5", "express": "^4.17.1" } } ================================================ FILE: examples/multiple-scenario-specs/README.md ================================================ # Separating scenarios into separate files This example shows how an Artillery test suite can be organized into individual scenario specs, and how those specs can be run one at a time, and all together at the same time. ## Artillery's `--config` flag This example makes use of the `--config` flag availale in `run` and `aws:run` commands. ([docs](https://www.artillery.io/docs/guides/guides/command-line#run---run-a-test-script)) This flag allows us to extract the `config` section of an Artillery's test script into a separate file and provide it to Artillery when we run a test through the flag. We can take this one step further, and also extract our scenarios into separate files for a tidier codebase. ✨ ## Code layout Our code in this example is laid out as follows: ``` ├── README.md ├── common-config.yml └── scenarios ├── armadillo.yml ├── dino.yml └── pony.yml ``` - the `common-config.yml` contains configuration for our test - the `scenarios/` directory contains 3 scenarios ## Service we're testing We're testing a simple service that returns ASCII pictures of various animals. For example, to see a picture of an armadillo, send a GET request to http://asciizoo.artillery.io:8080/armadillo: ```sh skytrace probe http://asciizoo.artillery.io:8080/armadillo -b ``` ![armadillo](./artillery-probe.png) ## Running one scenario To run one scenario, e.g. the scenario that tests the `/armadillo` endpoint, run: ```sh artillery run --config common-config.yml scenarios/armadillo.yml ``` ## Running multiple scenarios at once Artillery CLI does not support running multiple scenarios with one command yet, but we can use a short shell script to iterate through the scenario files for us, and run Artillery on each of them in sequence. On a Mac/Linux system it would look like this: ```sh for scenarioFile in `ls scenarios/*.yml` ; do artillery run --config common-config.yml scenarios/$scenarioFile done ``` The script looks for files with the `yml` extension under `scenarios/` and runs Artillery with each of those one after another. ================================================ FILE: examples/multiple-scenario-specs/common-config.yml ================================================ config: target: http://asciizoo.artillery.io:8080 plugins: metrics-by-endpoint: {} expect: {} ================================================ FILE: examples/multiple-scenario-specs/scenarios/armadillo.yml ================================================ scenarios: - name: armadillo flow: - get: url: "/armadillo" expect: statusCode: 200 ================================================ FILE: examples/multiple-scenario-specs/scenarios/dino.yml ================================================ scenarios: - name: dino flow: - get: url: "/dino" expect: statusCode: 200 ================================================ FILE: examples/multiple-scenario-specs/scenarios/pony.yml ================================================ scenarios: - name: pony flow: - get: url: "/pony" expect: statusCode: 200 ================================================ FILE: examples/prometheus-grafana-dashboards/README.md ================================================ # Prometheus Grafana Dashboards This a collection of Grafana dashboards that visualise Artillery test result data collected using the [Prometheus](https://www.artillery.io/docs/guides/plugins/plugin-publish-metrics#prometheus-pushgateway) target of the [publish-metrics](https://www.artillery.io/docs/guides/plugins/plugin-publish-metrics) plugin. The dashboards were exported as JSON and [can be easily imported into Grafana](https://grafana.com/docs/grafana/latest/dashboards/export-import/#import-dashboard). __NOTE__ The data is collected by Prometheus using the Pushgateway, this caches the data so the graphs never reset to zero. This means as a user viewing the data, the last value will keep repeating indefinitely. Generally, this is the expected behaviour when using the Pushgateway. See, [Should I be using the Pushgateway](https://prometheus.io/docs/practices/pushing/). And this [Stack Overflow ticket](https://stackoverflow.com/questions/60039289/how-to-display-zero-instead-of-last-value-in-prometheus-grafana). ## vusers metrics This dashboard, `dashboard-vusers-metrics-1652971366368.json`, visualizes `vusers` metrics. ## http metrics This dashboard, `dashboard-http-metrics-1652971310916.json`, visualizes `http` metrics. ================================================ FILE: examples/prometheus-grafana-dashboards/dashboard-http-metrics-1652971310916.json ================================================ { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": [], "__requires": [ { "type": "panel", "id": "barchart", "name": "Bar chart", "version": "" }, { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "8.4.5" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "description": "http metrics", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "iteration": 1652970863157, "links": [], "liveNow": false, "panels": [ { "description": "Metrics for http.codes", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 11, "w": 18, "x": 3, "y": 0 }, "id": 2, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=~\"http_codes_$http_codes\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "http.codes.$http_codes", "transformations": [], "type": "barchart" }, { "description": "Metrics for http.requests", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 11, "w": 18, "x": 3, "y": 11 }, "id": 3, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=\"http_requests\"}", "format": "table", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "http.requests", "transformations": [], "type": "barchart" }, { "description": "Metrics for http.response_time", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 25, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 12, "w": 18, "x": 3, "y": 22 }, "id": 5, "interval": "10s", "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"http_response_time_min\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "min", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"http_response_time_max\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "max", "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"http_response_time_p95\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "p95", "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"http_response_time_p99\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "p99", "refId": "D" } ], "title": "http.response_time", "transformations": [], "type": "timeseries" }, { "description": "Metrics for http.responses", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 11, "w": 18, "x": 3, "y": 34 }, "id": 6, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=\"http_responses\"}", "format": "table", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "http.responses", "transformations": [], "type": "barchart" } ], "schemaVersion": 35, "style": "dark", "tags": [], "templating": { "list": [ { "current": {}, "definition": "artillery_counters", "description": "Test Ids as tagged in the artillery.io test-script.yaml", "hide": 0, "includeAll": false, "label": "Select test_id", "multi": false, "name": "test_id", "options": [], "query": { "query": "artillery_counters", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "/test_id=\"(?[^\"]+)/g", "skipUrlSync": false, "sort": 0, "type": "query", "datasource": "${DS_PROMETHEUS}" }, { "current": {}, "definition": "artillery_counters", "hide": 0, "includeAll": false, "label": "Select http_code", "multi": false, "name": "http_codes", "options": [], "query": { "query": "artillery_counters", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "/.*http_codes_(.*)\".*,/", "skipUrlSync": false, "sort": 0, "type": "query", "datasource": "${DS_PROMETHEUS}" } ] }, "time": { "from": "now-6h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Artillery - http metrics", "uid": "hsGnZJunz", "version": 11, "weekStart": "" } ================================================ FILE: examples/prometheus-grafana-dashboards/dashboard-vusers-metrics-1652971366368.json ================================================ { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": [], "__requires": [ { "type": "panel", "id": "barchart", "name": "Bar chart", "version": "" }, { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "8.4.5" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "description": "vusers metrics", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "iteration": 1652971357928, "links": [], "liveNow": true, "panels": [ { "description": "Metrics for vusers.completed", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 11, "w": 18, "x": 2, "y": 0 }, "id": 3, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=\"vusers_completed\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "vusers.completed", "transformations": [], "type": "barchart" }, { "description": "Metrics for vusers.created", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 12, "w": 18, "x": 2, "y": 11 }, "id": 4, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=\"vusers_created\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "vusers.created", "transformations": [], "type": "barchart" }, { "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 11, "w": 18, "x": 2, "y": 23 }, "id": 2, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=~\"vusers_created_by_name_$vusers_created_by_name\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "vusers_created_by_name(_$vusers_created_by_name)", "transformations": [], "type": "barchart" }, { "description": "Metrics for vusers_failed", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "Count", "axisPlacement": "left", "axisSoftMin": 0, "axisWidth": 30, "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "log": 10, "type": "log" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 12, "w": 18, "x": 2, "y": 34 }, "id": 5, "interval": "10s", "options": { "barRadius": 0, "barWidth": 1, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "orientation": "auto", "showValue": "auto", "stacking": "none", "text": { "valueSize": 0 }, "tooltip": { "mode": "single", "sort": "none" }, "xField": "Time", "xTickLabelMaxLength": 1, "xTickLabelRotation": 0, "xTickLabelSpacing": -100 }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_counters{test_id=~\"$test_id\", metric=\"vusers_failed\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "vusers.failed", "transformations": [], "type": "barchart" }, { "description": "Metrics for vusers.session_length", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 25, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 12, "w": 18, "x": 2, "y": 46 }, "id": 6, "interval": "10s", "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "8.4.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"vusers_session_length_min\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "min", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"vusers_session_length_max\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "max", "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"vusers_session_length_p95\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "p95", "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "exemplar": true, "expr": "artillery_summaries{test_id=~\"$test_id\", metric=\"vusers_session_length_p99\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "p99", "refId": "D" } ], "title": "vusers.session_length", "transformations": [], "type": "timeseries" } ], "refresh": false, "schemaVersion": 35, "style": "dark", "tags": [], "templating": { "list": [ { "current": {}, "definition": "artillery_counters", "description": "Test Ids as tagged in the artillery.io test-script.yaml", "hide": 0, "includeAll": false, "label": "Select test_id", "multi": false, "name": "test_id", "options": [], "query": { "query": "artillery_counters", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "/test_id=\"(?[^\"]+)/g", "skipUrlSync": false, "sort": 0, "type": "query", "datasource": "${DS_PROMETHEUS}" }, { "current": {}, "definition": "artillery_counters", "hide": 0, "includeAll": false, "label": "Select vusers_created_by_name", "multi": false, "name": "vusers_created_by_name", "options": [], "query": { "query": "artillery_counters", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "/.*vusers_created_by_name_(.*)\".*,/", "skipUrlSync": false, "sort": 0, "type": "query", "datasource": "${DS_PROMETHEUS}" } ] }, "time": { "from": "now-5m", "to": "now" }, "timepicker": { "hidden": true }, "timezone": "", "title": "Artillery - vusers metrics", "uid": "siOy7cXnk", "version": 22, "weekStart": "" } ================================================ FILE: examples/refresh-auth-token/README.md ================================================ # refresh-auth-token example This example shows how you can refresh an authentication token used by individual VUs as they're running. It's a solution to the problem of the VUs needing to use a short-lived authentication token (or another short-lived value) in a test that runs longer than the expiration window of the token. It works as follows: 1. A `refreshTokenIfNeeded` function is set to be called before each request in a scenario. The function checks whether a token has already been created, and if it needs to be refreshed. The expiry window is set to 5s in this example. 2. If a token needs to be created for the first time or refreshed, the `fetchToken()` function is called, and the result is stored in the `authToken` template variable. 3. The `authToken` template variable is used as the value of `x-auth-token` header on the requests in the scenario. 4. The VU scenario includes a 10s pause that will cause the existing token to expire and get refreshed before the last call to the `/armadillo` endpoint is made. To adapt the example for your use-case: 1. Increase the value of `TOKEN_REFRESH_INTERVAL` to match the expiry window of the tokens in your application. 2. Update `fetchToken()` function with the logic to fetch a token, e.g. by making a HTTP call to an external API endpoint. Run the example: ```sh DEBUG=http artillery run refresh.yml ``` You should see output that looks similar to: ``` Test run id: tdhbk_bm63epyfgx8yt4atfethqmea34pxm_h9t9 Fetching new token expiry time: 1716981850536 new token: token-1716981845535 2024-05-29T11:24:05.840Z http request: { "url": "http://asciizoo.artillery.io:8080/dino", "method": "GET", "headers": { "user-agent": "Artillery (https://artillery.io)", "x-auth-token": "token-1716981845535" } } 2024-05-29T11:24:05.913Z http request: { "url": "http://asciizoo.artillery.io:8080/pony", "method": "GET", "headers": { "user-agent": "Artillery (https://artillery.io)", "x-auth-token": "token-1716981845535" } } Used auth token: token-1716981845535 Fetching new token expiry time: 1716981860917 new token: token-1716981855917 2024-05-29T11:24:16.627Z http request: { "url": "http://asciizoo.artillery.io:8080/armadillo", "method": "GET", "headers": { "user-agent": "Artillery (https://artillery.io)", "x-auth-token": "token-1716981855917" } } Now used a refreshed auth token: token-1716981855917 ``` You can see that a new token was created before any requests were made by the VU, and that the first two requests (to `/dino` and `/pony` endpoints) used that token. Because of the 10s pause in the VU scenario the token was deemed as expired, and was refreshed before the third call to `/armadillo` endpoint was made. The call to `/armadillo` endpoint used the refreshed value of the token. ================================================ FILE: examples/refresh-auth-token/refresh.mjs ================================================ const TOKEN_REFRESH_INTERVAL = 1000 * 5; // 5 seconds export async function refreshTokenIfNeeded(_requestParams, vuContext, _events) { if (!vuContext.tokenExpiryTime || vuContext.tokenExpiryTime < Date.now()) { console.log('Fetching new token'); const token = await fetchToken(); vuContext.tokenExpiryTime = Date.now() + TOKEN_REFRESH_INTERVAL; vuContext.vars.authToken = token; console.log(' expiry time:', vuContext.tokenExpiryTime); console.log(' new token:', vuContext.vars.authToken); } } async function fetchToken() { // Return a dummy token for the sake of this example. A real-world // implementation would usually fetch a token from an external endpoint. return `token-${Date.now()}`; } ================================================ FILE: examples/refresh-auth-token/refresh.yml ================================================ config: target: http://asciizoo.artillery.io:8080 processor: ./refresh.mjs scenarios: - name: "refresh_auth_token" beforeRequest: "refreshTokenIfNeeded" flow: - get: url: "/dino" headers: x-auth-token: "{{ authToken}}" - get: url: "/pony" headers: x-auth-token: "{{ authToken}}" - log: "Used auth token: {{ authToken}}" # Pause for 10 seconds. This will cause the "refreshTokenIfNeeded" # function to refresh it before the next request to /armadillo is # made - think: 10 # This request will use a refreshed auth token - get: url: "/armadillo" headers: x-auth-token: "{{ authToken }}" - log: "Now used a refreshed auth token: {{ authToken}}" ================================================ FILE: examples/rpc-twirp-with-custom-function/README.md ================================================ # Load testing an RPC service created with Twirp [Twirp](https://github.com/twitchtv/twirp) is a simple RPC framework for service-to-service communication. In the following example, we will use the clients auto-generated from [Twirpscript](https://github.com/tatethurston/TwirpScript), a NodeJS implementation of Twirp, to load test a server using Twirp. While we could build [a dedicated engine](https://www.artillery.io/blog/extend-artillery-by-creating-your-own-engines), you can also use custom functions and leverage the existing default engine. This example shows you how to do that. ## Pre-requisites - Protobuf [installed](https://github.com/tatethurston/TwirpScript?tab=readme-ov-file#installation-) ## How the example works This example imports the auto-generated `.pb.js` file into a processor, and calls the `MakeHat` client method, which will call the server. We also emit [custom metrics](https://www.artillery.io/docs/reference/extension-apis#custom-metrics-api) from the function to track the number of requests and responses, as well as the time taken in the RPC call. ``` twirp.requests: ................................................................ 300 twirp.response_time: min: ......................................................................... 1.1 max: ......................................................................... 60.7 mean: ........................................................................ 5.4 median: ...................................................................... 2.2 p95: ......................................................................... 22.4 p99: ......................................................................... 55.2 twirp.responses: ............................................................... 300 twirp.responses.success: ....................................................... 300 ``` ## Running the Twirp server First, install the dependencies: ``` cd twirp && npm install ``` Then, start the server with `npm start` ## Running Artillery test Once the server is up and running, execute the test script: ``` npx artillery run ./test/scenario.yml ``` ================================================ FILE: examples/rpc-twirp-with-custom-function/test/processor.mjs ================================================ import { client } from 'twirpscript'; import { MakeHat } from '../twirp/protos/haberdasher.pb.js'; client.baseURL = 'http://localhost:8080'; function recordMetrics(startedAt, ee, error) { //you can add more domain specific metrics here dependant on the response ee.emit('counter', 'twirp.requests', 1); ee.emit('counter', 'twirp.responses', 1); if (error) { ee.emit('counter', 'twirp.responses.error', 1); ee.emit('counter', `twirp.codes.${error.code}`, 1); } else { ee.emit('counter', 'twirp.responses.success', 1); } const took = Number(process.hrtime.bigint() - startedAt) / 1e6; ee.emit('histogram', 'twirp.response_time', took); } export async function callRpcServer(_context, ee, _next) { const startedAt = process.hrtime.bigint(); try { const res = await MakeHat({ inches: 15, potato: true }); console.log(res); recordMetrics(startedAt, ee); } catch (error) { recordMetrics(startedAt, ee, error); } } ================================================ FILE: examples/rpc-twirp-with-custom-function/test/scenario.yml ================================================ config: target: "http://localhost:8080" phases: - duration: 10 arrivalRate: 30 name: "Phase 1" processor: "./processor.mjs" scenarios: - flow: - function: "callRpcServer" ================================================ FILE: examples/rpc-twirp-with-custom-function/twirp/package.json ================================================ { "name": "twirp", "version": "1.0.0", "description": "", "type": "module", "scripts": { "start": "node ./server/index.js" }, "keywords": [], "author": "", "license": "ISC" } ================================================ FILE: examples/rpc-twirp-with-custom-function/twirp/protos/haberdasher.pb.js ================================================ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // Source: example/protos/haberdasher.proto import * as protoscript from 'protoscript'; import { JSONrequest, PBrequest } from 'twirpscript'; // This is the minimum version supported by the current runtime. // If this line fails typechecking, breaking changes have been introduced and this // file needs to be regenerated by running `npx twirpscript`. export { MIN_SUPPORTED_VERSION_0_0_56 } from 'twirpscript'; //========================================// // Haberdasher Protobuf Client // //========================================// /** * MakeHat produces a hat of mysterious, randomly-selected color! */ export async function MakeHat(size, config) { const response = await PBrequest( '/Haberdasher/MakeHat', Size.encode(size), config ); return Hat.decode(response); } //========================================// // Haberdasher JSON Client // //========================================// /** * MakeHat produces a hat of mysterious, randomly-selected color! */ export async function MakeHatJSON(size, config) { const response = await JSONrequest( '/Haberdasher/MakeHat', SizeJSON.encode(size), config ); return HatJSON.decode(response); } export function createHaberdasher(service) { return { name: 'Haberdasher', methods: { MakeHat: { name: 'MakeHat', handler: service.MakeHat, input: { protobuf: Size, json: SizeJSON }, output: { protobuf: Hat, json: HatJSON } } } }; } //========================================// // Protobuf Encode / Decode // //========================================// export const Size = { /** * Serializes Size to protobuf. */ encode: (msg) => Size._writeMessage(msg, new protoscript.BinaryWriter()).getResultBuffer(), /** * Deserializes Size from protobuf. */ decode: (bytes) => Size._readMessage(Size.initialize(), new protoscript.BinaryReader(bytes)), /** * Initializes Size with all fields set to their default value. */ initialize: (msg) => ({ inches: 0, ...msg }), /** * @private */ _writeMessage: (msg, writer) => { if (msg.inches) { writer.writeInt32(1, msg.inches); } return writer; }, /** * @private */ _readMessage: (msg, reader) => { while (reader.nextField()) { const field = reader.getFieldNumber(); switch (field) { case 1: { msg.inches = reader.readInt32(); break; } default: { reader.skipField(); break; } } } return msg; } }; export const Hat = { /** * Serializes Hat to protobuf. */ encode: (msg) => Hat._writeMessage(msg, new protoscript.BinaryWriter()).getResultBuffer(), /** * Deserializes Hat from protobuf. */ decode: (bytes) => Hat._readMessage(Hat.initialize(), new protoscript.BinaryReader(bytes)), /** * Initializes Hat with all fields set to their default value. */ initialize: (msg) => ({ inches: 0, color: '', name: '', ...msg }), /** * @private */ _writeMessage: (msg, writer) => { if (msg.inches) { writer.writeInt32(1, msg.inches); } if (msg.color) { writer.writeString(2, msg.color); } if (msg.name) { writer.writeString(3, msg.name); } return writer; }, /** * @private */ _readMessage: (msg, reader) => { while (reader.nextField()) { const field = reader.getFieldNumber(); switch (field) { case 1: { msg.inches = reader.readInt32(); break; } case 2: { msg.color = reader.readString(); break; } case 3: { msg.name = reader.readString(); break; } default: { reader.skipField(); break; } } } return msg; } }; //========================================// // JSON Encode / Decode // //========================================// export const SizeJSON = { /** * Serializes Size to JSON. */ encode: (msg) => JSON.stringify(SizeJSON._writeMessage(msg)), /** * Deserializes Size from JSON. */ decode: (json) => SizeJSON._readMessage(SizeJSON.initialize(), JSON.parse(json)), /** * Initializes Size with all fields set to their default value. */ initialize: (msg) => ({ inches: 0, ...msg }), /** * @private */ _writeMessage: (msg) => { const json = {}; if (msg.inches) { json.inches = msg.inches; } return json; }, /** * @private */ _readMessage: (msg, json) => { const _inches_ = json.inches; if (_inches_) { msg.inches = protoscript.parseNumber(_inches_); } return msg; } }; export const HatJSON = { /** * Serializes Hat to JSON. */ encode: (msg) => JSON.stringify(HatJSON._writeMessage(msg)), /** * Deserializes Hat from JSON. */ decode: (json) => HatJSON._readMessage(HatJSON.initialize(), JSON.parse(json)), /** * Initializes Hat with all fields set to their default value. */ initialize: (msg) => ({ inches: 0, color: '', name: '', ...msg }), /** * @private */ _writeMessage: (msg) => { const json = {}; if (msg.inches) { json.inches = msg.inches; } if (msg.color) { json.color = msg.color; } if (msg.name) { json.name = msg.name; } return json; }, /** * @private */ _readMessage: (msg, json) => { const _inches_ = json.inches; if (_inches_) { msg.inches = protoscript.parseNumber(_inches_); } const _color_ = json.color; if (_color_) { msg.color = _color_; } const _name_ = json.name; if (_name_) { msg.name = _name_; } return msg; } }; ================================================ FILE: examples/rpc-twirp-with-custom-function/twirp/protos/haberdasher.proto ================================================ syntax = "proto3"; // Haberdasher service makes hats for clients. service Haberdasher { // MakeHat produces a hat of mysterious, randomly-selected color! rpc MakeHat(Size) returns (Hat); } // Size of a Hat, in inches. message Size { int32 inches = 1; // must be > 0 } // A Hat is a piece of headwear made by a Haberdasher. message Hat { int32 inches = 1; string color = 2; // anything but "invisible" string name = 3; // i.e. "bowler" } ================================================ FILE: examples/rpc-twirp-with-custom-function/twirp/server/haberdasher/index.js ================================================ import { createHaberdasher } from '../../../../../examples/rpc-twirp-with-custom-function/twirp/protos/haberdasher.pb.js'; function choose(list) { return list[Math.floor(Math.random() * list.length)]; } export const haberdasher = { MakeHat: (size) => { return { inches: size.inches, color: choose(['red', 'green', 'blue', 'purple']), name: choose(['beanie', 'fedora', 'top hat', 'cowboy', 'beret']) }; } }; export const habderdasherHandler = createHaberdasher(haberdasher); ================================================ FILE: examples/rpc-twirp-with-custom-function/twirp/server/index.js ================================================ import { createServer } from 'node:http'; import { createTwirpServer } from 'twirpscript'; import { habderdasherHandler } from './haberdasher/index.js'; const PORT = 8080; const app = createTwirpServer([habderdasherHandler]); // CORS app.use(async (req, _ctx, next) => { if (req.method === 'OPTIONS') { return { statusCode: 204, headers: { 'access-control-allow-origin': '*', 'access-control-request-method': '*', 'access-control-allow-methods': '*', 'access-control-allow-headers': '*', 'content-type': 'application/json' }, body: '' }; } const { statusCode, headers, body } = await next(); return { statusCode, body, headers: { 'access-control-allow-origin': '*', ...headers } }; }); createServer(app).listen(PORT, () => console.log(`Server listening on port ${PORT}`) ); ================================================ FILE: examples/scenario-weights/.gitignore ================================================ node_modules ================================================ FILE: examples/scenario-weights/README.md ================================================ # Setting scenario weights This example shows how you can modify how Artillery selects a scenario for a virtual user during load testing. In Artillery, each VU will be assigned to one of the defined scenarios. By default, each scenario has a weight of 1, meaning each scenario has the same probability of getting assigned to a VU. By specifying a weight in a scenario, you'll increase the chances of Artillery assigning the scenario for a VU. The probability of a scenario getting chosen depends on the total weight for all scenarios. To learn more, read the Artillery documentation on scenario weights: https://artillery.io/docs/guides/guides/test-script-reference.html#Scenario-weights ## Running the HTTP server This example includes an Express.js application running an HTTP server. First, install the server dependencies: ```shell npm install ``` After installing the dependencies, start the HTTP server: ```shell npm run app:start ``` This command will start an HTTP server listening at http://localhost:3000/. ## Running Artillery tests This directory contains a test script (`scenario-weights.yml`) which demonstrates how to set different scenario weights. Once the HTTP server is up and running, execute the test script: ``` artillery run scenario-weights.yml ``` ================================================ FILE: examples/scenario-weights/app.js ================================================ const express = require('express'); const app = express(); const port = 3000; app.use(express.json()); app.get('/common', (_, res) => { res.send({ route: '/common' }); }); app.get('/average', (_, res) => { res.send({ route: '/average' }); }); app.get('/rare', (_, res) => { res.send({ route: '/rare' }); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); }); ================================================ FILE: examples/scenario-weights/package.json ================================================ { "name": "scenario-weights", "version": "1.0.0", "description": "See how to change how often a scenario is run in your Artillery tests", "main": "app.js", "scripts": { "app:start": "node app.js", "test": "artillery run scenario-weights.yml" }, "author": "Dennis Martinez", "license": "ISC", "dependencies": { "express": "^4.17.1" } } ================================================ FILE: examples/scenario-weights/scenario-weights.yml ================================================ # In Artillery, each VU will be assigned to one of the defined # scenarios. By default, each scenario has a weight of 1, meaning # each scenario has the same probability of getting assigned to a # VU. # # By specifying a weight in a scenario, you'll increase the chances # of Artillery assigning the scenario for a VU. The probability of # a scenario getting chosen depends on the total weight for all # scenarios. # # To learn more, read the Artillery documentation on scenario weights: # https://artillery.io/docs/guides/guides/test-script-reference.html#Scenario-weights config: target: "http://localhost:3000" phases: - duration: 10min arrivalRate: 25 scenarios: # Approximately 60% of all VUs will access this scenario. - name: "access_common_route" weight: 6 flow: - get: url: "/common" # Approximately 30% of all VUs will access this scenario. - name: "access_average_route" weight: 3 flow: - get: url: "/average" # Approximately 10% of all VUs will access this scenario. - name: "access_rare_route" weight: 1 flow: - get: url: "/rare" ================================================ FILE: examples/script-overrides/README.md ================================================ # Overriding values dynamically This example shows how values in an Artillery script can be changed dynamically. A typical use-case is running Artillery in CI and being able to change the load generated dynamically, e.g. by overriding Input Parameters to a job in Jenkins or AWS CodeBuild. The example test script defines 3 environments with different load phases: - `smoke` - a low TPS short phase for smoke testing, with hardcoded values - `preprod` - a higher TPS longer phase for preprod testing, with hardcoded values - `dynamic` - a load phase that can be set at runtime ## Running the example Run the test at low TPS with the `smoke` config environment: ```sh npx artillery run -e smoke test.yaml ``` Run the test at higher TPS with the `preprod` environment: ```sh npx artillery run -e preprod test.yaml ``` If the test script is used in a Jenkins job, those environment names could be an input themselves to allow the user to choose between pre-configured load profiles. Finally, to override the load phase completely at runtime we need to set `ARRIVAL_RATE` and `DURATION` environment variables before running Artillery, such as: ```sh ARRIVAL_RATE=20 DURATION=600 artillery run -e dynamic test.yaml ``` ================================================ FILE: examples/script-overrides/test.yaml ================================================ config: target: http://asciiart.artillery.io:8080 environments: # Informal short run: smoke: phases: - arrivalRate: 1 duration: 10 # Long-running job: preprod: phases: - arrivalRate: 5 duration: 20 dynamic: phases: - arrivalRate: "{{ $processEnvironment.ARRIVAL_RATE }}" duration: "{{ $processEnvironment.DURATION }}" scenarios: - flow: - get: url: "/" ================================================ FILE: examples/soap-with-custom-function/README.md ================================================ # SOAP Load Testing Example Artillery doesn't have an official SOAP engine, but it's still possible to test SOAP with it. While building [a dedicated engine](https://www.artillery.io/blog/extend-artillery-by-creating-your-own-engines) is one option, you can also use custom functions and leverage the existing HTTP engine. This example shows you how to do that. ## What the example does This example calls the SOAP server by using a SOAP client (node-soap) in a custom function. That custom function gets called with each VU execution. We also emit [custom metrics](https://www.artillery.io/docs/reference/extension-apis#custom-metrics-api) from the function to track the number of requests and responses, as well as the time taken to make the SOAP request. ``` soap.addNumbers.requests: ...................................................... 8 soap.addNumbers.response_time: min: ......................................................................... 2 max: ......................................................................... 9 mean: ........................................................................ 4.9 median: ...................................................................... 2 p95: ......................................................................... 7.9 p99: ......................................................................... 7.9 soap.addNumbers.responses: ..................................................... 8 ``` Notes: - The `callSoapOperation` function has been abstracted to allow calling other operations, should you wish to extend this example. - The creation of the client is also cached to prevent creating it for every virtual user. ## Running the SOAP server We provide a very simple SOAP server for this example, containing the `AddNumbersService` with a single `addNumbers` operation. First, install the server dependencies: ``` cd server && npm install ``` After installing the dependencies, start the SOAP server: ``` node app.js ``` This command will start a Socket.IO server listening at http://localhost:8000/. ## Running Artillery test Once the SOAP server is up and running, execute the test script: ``` npx artillery run soap.yml ``` ================================================ FILE: examples/soap-with-custom-function/package.json ================================================ { "name": "soap-with-custom-function", "version": "1.0.0", "description": "", "scripts": { "test": "npx artillery run soap.yml" }, "author": "", "license": "ISC", "dependencies": { "soap": "^1.0.0" } } ================================================ FILE: examples/soap-with-custom-function/processor.js ================================================ const soap = require('soap'); let client; const setupSoapClientIfNeeded = async (context) => { const url = `${context.vars.target}/wsdl?wsdl`; //caches client to avoid creating a new one for each VU if (!client) { client = await soap.createClientAsync(url); } }; const callSoapOperation = async (operationName, events) => { const args = { number1: 5, number2: 3 }; events.emit('counter', `soap.${operationName}.requests`, 1); const timeBefore = Date.now(); await client[`${operationName}Async`](args); const timeTaken = Date.now() - timeBefore; events.emit('counter', `soap.${operationName}.responses`, 1); events.emit('histogram', `soap.${operationName}.response_time`, timeTaken); }; module.exports = { sendSOAPRequest: async (context, events, done) => { try { await setupSoapClientIfNeeded(context); await callSoapOperation('addNumbers', events); done(); } catch (err) { console.error('SOAP Request Error:', err); done(err); } } }; ================================================ FILE: examples/soap-with-custom-function/server/MyService.wsdl ================================================ This service adds two numbers. ================================================ FILE: examples/soap-with-custom-function/server/app.js ================================================ const soap = require('soap'); const express = require('express'); const bodyParser = require('body-parser'); const service = { AddNumbersService: { AddNumbersPort: { addNumbers: (args) => { console.log('RECEIVED REQUEST', args); return { sum: Number(args.number1) + Number(args.number2) }; } } } }; const xml = require('node:fs').readFileSync('MyService.wsdl', 'utf8'); const app = express(); app.use( bodyParser.raw({ type: () => true, limit: '5mb' }) ); app.listen(8000, () => { soap.listen(app, '/wsdl', service, xml); console.log('Server running on port 8000'); }); ================================================ FILE: examples/soap-with-custom-function/server/package.json ================================================ { "name": "server", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "body-parser": "^1.20.2", "express": "^4.18.2", "soap": "^1.0.0" } } ================================================ FILE: examples/soap-with-custom-function/soap.yml ================================================ config: target: "http://localhost:8000" phases: - duration: 2 arrivalRate: 4 name: "Phase 1" processor: "./processor.js" scenarios: - flow: - function: "sendSOAPRequest" ================================================ FILE: examples/socket-io/.gitignore ================================================ node_modules/ ================================================ FILE: examples/socket-io/README.md ================================================ # Socket.IO load testing example This example shows you how to test a [Socket.IO](https://socket.io/) server using Artillery's built-in Socket.IO engine. ⚠️ _**Note:** This Socket.IO server in this example uses Socket.IO v3.x, as the official Artillery engine now uses 3.x. For extended v3.x support, check out the [artillery-engine-socketio-v3 plugin](https://github.com/ptejada/artillery-engine-socketio-v3)._ ## Running the Socket.IO server First, install the server dependencies: ``` npm install ``` After installing the dependencies, start the Socket.IO server: ``` node app.js ``` This command will start a Socket.IO server listening at http://localhost:8080/. ## Running Artillery test This directory contains a test script (`socket-io.yml`) which demonstrates different test scenarios for load testing a Socket.IO implementation. Once the Socket.IO server is up and running, execute the test script: ``` npx artillery run socket-io.yml ``` ================================================ FILE: examples/socket-io/app.js ================================================ const io = require('socket.io')(8080, { path: '/', serveClient: false }); const personalisedNamespace = io.of('/personalised'); personalisedNamespace.on('connection', (socket) => { socket.on('echo', (msg) => { socket.emit('echoResponse', msg); }); socket.on('userDetails', (_, callback) => { callback({ name: 'Artillery' }); }); }); console.log('Socket.IO server listening at http://localhost:8080/'); ================================================ FILE: examples/socket-io/package.json ================================================ { "name": "socket-io", "version": "1.0.0", "description": "Artillery.io - Socket.IO example", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "socket.io": "^3.1.2" } } ================================================ FILE: examples/socket-io/public/index.html ================================================ Artillery.io - Socket.IO example

Artillery.io - Socket.IO example

================================================ FILE: examples/socket-io/socket-io.yml ================================================ config: target: "http://localhost:8080" phases: - duration: 60 arrivalRate: 25 scenarios: - name: "emit_and_validate_response" engine: socketio flow: - namespace: /personalised emit: channel: "echo" data: "Hello from Artillery" response: channel: "echoResponse" data: "Hello from Artillery" - name: "emit_and_validate_acknowledgment" engine: socketio flow: - namespace: /personalised emit: channel: "userDetails" acknowledge: match: json: "$.0.name" value: "Artillery" ================================================ FILE: examples/starter-kit/.gitignore ================================================ node_modules reports/** ================================================ FILE: examples/starter-kit/package.json ================================================ { "name": "artillery-starter-kit", "version": "1.0.0", "description": "Artillery.io Starter Kit", "scripts": { "sample_task_01": "npx artillery run ./scenarios/sample_task_01.yaml -o ./reports/sample_report_01.json", "sample_task_02": "npx artillery run ./scenarios/sample_task_02.yaml -o ./reports/sample_report_02.json" }, "author": "", "license": "ISC", "dependencies": { "faker": "^5.5.3" } } ================================================ FILE: examples/starter-kit/processors/_baseProcessor.js ================================================ var faker = require('faker'); module.exports = { generateRandomData: (userContext, _events, done) => { userContext.vars.name = faker.name.findName(); userContext.vars.id = faker.datatype.number({ min: 543200000, max: 555550000 }); return done(); }, generateRandomTiming: (userContext, _events, done) => { userContext.vars.timing = faker.datatype.number({ min: 100, max: 3000 }); return done(); }, printStatus: (_requestParams, response, _context, _ee, next) => { console.log( `ENDPOINT: [${response.request.method}] ${response.request.uri.path}: ${response.statusCode}` ); if (response.statusCode >= 400) { console.warn(response.body); } return next(); } }; ================================================ FILE: examples/starter-kit/processors/sample_task_01.js ================================================ var _faker = require('faker'); var base = require('./_baseProcessor'); module.exports = { doSomething: (userContext, _events, done) => { userContext.vars.something = 'do'; return done(); }, printStatus: base.printStatus, generateRandomTiming: base.generateRandomTiming }; ================================================ FILE: examples/starter-kit/processors/sample_task_02.js ================================================ var _faker = require('faker'); var base = require('./_baseProcessor'); module.exports = { doSomethingElse: (userContext, _events, done) => { userContext.vars.something = 'do'; return done(); }, printStatus: base.printStatus, generateRandomData: base.generateRandomData }; ================================================ FILE: examples/starter-kit/reports/.gitkeep ================================================ ================================================ FILE: examples/starter-kit/scenarios/sample_task_01.yaml ================================================ version: 1 config: target: "https://run.mocky.io" phases: - duration: 30 arrivalRate: 3 maxVusers: 10 processor: "../processors/sample_task_01.js" scenarios: - flow: - function: "generateRandomTiming" - get: url: "/v3/0eff1291-866e-4afd-a462-e3711607caa4?mocky-delay={{ timing }}ms" afterResponse: "printStatus" ================================================ FILE: examples/starter-kit/scenarios/sample_task_02.yaml ================================================ version: 1 config: target: "https://api.somewebsite.com" phases: - duration: 30 arrivalRate: 3 maxVusers: 10 processor: "../processors/sample_task_02.js" scenarios: - flow: - function: "generateRandomData" - get: url: "/members/list" afterResponse: "printStatus" - post: url: "members/create" afterResponse: "printStatus" json: id: "{{ id }}" name: "{{ name }}" - get: url: "/members/{{ id }}" afterResponse: "printStatus" ================================================ FILE: examples/starter-kit/scenarios/sample_task_03.yaml ================================================ version: 1 config: target: "https://api.someservice.com" phases: - name: "Warming up the application" duration: 5 arrivalRate: 1 - name: "Mild load on the application" duration: 10 rampTo: 30 - name: "Putting load on the application" duration: 180 arrivalRate: 3 maxVusers: 120 payload: - path: "../data/users.csv" fields: - "firstName" - "lastName" - "emailAddress" order: sequence skipHeader: true ensure: p95: 200 maxErrorRate: 1 defaults: headers: x-api-key: "{{ $processEnvironment.SERVICE_API_KEY }}" Content-Type: "application/json" Accept: "application/json" tls: rejectUnauthorized: false http: pool: 10 timeout: 15 maxSockets: 6 environments: production: target: "http://wontresolve.prod:44321" phases: - duration: 10 arrivalRate: 10 local: target: "http://127.0.0.1:3003" phases: - duration: 60 arrivalRate: 20 variables: postcode: - "SE1" - "EC1" - "E8" - "WH9" id: - "8731" - "9965" - "2806" processor: "../functions.js" scenarios: - name: "The first flow" flow: - function: "generateRandomData" - get: url: "/members/{{ id }}" afterResponse: "printStatus" - post: url: "/members/member" afterResponse: "printStatus" json: id: "{{ id }}" name: "{{ name }}" description: "Some randomly generated user" salary: 666000 - name: "The second flow" flow: - function: "generateRandomData" - get: url: "/members/{{ id }}" afterResponse: "printStatus" - post: url: "/members/member" afterResponse: "printStatus" json: id: "{{ id }}" name: "{{ name }}" description: "Some randomly generated user" salary: 666000 ================================================ FILE: examples/table-driven-functional-tests/README.md ================================================ # Table-driven functional tests with Artillery This example shows how you can drive functional testing with Artillery with a simple CSV file. We define a CSV file which contains URLs + a status code expectation for each URL: ```csv /,200 /docs,302 /dinosaur,404 ``` An Artillery script uses the data in the CSV file to make a request to each URL and check the assertions. This makes it easy to add new test cases without having to modify the Artillery script itself. ## Run the example To run the example: ```sh # install dependencies: npm install # run the Artillery test: npm run functional-test ``` You can also run the Artillery test with: ```sh npx artillery run --solo -q functional-test.yml ``` ================================================ FILE: examples/table-driven-functional-tests/functional-test.yml ================================================ config: target: http://asciiart.artillery.io:8080 payload: - path: ./request-response.csv fields: [url, code] # NOTE: loadAll requires Artillery v2.0.0-19 or later loadAll: true name: data plugins: expect: {} scenarios: - flow: # Loop over each element in the data variable, with loop-over # Each element in the "data" array can be accessed via the special # $loopElement variable. # https://artillery.io/docs/guides/guides/http-reference.html#Looping-through-an-array - loop: - get: url: "{{ $loopElement.url }}" followRedirect: false expect: statusCode: "{{ $loopElement.code }}" over: "data" ================================================ FILE: examples/table-driven-functional-tests/package.json ================================================ { "name": "table-driven-functional-tests", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "func-test": "npx artillery run --solo -q functional-test.yml", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "csv-parse": "^4.16.0" } } ================================================ FILE: examples/table-driven-functional-tests/request-response.csv ================================================ /,200 /dino,200 https://play.artillery.io/notapath,404 ================================================ FILE: examples/tracetest/README.md ================================================ # Tracetest [Tracetest.io](https://tracetest.io/) is a modern testing tool that leverages distributed tracing to run observability-enabled tests, allowing to assert on whether your system is working correctly at a deeper level. ## Why is this important? With Performance Testing, it can sometimes be difficult to assert that everything is working correctly in a distributed system. For example, let's say your API responds within your SLO, but fails to send a message to a message queue at large scale (which another internal component needs). Your Performance test may be passing, but your system as a whole would still have a broken component. This is where combining Artillery with Tracetest comes in - you can make sure your entire system still functions as expected under load (even components that aren't user-facing). ## Examples ### Playwright Please follow this example created by the Tracetest team: https://docs.tracetest.io/examples-tutorials/recipes/running-playwright-performance-tests-with-artillery-and-tracetest In the example above, you'll be able to create a Playwright scenario to run with Artillery's Playwright engine. The scenario will also generate a distributed trace per virtual user that can be tested using Tracetest. ### HTTP Engine Please follow the example available in the Tracetest documentation: https://docs.tracetest.io/tools-and-integrations/artillery-plugin In the example above, you'll be creating an HTTP scenario and running it with Artillery, also publishing metrics by using the `publish-metrics` OpenTelemetry reporter. The scenario will also generate a distributed trace per virtual user that can be tested using Tracetest, allowing you to test things like database processing time. ## Questions For any questions on integrating Artillery with Tracetest, please reach out to the [Tracetest team](https://tracetest.io/community) or reach out to Artillery on [Github](https://github.com/artilleryio/artillery/discussions). ================================================ FILE: examples/track-custom-metrics/.gitignore ================================================ node_modules ================================================ FILE: examples/track-custom-metrics/README.md ================================================ # Tracking custom metrics example This example shows you how to track custom metrics when testing an API using Artillery's built-in HTTP engine. ## Running the API server First, install the server dependencies: ``` npm install ``` After installing the dependencies, start the API server: ``` node app.js ``` This command will start a server listening at http://localhost:3000/. ## Running Artillery test This directory contains a test script (`custom-metrics.yml`) which loads a custom JS function to track of two custom metrics using an `afterResponse` hook: - A counter to keep track of the number of requests made. - A histogram of the request latency returned via a response header. Once the API server is up and running, execute the test script: ``` artillery run custom-metrics.yml ``` ================================================ FILE: examples/track-custom-metrics/app.js ================================================ const express = require('express'); const serverTiming = require('server-timing'); const app = express(); const port = 3000; app.use(express.json()); app.use(serverTiming()); app.post('/pets', (req, res) => { res.startTime('pets', 'Creating pet'); setTimeout( () => { res.endTime('pets'); res.json({ species: req.body.species, name: req.body.name }); }, Math.ceil(Math.random() * 500) ); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); }); ================================================ FILE: examples/track-custom-metrics/custom-metrics.yml ================================================ config: target: "http://localhost:3000" processor: "./metrics.js" phases: - arrivalRate: 25 duration: 60 scenarios: - afterResponse: "trackPets" flow: - post: url: "/pets" json: species: "pony" name: "Tiki" ================================================ FILE: examples/track-custom-metrics/metrics.js ================================================ module.exports = { trackPets }; function trackPets(_req, res, _context, events, done) { // After every response, increment the 'pets_created' counter by 1. events.emit('counter', 'pets_created', 1); // Parse the 'server-timing' header and look for the 'pets' metric, // and add it to 'pet_creation_latency' histogram. const latency = parseServerTimingLatency( res.headers['server-timing'], 'pets' ); events.emit('histogram', 'pet_creation_latency', latency); return done(); } function parseServerTimingLatency(header, timingMetricName) { const serverTimings = header.split(','); for (const timing of serverTimings) { const timingDetails = timing.split(';'); if (timingDetails[0] === timingMetricName) { return parseFloat(timingDetails[1].split('=')[1]); } } } ================================================ FILE: examples/track-custom-metrics/package.json ================================================ { "name": "track-custom-metrics", "version": "1.0.0", "description": "Artillery.io - Custom metrics example", "main": "app.js", "author": "Dennis Martinez", "license": "ISC", "dependencies": { "express": "^4.17.1", "server-timing": "^3.3.1" } } ================================================ FILE: examples/using-cookies/.gitignore ================================================ node_modules ================================================ FILE: examples/using-cookies/README.md ================================================ # Using cookies when testing HTTP services This example shows you how Artillery works with cookies when testing HTTP services. ## Running the HTTP server This example includes an Express.js application running an HTTP server. First, install the server dependencies: ```shell npm install ``` After installing the dependencies, start the HTTP server: ```shell npm run app:start ``` This command will start an HTTP server listening at http://localhost:3000/. ## Running Artillery tests This directory contains a test script (`cookies.yml`) which demonstrates the different ways to work with cookies when testing an HTTP service: - Using cookies set by the HTTP service. - Manually setting a custom cookie in an Artillery request. Once the HTTP server is up and running, execute the test script: ``` artillery run cookies.yml ``` ================================================ FILE: examples/using-cookies/app.js ================================================ const express = require('express'); const cookieParser = require('cookie-parser'); const app = express(); const port = 3000; app.use(express.json()); app.use(cookieParser()); app.post('/login', (req, res) => { const { email, password } = req.body; if (email && password) { res.cookie('email', email); res.json({ success: true, email }); } else { res .status(422) .json({ success: false, error: 'Email and password are required' }); } }); app.get('/account', (req, res) => { res.json({ user: req.cookies }); }); app.post('/set-state', (_req, res) => { // Cookie will be set from the request, just send a 200 OK response. res.sendStatus(200); }); app.get('/state', (req, res) => { const { state } = req.cookies; res.json({ currentState: state }); }); app.listen(port, () => { console.log(`App listening at http://localhost:${port}`); }); ================================================ FILE: examples/using-cookies/cookies.yml ================================================ config: target: "http://localhost:3000" phases: - duration: 10min arrivalRate: 25 variables: email: - "testuser1@artillery.io" - "testuser2@artillery.io" - "testuser3@artillery.io" - "testuser4@artillery.io" - "testuser5@artillery.io" scenarios: # In this scenario, the request to /login will capture the user's # email and set it in a cookie, which will get returned in the response. # The subsequent request to /account will return the value of the # email from the saved cookie. - name: "login_and_verify_cookie" flow: - post: url: "/login" json: email: "{{ email }}" password: "test-password-123" - get: url: "/account" match: json: "$.user.email" value: "{{ email }}" # In this scenario, we'll manually set cookie values when making a # request to /set-state, and validating the value saved in the cookie # in a request to /state. - name: "set_cookie_values" flow: - post: url: "/login" json: email: "{{ email }}" password: "test-password-123" - post: url: "/set-state" cookie: state: "online" - get: url: "/state" match: json: "$.currentState" value: "online" - post: url: "/set-state" cookie: state: "busy" - get: url: "/state" match: json: "$.currentState" value: "busy" ================================================ FILE: examples/using-cookies/package.json ================================================ { "name": "using-cookies", "version": "1.0.0", "description": "Using cookies when testing HTTP services with Artillery", "main": "app.js", "scripts": { "test": "artillery run cookies.yml", "app:start": "node app.js" }, "author": "Dennis Martinez", "license": "ISC", "dependencies": { "cookie-parser": "^1.4.5", "express": "^4.17.1" } } ================================================ FILE: examples/using-data-from-csv/csv/urls.csv ================================================ / /dino /pony /armadillo ================================================ FILE: examples/using-data-from-csv/website-test.yml ================================================ config: target: http://asciiart.artillery.io:8080 phases: - arrivalCount: 300 duration: 5min payload: - path: "./csv/urls.csv" fields: - url plugins: ensure: maxErrorRate: 0 expect: reportFailuresAsErrors: true scenarios: - flow: - loop: - get: url: "{{ url }}" expect: statusCode: 200 - think: 1 count: 100 ================================================ FILE: examples/using-data-from-redis/README.md ================================================ # Using Data from Redis Due to Artillery's concurrent nature, there is no way to guarantee that test data used (e.g. using CSV) is uniquely accessed by your virtual users, especially running distributed load tests. In some cases, you may require users to be unique. In those cases, using Redis is a simple alternative. You can use your own Redis (e.g. AWS Elasticache), or use a managed solution like Upstash. We recommend Upstash due to its simplicity and serverless nature. This example will show you how to connect an Artillery test to an Upstash Redis instance, and pull a unique user each time. ## Pre-requisites - Create an Upstash account - Follow the simple guide to create an [Upstash Redis instance](https://upstash.com/docs/redis/overall/getstarted) - Obtain the `endpoint` and `token` from the UI ## How it works We'll first seed a Redis database with auto-generated users. Using a [`beforeScenario`](https://www.artillery.io/docs/reference/engines/http#function-actions-and-beforescenario--afterscenario-hooks) hook we pull a unique user per VU from Redis (`using Redis lpop`), and save its username and password in `context.vars`. The scenario is then simply logging the username and password to the console to demonstrate that they are unique. ## Running the example - First, create a `.env` file in this directory, with the same contents as `.env.sample`, filling in the information with your endpoint and token. - Run `npm install` in this directory to install the needed dependencies - Run `npm run seed` to seed the Redis instance with 100 auto-generated users (username and password) You can now run your Artillery test with `npm run test`. You'll see the users printed to the console. ## Additional thoughts - We seed the database in a separate script. However, you could easily run the seeding step as part of a [`before`](https://www.artillery.io/docs/reference/test-script#before-and-after-sections) hook if desired - If you're interested in the additional overhead of pulling the user from Redis, you can run the test with the `SHOW_TIMING=true` variable. Redis is very fast, and typically each call should only add <30 ms to each VU execution (depending on factors like instance sizes, size of the load test, network, etc) ================================================ FILE: examples/using-data-from-redis/package.json ================================================ { "name": "using-data-from-redis", "version": "1.0.0", "description": "", "main": "processor.js", "scripts": { "seed": "node scripts/seed-redis-with-users.js", "test": "npx artillery run scenario.yml --dotenv .env" }, "author": "", "license": "ISC", "dependencies": { "@upstash/redis": "^1.29.0" }, "devDependencies": { "@ngneat/falso": "^7.2.0", "dotenv": "^16.4.5" } } ================================================ FILE: examples/using-data-from-redis/processor.js ================================================ const { Redis } = require('@upstash/redis'); const redis = new Redis({ url: process.env.UPSTASH_REDIS_URL, token: process.env.UPSTASH_REDIS_TOKEN }); async function getUser(context, _events) { const initialTime = Date.now(); const res = await redis.lpop('users', 1); if (res.length === 0) { console.error('No users found in Redis'); throw new Error('err_no_users_found_in_redis'); } context.vars.username = res[0].username; context.vars.password = res[0].password; const finalTime = Date.now(); if (process.env.SHOW_TIMING) { console.log(`Time taken: ${finalTime - initialTime}ms`); } } module.exports = { getUser }; ================================================ FILE: examples/using-data-from-redis/scenario.yml ================================================ config: target: "http://doesntmatter" phases: - duration: 10 arrivalRate: 10 name: "Phase 1" processor: "./processor.js" scenarios: - beforeScenario: getUser flow: - log: "Username: {{ username }} | Password: {{ password }}" ================================================ FILE: examples/using-data-from-redis/scripts/seed-redis-with-users.js ================================================ const { Redis } = require('@upstash/redis'); const falso = require('@ngneat/falso'); const dotenv = require('dotenv'); dotenv.config(); // Configuration const USERS_COUNT = 100; const BATCH_SIZE = 5; // Initialize Upstash Redis const redis = new Redis({ url: process.env.UPSTASH_REDIS_URL, token: process.env.UPSTASH_REDIS_TOKEN, }) // Generate a random username and password function generateUser() { return { username: falso.randUserName(), password: falso.randPassword(), }; } async function storeUsersInRedis(users) { const pipeline = redis.pipeline(); users.forEach(user => { if (user) { pipeline.lpush('users', JSON.stringify(user)); } }); await pipeline.exec(); } // Main function to seed users async function seedUsers() { for (let i = 0; i < USERS_COUNT; i += BATCH_SIZE) { // Generate users const users = Array.from({ length: BATCH_SIZE }, generateUser); await storeUsersInRedis(users); console.log(users); console.log(`Batch ${i / BATCH_SIZE + 1} completed.`); } console.log('All users have been seeded and stored in Redis.'); process.exit(0); } seedUsers(); ================================================ FILE: examples/websockets/.gitignore ================================================ node_modules/ ================================================ FILE: examples/websockets/README.md ================================================ # WebSockets load testing example This example shows you how to test a [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server using Artillery's built-in WebSockets engine. ## Running the WebSockets server First, install the server dependencies: ``` npm install ``` After installing the dependencies, start the WebSockets server: ``` npm run server ``` This command will start a WebSockets server listening at ws://localhost:8888. ## Running Artillery test This directory contains a test script (`test.yml`) which demonstrates different test scenarios for load testing a WebSockets implementation. Once the WebSockets server is up and running, execute the test script: ``` npx artillery run test.yml ``` ================================================ FILE: examples/websockets/app.js ================================================ const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8888 }); wss.on('connection', (ws) => { ws.on('message', (msg) => { ws.send(msg); }); }); console.log('WebSockets server listening at ws://localhost:8888'); ================================================ FILE: examples/websockets/my-functions.js ================================================ module.exports = { createRandomScore }; function createRandomScore(userContext, _events, done) { const data = { timestamp: Date.now(), score: Math.floor(Math.random() * 100) }; // set the "data" variable for the virtual user to use in the subsequent action userContext.vars.data = data; return done(); } ================================================ FILE: examples/websockets/package.json ================================================ { "name": "websockets", "version": "1.0.0", "description": "Artillery.io - WebSockets example", "main": "app.js", "scripts": { "server": "node app.js", "test": "npx artillery run test.yml" }, "license": "ISC", "dependencies": { "ws": "^7.4.6" } } ================================================ FILE: examples/websockets/test.yml ================================================ config: target: 'ws://localhost:8888/' processor: './my-functions.js' phases: - duration: 60 arrivalRate: 25 scenarios: - name: 'sending_a_string' engine: ws flow: - send: 'Artillery' - name: 'sending_object_from_function' engine: ws flow: - function: 'createRandomScore' - send: '{{ data }}' ================================================ FILE: package.json ================================================ { "name": "artillery-monorepo", "packageManager": "^npm@10.8.2", "workspaces": [ "packages/*" ], "scripts": { "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint --continue", "lint-fix": "turbo run lint-fix --continue", "format": "npx prettier -w ./packages/**/*.{js,ts,json}", "prepare": "npx simple-git-hooks" }, "devDependencies": { "@biomejs/biome": "^2.3.3", "@commitlint/cli": "^9.1.2", "@commitlint/config-conventional": "^7.6.0", "lint-staged": "^13.2.3", "prettier": "^2.8.8", "simple-git-hooks": "^2.8.1", "turbo": "2.0.11" }, "simple-git-hooks": { "commit-msg": "npx commitlint --edit $1", "pre-commit": "npx lint-staged" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": [ "npx @biomejs/biome check --write --files-ignore-unknown=true --no-errors-on-unmatched", "prettier --write" ], "**/*.json": [ "prettier --write" ] }, "prettier": { "semi": true, "singleQuote": true, "useTabs": false, "trailingComma": "none", "overrides": [ { "files": "*.(yaml|yml)", "options": { "singleQuote": false } } ] } } ================================================ FILE: packages/artillery/Dockerfile ================================================ FROM node:22-bookworm-slim LABEL maintainer="team@artillery.io" WORKDIR /home/node/artillery COPY package*.json ./ RUN npm --ignore-scripts --production install RUN npx playwright install --with-deps chromium COPY . ./ ENV PATH="/home/node/artillery/bin:${PATH}" ENTRYPOINT ["/home/node/artillery/bin/run"] ================================================ FILE: packages/artillery/Makefile ================================================ man: ronn man/artillery.1.md --roff --organization=artillery.io man-docker: cat man/artillery.1.md | docker run --rm -i kadock/ronn > man/artillery.1 .PHONY: man ================================================ FILE: packages/artillery/README.md ================================================

Artillery

Docs | Discussions

npm

## Features - **Test at cloud scale.** Cloud-native distributed load testing at scale, **out-of-the box**. Scale out with AWS Lambda, AWS Fargate or Azure ACI. No DevOps needed, zero infrastructure to set up or manage. - **Test with Playwright**. Reuse existing Playwright tests and load test with real headless browsers. - **Batteries-included.** 20+ integrations for monitoring, observability, and CICD. - **Test anything**. HTTP, WebSocket, Socket.io, gRPC, Kinesis, and more. - **Powerful workload modeling**. Emulate complex user behavior with request chains, multiple steps, transactions, and more. - **Extensible & hackable**. Artillery has a plugin API to allow extending and customization. ## Get started ### Install Artillery ``` npm install -g artillery ``` ### Run your first test Follow our 5-minute guide to run your first load test - https://www.artillery.io/docs/get-started/first-test ## Learn more ### Docs and guides - [Load testing with Playwright](https://www.artillery.io/docs/playwright) - Distributed load testing with Artillery on [AWS Lambda](https://docs.art/lambda), [AWS Fargate](https://docs.art/fargate), or [Azure ACI](https://docs.art/azure) - Set [API response expectations](https://docs.art/expect), automate [SLO checks](https://docs.art/ensure), and report [Apdex scores](https://docs.art/apdex) - [Publishing metrics](https://docs.art/o11y) to Datadog, New Relic, Honeycomb, and any other OTel-compatible platform ### Integrations and plugins We maintain a list of official and community-built [integrations and plugins](https://www.artillery.io/integrations) on our website: https://www.artillery.io/integrations. ### Example tests You can find a list of ready-to-run Artillery examples under [`examples/`](https://github.com/artilleryio/artillery/tree/master/examples#readme). ================================================ FILE: packages/artillery/bin/run ================================================ #!/usr/bin/env node /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const oclif = require('@oclif/core'); const { createGlobalObject } = require('../lib/artillery-global'); const { rainbow } = require('../util'); const banner = require('../lib/cli/banner'); const setupConsoleCapture = require('../lib/console-capture'); async function main() { await createGlobalObject(); await setupConsoleCapture(); if (!process.argv.slice(2).length) { console.log(Math.random() * 100 > 34 ? banner : rainbow(banner)); } else if (process.argv.slice(2).length === 1 && process.argv.slice(2)[0] === '-V') { process.argv[2] = 'version'; } oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')); } main(); ================================================ FILE: packages/artillery/bin/run.cmd ================================================ @echo off node "%~dp0\run" %* ================================================ FILE: packages/artillery/console-reporter.js ================================================ module.exports = require('./lib/console-reporter'); ================================================ FILE: packages/artillery/doc/CLA.md ================================================ In order to clarify the intellectual property license granted with Contributions from any person or entity, Artillery Software Inc. ("Artillery") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use your own Contributions for any other purpose. You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Artillery. Except for the license granted herein to Artillery and recipients of software distributed by Artillery, You reserve all right, title, and interest in and to Your Contributions. 1. Definitions. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Artillery. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is or previously has been intentionally submitted by You to Artillery for inclusion in, or documentation of, any of the products owned or managed by Artillery (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Artillery or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Artillery for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Artillery and to recipients of software distributed by Artillery a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Artillery and to recipients of software distributed by Artillery a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that you will have received permission from your current and future employers for all future Contributions, that your applicable employer has waived such rights for all of your current and future Contributions to Artillery, or that your employer has executed a separate Corporate CLA with Artillery. 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 7. Should You wish to submit work that is not Your original creation, You may submit it to Artillery separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 8. You agree to notify Artillery of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. ================================================ FILE: packages/artillery/lib/artillery-global.js ================================================ const version = require('../package.json').version; const telemetry = require('./telemetry'); const { updateGlobalObject } = require('@artilleryio/int-core'); async function createGlobalObject(_opts) { await updateGlobalObject({ version, telemetry }); global.artillery.runtimeOptions = global.artillery.runtimeOptions || {}; global.artillery.runtimeOptions.legacyReporting = typeof process.env.ARTILLERY_USE_LEGACY_REPORT_FORMAT !== 'undefined'; global.artillery._workerThreadSend = global.artillery._workerThreadSend || null; global.artillery.__createReporter = require('./console-reporter'); global.artillery._exitCode = 0; global.artillery.shutdown = global.artillery.shutdown || ( async () => { // TODO: Move graceful shutdown logic into here process.exit(global.artillery.suggestedExitCode); }); } module.exports = { createGlobalObject }; ================================================ FILE: packages/artillery/lib/cli/banner.js ================================================ module.exports = ` ___ __ _ ____ _____/ | _____/ /_(_) / /__ _______ __ ___ /____/ /| | / ___/ __/ / / / _ \\/ ___/ / / /____/ /____/ ___ |/ / / /_/ / / / __/ / / /_/ /____/ /_/ |_/_/ \\__/_/_/_/\\___/_/ \\__ / /____/ `; ================================================ FILE: packages/artillery/lib/cli/common-flags.js ================================================ const { Flags } = require('@oclif/core'); const CommonRunFlags = { target: Flags.string({ char: 't', description: 'Set target endpoint. Overrides the target already set in the test script' }), config: Flags.string({ char: 'c', description: 'Read configuration for the test from the specified file' }), // TODO: Replace with --profile environment: Flags.string({ char: 'e', description: 'Use one of the environments specified in config.environments' }), 'scenario-name': Flags.string({ description: 'Name of the specific scenario to run' }), output: Flags.string({ char: 'o', description: 'Write a JSON report to file' }), dotenv: Flags.string({ description: 'Path to a dotenv file to load environment variables from', aliases: ['env-file'] }), variables: Flags.string({ char: 'v', description: 'Set variables available to vusers during the test; a JSON object' }), overrides: Flags.string({ description: 'Dynamically override values in the test script; a JSON object' }), insecure: Flags.boolean({ char: 'k', description: 'Allow insecure TLS connections; do not use in production' }), quiet: Flags.boolean({ char: 'q', description: 'Quiet mode' }), // multiple allows multiple arguments for the -i flag, which means that e.g.: // artillery -i one.yml -i two.yml main.yml // does not work as expected. Instead of being considered an argument, "main.yml" // is considered to be input for "-i" and oclif then complains about missing // argument input: Flags.string({ char: 'i', description: 'Input script file', multiple: true, hidden: true }), //Artillery Cloud options: name: Flags.string({ description: 'Name of the test run. This name will be shown in the Artillery Cloud dashboard. Equivalent to setting a "name" tag.' }), tags: Flags.string({ description: 'Comma-separated list of tags in key:value format to tag the test run with in Artillery Cloud, for example: --tags team:sqa,service:foo' }), note: Flags.string({ description: 'Add a note/annotation to the test run' }), record: Flags.boolean({ description: 'Record test run to Artillery Cloud' }), key: Flags.string({ description: 'API key for Artillery Cloud' }) }; module.exports = { CommonRunFlags }; ================================================ FILE: packages/artillery/lib/cli/hooks/version.js ================================================ const banner = require('../banner'); const version = require('../../../package.json').version; async function versionHook() { if (['-v', '-V', '--version', 'version'].includes(process.argv[2])) { console.log(banner); console.log(` VERSION INFO: Artillery: ${version} Node.js: ${process.version} OS: ${process.platform} `); return process.exit(0); } } module.exports = versionHook; ================================================ FILE: packages/artillery/lib/cmds/dino.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { Command, Flags } = require('@oclif/core'); const { rainbow } = require('../util'); class DinoCommand extends Command { async run() { const { flags } = await this.parse(DinoCommand); let output = ''; const message = flags.message ? flags.message : flags.quiet ? "You can't silence me!" : 'Artillery!'; const n = message.length + 2; const balloon = ` ${'-'.repeat(n)}\n< ${message} >\n ${'-'.repeat(n)}\n`; output += `${balloon} \\\n \\\n`; const i = Math.floor(Math.random() * dinos.length); output += dinos[i]; if (flags.rainbow) { console.log(rainbow(output)); } else { console.log(output); } } } DinoCommand.description = 'here be dinosaurs'; DinoCommand.flags = { message: Flags.string({ char: 'm', description: 'Tell dinosaur what to say' }), rainbow: Flags.boolean({ char: 'r', description: 'Add some color' }), quiet: Flags.boolean({ char: 'q', description: 'Quiet mode' }) }; const dinos = [ ' __' + '\n' + ' / _)' + '\n' + ' _/\\/\\/\\_/ /' + '\n' + ' _| /' + '\n' + ' _| ( | ( |' + '\n' + "/__.-'|_|--|_|" + '\n', ' __ \n' + ' / _) \n' + ' .-^^^-/ / \n' + ' __/ / \n' + '<__.|_|-|_|', ' .@\n' + ' @.+\n' + ' @,\n' + " @'\n" + " @'\n" + ' @;\n' + ' `@;\n' + ' @+;\n' + " .@#;'\n" + " #@###@;'.\n" + ' :#@@@@@;.\n' + " @@@+;'@@:\n" + " `@@@';;;@@\n" + ' @;:@@;;;;+#\n' + '`@;` ,@@,, @@`\n' + ' @`@ @`+\n' + ' @ , @ @\n' + ' @ @ @ @', ' ..`\n' + " ;:,'+;\n" + ' ,;;,,;,\n' + " #:;':\n" + " @';'+;\n" + " `::;''';\n" + " '; ,:'+;;\n" + " `,,` .;';'+;'\n" + " ; `'+;;;::';++':,;\n" + " `+++++##+'';#\n" + " .;+##+''';\n" + " '+##'''#'\n" + " ++# +;'.##\n" + ' ##, `: .#,\n' + " :# '+\n" + " #. '\n" + ' # +\n' + " :+ #'\n" + " #+` ';." ]; module.exports = DinoCommand; ================================================ FILE: packages/artillery/lib/cmds/quick.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const RunCommand = require('./run'); const parse = require('node:url').parse; const fs = require('node:fs'); const _ = require('lodash'); const _debug = require('debug')('commands:quick'); const { Command, Flags, Args } = require('@oclif/core'); class QuickCommand extends Command { async run() { const { flags, args } = await this.parse(QuickCommand); const url = args.target; const script = { config: { target: '', phases: [], mode: 'uniform', __createdByQuickCommand: true }, scenarios: [ { flow: [] } ] }; const p = parse(url); const target = `${p.protocol}//${p.host}`; script.config.target = target; if (flags.insecure && p.protocol.match(/https/)) { script.config.tls = { rejectUnauthorized: false }; } script.config.phases.push({ duration: 1, arrivalCount: flags.count }); let requestSpec = {}; if (p.protocol.match('http')) { requestSpec.get = { url: url }; } else if (p.protocol.match('ws')) { requestSpec.send = 'hello from Artillery!'; } else { console.error('Unknown protocol in target:', args.target); console.error('Supported protocols: HTTP(S) and WS(S)'); process.exit(1); } if (flags.num > 1) { requestSpec = { loop: [requestSpec], count: flags.num }; } script.scenarios[0].flow.push(requestSpec); if (p.protocol.match(/ws/)) { script.scenarios[0].engine = 'ws'; } const temporaryFile = (await import('tempy')).temporaryFile; const tmpf = temporaryFile({ extension: 'yml' }); fs.writeFileSync(tmpf, JSON.stringify(script, null, 2), { flag: 'w' }); const runArgs = []; if (flags.output) { runArgs.push('--output'); runArgs.push(flags.output); } if (flags.quiet) { runArgs.push('--quiet'); } runArgs.push(tmpf); RunCommand.run(runArgs); } } QuickCommand.description = 'run a simple test without writing a test script'; QuickCommand.flags = { count: Flags.string({ char: 'c', description: 'Number of VUs to create', parse: (input) => parseInt(input, 10), default: 10 }), num: Flags.string({ char: 'n', description: 'Number of requests/messages that each VU will send', parse: (input) => parseInt(input, 10), default: 10 }), output: Flags.string({ char: 'o', description: 'Filename of the JSON report' }), insecure: Flags.boolean({ char: 'k', description: 'Allow insecure TLS connections' }), quiet: Flags.boolean({ char: 'q', description: 'Quiet mode' }) }; QuickCommand.args = { target: Args.string() }; module.exports = QuickCommand; ================================================ FILE: packages/artillery/lib/cmds/report.js ================================================ const { Command, Flags, Args } = require('@oclif/core'); const chalk = require('chalk'); class ReportCommand extends Command { async run() { console.error(deprecationNotice); } } ReportCommand.description = 'generate a HTML report from a JSON log produced with artillery run'; ReportCommand.flags = { output: Flags.string({ char: 'o', description: 'Write HTML report to specified location' }) }; const deprecationNotice = ` ┌───────────────────────────────────────────────────────────────────────┐ | ${chalk.blue( 'The "report" command has been deprecated and is no longer supported' )} | | | | You can use Artillery Cloud (https://app.artillery.io) to visualize | | test results, create custom reports, and share them with your team. | └───────────────────────────────────────────────────────────────────────┘ `; ReportCommand.args = { file: Args.string() }; module.exports = ReportCommand; ================================================ FILE: packages/artillery/lib/cmds/run-aci.js ================================================ // Copyright (c) Artillery Software Inc. // SPDX-License-Identifier: BUSL-1.1 // // Non-evaluation use of Artillery on Azure requires a commercial license const { Command, Flags, Args } = require('@oclif/core'); const { CommonRunFlags } = require('../cli/common-flags'); const RunCommand = require('./run'); class RunACICommand extends Command { static aliases = ['run:aci']; static strict = false; async run() { const { flags, argv, args } = await this.parse(RunACICommand); flags.platform = 'az:aci'; flags['platform-opt'] = [ `region=${flags.region}`, `count=${flags.count}`, `cpu=${flags.cpu}`, `memory=${flags.memory}`, `tenant-id=${flags['tenant-id']}`, `subscription-id=${flags['subscription-id']}`, `storage-account=${flags['storage-account']}`, `blob-container=${flags['blob-container']}`, `resource-group=${flags['resource-group']}` ]; RunCommand.runCommandImplementation(flags, argv, args); } } RunACICommand.description = `launch a test using Azure ACI Launch a test on Azure ACI Examples: To run a test script in my-test.yml on Azure ACI in eastus region with 10 workers: $ artillery run:aci --region eastus --count 10 my-test.yml `; RunACICommand.flags = { ...CommonRunFlags, count: Flags.string({ default: '1' }), region: Flags.string({ description: 'Azure region to run the test in', default: 'eastus' }), cpu: Flags.string({ description: 'Number of CPU cores per worker (defaults to 4 CPUs). A number between 1-4.' }), memory: Flags.string({ description: 'Memory in GB per worker (defaults to 8 GB). A number between 1-16.' }), 'tenant-id': Flags.string({ description: 'Azure tenant ID. May also be set via AZURE_TENANT_ID environment variable.' }), 'subscription-id': Flags.string({ description: 'Azure subscription ID. May also be set via AZURE_SUBSCRIPTION_ID environment variable.' }), 'storage-account': Flags.string({ description: 'Azure Blob Storage account name. May also be set via AZURE_STORAGE_ACCOUNT environment variable.' }), 'blob-container': Flags.string({ description: 'Azure Blob Storage container name. May also be set via AZURE_STORAGE_BLOB_CONTAINER environment variable.' }), 'resource-group': Flags.string({ description: 'Azure Resource Group name. May also be set via AZURE_RESOURCE_GROUP environment variable.' }) }; RunACICommand.args = { script: Args.string({ name: 'script', required: true }) }; module.exports = RunACICommand; ================================================ FILE: packages/artillery/lib/cmds/run-fargate.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { Command, Flags, Args } = require('@oclif/core'); const { CommonRunFlags } = require('../cli/common-flags'); const telemetry = require('../telemetry'); const runCluster = require('../platform/aws-ecs/legacy/run-cluster'); const { supportedRegions } = require('../platform/aws-ecs/legacy/util'); const PlatformECS = require('../platform/aws-ecs/ecs'); const { ECS_WORKER_ROLE_NAME } = require('../platform/aws/constants'); const { Plugin: CloudPlugin } = require('../platform/cloud/cloud'); const generateId = require('../util/generate-id'); const dotenv = require('dotenv'); const path = require('node:path'); const fs = require('node:fs'); class RunCommand extends Command { static aliases = ['run:fargate', 'run:ecs', 'run-ecs']; // Enable multiple args: static strict = false; async run() { const { flags, _argv, args } = await this.parse(RunCommand); flags['platform-opt'] = [`region=${flags.region}`]; flags.platform = 'aws:ecs'; if (flags.dotenv) { const dotEnvPath = path.resolve(process.cwd(), flags.dotenv); try { fs.statSync(dotEnvPath); } catch (_err) { console.log(`WARNING: could not read dotenv file: ${flags.dotenv}`); } dotenv.config({ path: dotEnvPath }); } const testRunId = process.env.ARTILLERY_TEST_RUN_ID || generateId('t'); global.artillery.testRunId = testRunId; const cloud = new CloudPlugin(null, null, { flags }); global.artillery.cloudEnabled = cloud.enabled; if (cloud.enabled) { try { await cloud.init(); } catch (err) { if (err.name === 'CloudAPIKeyMissing') { console.error( 'Error: API key is required to record test results to Artillery Cloud' ); console.error( 'See https://docs.art/get-started-cloud for more information' ); process.exit(7); } else if (err.name === 'APIKeyUnauthorized') { console.error( 'Error: API key is not recognized or is not authorized to record tests' ); process.exit(7); } else if (err.name === 'PingFailed') { console.error( 'Error: unable to reach Artillery Cloud API. This could be due to firewall restrictions on your network' ); console.log('Please see https://docs.art/cloud/err-ping'); process.exit(7); } else { console.error( 'Error: something went wrong connecting to Artillery Cloud' ); console.error('Check https://status.artillery.io for status updates'); console.error(err); } } } flags.taskRoleName = flags['task-role-name'] || ECS_WORKER_ROLE_NAME; const ECS = new PlatformECS( null, null, {}, { testRunId: 'foo', region: flags.region, taskRoleName: flags.taskRoleName } ); await ECS.init(); process.env.USE_NOOP_BACKEND_STORE = 'true'; telemetry.capture('run:fargate', { region: flags.region, count: flags.count }); // Delegate the rest to existing implementation: runCluster(args.script, flags); } } RunCommand.description = `launch a test using AWS ECS/Fargate Examples: To launch a test with 10 load generating workers using AWS Fargate in us-east-1: $ artillery run:fargate --count 10 --region us-east-1 my-test.yml `; RunCommand.flags = { ...CommonRunFlags, count: Flags.integer({ description: 'Number of load generator workers to launch' }), cluster: Flags.string({ description: 'Name of the Fargate/ECS cluster to run the test on' }), region: Flags.string({ char: 'r', description: 'The AWS region to run in', options: supportedRegions, default: 'us-east-1' }), secret: Flags.string({ multiple: true, description: 'Make secrets available to workers. The secret must exist in SSM parameter store for the given region, under /artilleryio/' }), 'launch-type': Flags.string({ description: 'The launch type to use for the test. Defaults to Fargate.', options: ['ecs:fargate', 'ecs:ec2'] }), spot: Flags.boolean({ description: 'Use Fargate Spot (https://docs.art/fargate-spot) Ignored when --launch-type is set to ecs:ec2' }), 'launch-config': Flags.string({ description: 'JSON to customize launch configuration of ECS/Fargate tasks (see https://www.artillery.io/docs/reference/cli/run-fargate#using---launch-config)' }), 'container-dns-servers': Flags.string({ description: 'Comma-separated list of DNS servers for Artillery container. Maps to dnsServers parameter in ECS container definition' }), 'task-ephemeral-storage': Flags.string({ description: 'Ephemeral storage in GiB for the worker task. Maps to ephemeralStorage parameter in ECS container definition. Fargate-only.', type: 'integer' }), 'subnet-ids': Flags.string({ description: 'Comma-separated list of AWS VPC subnet IDs to launch Fargate tasks in' }), 'security-group-ids': Flags.string({ description: 'Comma-separated list of AWS VPC security group IDs to launch Fargate tasks in' }), 'task-role-name': Flags.string({ description: 'Custom IAM role name for Fargate containers to assume' }), cpu: Flags.string({ description: 'Set task vCPU on Fargate (defaults to 4 vCPU). Value may be set as a number of vCPUs between 1-16 (e.g. 4), or as number of vCPU units (e.g. 4096).' }), memory: Flags.string({ description: 'Set task memory on Fargate (defaults to 8 GB). Value may be set as number of GB between 1-120 (e.g. 8), or as MiB (e.g. 8192)' }), packages: Flags.string({ description: 'Path to package.json file which lists dependencies for the test script' }), 'max-duration': Flags.string({ description: 'Maximum duration of the test run' }), 'no-assign-public-ip': Flags.boolean({ description: 'Turn off the default behavior of assigning public IPs to Fargate worker tasks. When this option is used you must make sure tasks have a route to the internet, i.e. via a NAT gateway attached to a private subnet', default: false }) }; RunCommand.args = { script: Args.string() }; module.exports = RunCommand; ================================================ FILE: packages/artillery/lib/cmds/run-lambda.js ================================================ const { Command, Flags, Args } = require('@oclif/core'); const { CommonRunFlags } = require('../cli/common-flags'); const RunCommand = require('./run'); class RunLambdaCommand extends Command { static aliases = ['run:lambda']; static strict = false; async run() { const { flags, argv, args } = await this.parse(RunLambdaCommand); flags['platform-opt'] = [ `region=${flags.region}`, `memory-size=${flags['memory-size']}`, `architecture=${flags.architecture}` ]; delete flags.region; delete flags['memory-size']; delete flags.architecture; if (flags['lambda-role-arn']) { flags['platform-opt'].push(`lambda-role-arn=${flags['lambda-role-arn']}`); } if (flags['security-group-ids']) { flags['platform-opt'].push( `security-group-ids=${flags['security-group-ids']}` ); } if (flags['subnet-ids']) { flags['platform-opt'].push(`subnet-ids=${flags['subnet-ids']}`); } flags.platform = 'aws:lambda'; RunCommand.runCommandImplementation(flags, argv, args); } } RunLambdaCommand.description = `launch a test using AWS Lambda Launch a test on AWS Lambda Examples: To run a test script in my-test.yml on AWS Lambda in us-east-1 region distributed across 10 Lambda functions: $ artillery run:lambda --region us-east-1 --count 10 my-test.yml `; RunLambdaCommand.flags = { ...CommonRunFlags, payload: Flags.string({ char: 'p', description: 'Specify a CSV file for dynamic data' }), count: Flags.string({ // locally defaults to number of CPUs with mode = distribute default: '1' }), architecture: Flags.string({ description: 'Architecture of the Lambda function', default: 'arm64', options: ['arm64', 'x86_64'] }), 'memory-size': Flags.string({ description: 'Memory size of the Lambda function', default: '4096' }), region: Flags.string({ description: 'AWS region to run the test in', default: 'us-east-1' }), 'lambda-role-arn': Flags.string({ description: 'ARN of the IAM role to use for the Lambda function' }), 'security-group-ids': Flags.string({ description: 'Comma-separated list of security group IDs to use for the Lambda function' }), 'subnet-ids': Flags.string({ description: 'Comma-separated list of subnet IDs to use for the Lambda function' }) }; RunLambdaCommand.args = { script: Args.string({ name: 'script', required: true }) }; module.exports = RunLambdaCommand; ================================================ FILE: packages/artillery/lib/cmds/run.js ================================================ const { Command, Flags, Args } = require('@oclif/core'); const { CommonRunFlags } = require('../cli/common-flags'); const p = require('node:util').promisify; const _csv = require('csv-parse'); const debug = require('debug')('commands:run'); const dotenv = require('dotenv'); const _ = require('lodash'); const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); const os = require('node:os'); const createLauncher = require('../launch-platform'); const createConsoleReporter = require('../../console-reporter'); const moment = require('moment'); const { SSMS } = require('@artilleryio/int-core').ssms; const telemetry = require('../telemetry'); const { Plugin: CloudPlugin } = require('../platform/cloud/cloud'); const parseTagString = require('../util/parse-tag-string'); const generateId = require('../util/generate-id'); const prepareTestExecutionPlan = require('../util/prepare-test-execution-plan'); class RunCommand extends Command { static aliases = ['run']; // Enable multiple args: static strict = false; async run() { const { flags, argv, args } = await this.parse(RunCommand); if (flags.platform === 'aws:ecs') { // Delegate to existing implementation const RunFargateCommand = require('./run-fargate'); return await RunFargateCommand.run(argv); } await RunCommand.runCommandImplementation(flags, argv, args); } // async catch(err) { // throw err; // } } // Line no. 2 onwards is the description in help output RunCommand.description = `run a test script locally or on AWS Lambda Run a test script Examples: To run a test script in my-test.yml to completion from the local machine: $ artillery run my-test.yml To run a test script but override target dynamically: $ artillery run -t https://app2.acmecorp.internal my-test.yml `; // TODO: Link to an Examples section in the docs RunCommand.flags = { ...CommonRunFlags, // TODO: Deprecation notices for commands below: payload: Flags.string({ char: 'p', description: 'Specify a CSV file for dynamic data' }), solo: Flags.boolean({ char: 's', description: 'Create only one virtual user' }), platform: Flags.string({ description: 'Runtime platform', default: 'local', options: ['local', 'aws:lambda', 'az:aci'] }), 'platform-opt': Flags.string({ description: 'Set a platform-specific option, e.g. --platform-opt region=eu-west-1 for AWS Lambda', multiple: true }), count: Flags.string({ // locally defaults to number of CPUs with mode = distribute default: '1' }) }; RunCommand.args = { script: Args.string({ name: 'script', required: true }) }; let cloud; RunCommand.runCommandImplementation = async (flags, argv, args) => { // Collect all input files for reading/parsing - via args, --config, or -i const inputFiles = argv.concat(flags.input || [], flags.config || []); const tagResult = parseTagString(flags.tags); if (tagResult.errors.length > 0) { console.log( 'WARNING: could not parse some tags:', tagResult.errors.join(', ') ); } if (tagResult.tags.length > 10) { console.log('A maximum of 10 tags is supported'); process.exit(1); } // TODO: Move into PlatformLocal if (flags.dotenv) { const dotEnvPath = path.resolve(process.cwd(), flags.dotenv); try { fs.statSync(dotEnvPath); } catch (_err) { console.log(`WARNING: could not read dotenv file: ${flags.dotenv}`); } dotenv.config({ path: dotEnvPath }); } if (flags.output) { if (!checkDirExists(flags.output)) { console.error('Path does not exist:', flags.output); process.exit(1); } } const testRunId = process.env.ARTILLERY_TEST_RUN_ID || generateId('t'); console.log('Test run id:', testRunId); global.artillery.testRunId = testRunId; cloud = new CloudPlugin(null, null, { flags }); global.artillery.cloudEnabled = cloud.enabled; if (cloud.enabled) { try { await cloud.init(); } catch (err) { if (err.name === 'CloudAPIKeyMissing') { console.error( 'Error: API key is required to record test results to Artillery Cloud' ); console.error( 'See https://docs.art/get-started-cloud for more information' ); await gracefulShutdown({ exitCode: 7 }); } else if (err.name === 'APIKeyUnauthorized') { console.error( 'Error: API key is not recognized or is not authorized to record tests' ); await gracefulShutdown({ exitCode: 7 }); } else if (err.name === 'PingFailed') { console.error( 'Error: unable to reach Artillery Cloud API. This could be due to firewall restrictions on your network' ); console.log('https://docs.art/cloud/err-ping'); await gracefulShutdown({ exitCode: 7 }); } else { console.error( 'Error: something went wrong connecting to Artillery Cloud' ); console.error('Check https://status.artillery.io for status updates'); console.error(err); } } } let script; try { script = await prepareTestExecutionPlan(inputFiles, flags, args); } catch (err) { console.error('Error:', err.message); await gracefulShutdown({ exitCode: 1 }); } var runnerOpts = { environment: flags.environment, // This is used in the worker to resolve // the path to the processor module scriptPath: args.script, // TODO: This should be an array of files, like inputFiles above absoluteScriptPath: path.resolve(process.cwd(), args.script), plugins: [], scenarioName: flags['scenario-name'] }; // Set "name" tag if not set explicitly if (tagResult.tags.filter((t) => t.name === 'name').length === 0) { tagResult.tags.push({ name: 'name', value: path.basename(runnerOpts.scriptPath) }); } // Override the "name" tag with the value of --name if set if (flags.name) { for (const t of tagResult.tags) { if (t.name === 'name') { t.value = flags.name; } } } if (flags.config) { runnerOpts.absoluteConfigPath = path.resolve(process.cwd(), flags.config); } if (process.env.WORKERS) { runnerOpts.count = parseInt(process.env.WORKERS, 10) || 1; } if (flags.solo) { runnerOpts.count = 1; } const platformConfig = {}; if (flags['platform-opt']) { for (const opt of flags['platform-opt']) { const [k, v] = opt.split('='); platformConfig[k] = v; } } const launcherOpts = { platform: flags.platform, platformConfig, mode: flags.platform === 'local' ? 'distribute' : 'multiply', count: parseInt(flags.count || 1, 10), cliArgs: flags, testRunId }; var launcher = await createLauncher( script, script.config.payload, runnerOpts, launcherOpts ); if (!launcher) { console.log('Failed to create launcher'); await gracefulShutdown({ exitCode: 1 }); } const intermediates = []; const metricsToSuppress = getPluginMetricsToSuppress(script); // TODO: Wire up workerLog or something like that const consoleReporter = createConsoleReporter(launcher.events, { quiet: flags.quiet || false, metricsToSuppress }); var reporters = [consoleReporter]; if (process.env.CUSTOM_REPORTERS) { const customReporterNames = process.env.CUSTOM_REPORTERS.split(','); customReporterNames.forEach((name) => { const createReporter = require(name); const reporter = createReporter(launcher.events, flags); reporters.push(reporter); }); } launcher.events.on('phaseStarted', (_phase) => {}); launcher.events.on('stats', (stats) => { if (artillery.runtimeOptions.legacyReporting) { const report = SSMS.legacyReport(stats).report(); intermediates.push(report); } else { intermediates.push(stats); } }); launcher.events.on('done', async (stats) => { let report; if (artillery.runtimeOptions.legacyReporting) { report = SSMS.legacyReport(stats).report(); report.phases = _.get(script, 'config.phases', []); } else { report = stats; } if (flags.output) { const logfile = getLogFilename(flags.output); if (!flags.quiet) { console.log('Log file: %s', logfile); } for (const ix of intermediates) { delete ix.histograms; ix.histograms = ix.summaries; } delete report.histograms; report.histograms = report.summaries; fs.writeFileSync( logfile, JSON.stringify( { aggregate: report, intermediate: intermediates }, null, 2 ), { flag: 'w' } ); } // This is used in the beforeExit event handler in gracefulShutdown finalReport = report; await gracefulShutdown(); }); global.artillery.ext({ ext: 'beforeExit', method: async (event) => { try { const duration = Math.round( (event.report?.lastMetricAt - event.report?.firstMetricAt) / 1000 ); await sendTelemetry(script, flags, { duration }); } catch (_err) {} } }); global.artillery.globalEvents.emit('test:init', { flags, testRunId, tags: tagResult.tags, metadata: { testId: testRunId, startedAt: Date.now(), count: runnerOpts.count || Number(flags.count), tags: tagResult.tags, launchType: flags.platform, artilleryVersion: { core: global.artillery.version }, // Properties from the runnable script object: testConfig: { target: script.config.target, phases: script.config.phases, plugins: script.config.plugins, environment: script._environment, scriptPath: script._scriptPath, configPath: script._configPath } } }); launcher.run(); var finalReport = {}; var shuttingDown = false; process.on('SIGINT', async () => { gracefulShutdown({ earlyStop: true, exitCode: 130 }); }); process.on('SIGTERM', async () => { gracefulShutdown({ earlyStop: true, exitCode: 143 }); }); async function gracefulShutdown(opts = { exitCode: 0 }) { debug('shutting down 🦑'); if (shuttingDown) { return; } debug('Graceful shutdown initiated'); shuttingDown = true; global.artillery.globalEvents.emit('shutdown:start', opts); // Run beforeExit first, and then onShutdown const ps = []; for (const e of global.artillery.extensionEvents) { const testInfo = { endTime: Date.now() }; if (e.ext === 'beforeExit') { ps.push( e.method({ ...opts, report: finalReport, flags, runnerOpts, testInfo }) ); } } await Promise.allSettled(ps); const ps2 = []; for (const e of global.artillery.extensionEvents) { if (e.ext === 'onShutdown') { ps2.push(e.method(opts)); } } await Promise.allSettled(ps2); if (launcher) { await launcher.shutdown(); } await (async () => { if (reporters) { for (const r of reporters) { if (r.cleanup) { try { await p(r.cleanup.bind(r))(); } catch (cleanupErr) { debug(cleanupErr); } } } } if ( global.artillery.hasTypescriptProcessor && !process.env.ARTILLERY_TS_KEEP_BUNDLE ) { try { fs.unlinkSync(global.artillery.hasTypescriptProcessor); } catch (err) { console.log( `WARNING: Failed to remove typescript bundled file: ${global.artillery.hasTypescriptProcessor}` ); console.log(err); } try { fs.rmdirSync(path.dirname(global.artillery.hasTypescriptProcessor)); } catch (_err) {} } debug('Cleanup finished'); process.exit(artillery.suggestedExitCode || opts.exitCode); })(); } global.artillery.shutdown = gracefulShutdown; }; async function sendTelemetry(script, flags, extraProps) { if (process.env.WORKER_ID) { debug('Telemetry: Running in cloud worker, skipping test run event'); return; } function hash(str) { return crypto.createHash('sha1').update(str).digest('base64'); } const properties = {}; if (script.config?.__createdByQuickCommand) { properties.quick = true; } if (cloud?.enabled && cloud.user) { properties.cloud = cloud.user; } properties.solo = flags.solo; try { // One-way hash of target endpoint: if (script.config?.target) { const targetHash = hash(script.config.target); properties.targetHash = targetHash; } if (flags.target) { const targetHash = hash(flags.target); properties.targetHash = targetHash; } properties.platform = flags.platform; properties.count = flags.count; if (properties.targetHash) { properties.distinctId = properties.targetHash; } let macaddr; const nonInternalIpv6Interfaces = []; for (const [_iface, descrs] of Object.entries(os.networkInterfaces())) { for (const o of descrs) { if (o.internal === true) { continue; } //prefer ipv4 interface when available if (o.family !== 'IPv4') { nonInternalIpv6Interfaces.push(o); continue; } macaddr = o.mac; break; } } //default to first ipv6 interface if no ipv4 interface is available if (!macaddr && nonInternalIpv6Interfaces.length > 0) { macaddr = nonInternalIpv6Interfaces[0].mac; } if (macaddr) { properties.macHash = hash(macaddr); } properties.hostnameHash = hash(os.hostname()); properties.usernameHash = hash(os.userInfo().username); if (script.config?.engines) { properties.loadsEngines = true; } properties.enginesUsed = []; const OSS_ENGINES = [ 'http', 'socketio', 'ws', 'playwright', 'kinesis', 'socketio-v3', 'rediscluster', 'kafka', 'tcp', 'grpc', 'meteor', 'graphql-ws', 'ldap', 'lambda' ]; for (const scenario of script.scenarios || []) { if (OSS_ENGINES.indexOf(scenario.engine || 'http') > -1) { if (properties.enginesUsed.indexOf(scenario.engine || 'http') === -1) { properties.enginesUsed.push(scenario.engine || 'http'); } } } // Official plugins: if (script.config.plugins) { properties.plugins = true; properties.officialPlugins = []; const OFFICIAL_PLUGINS = [ 'apdex', 'expect', 'publish-metrics', 'metrics-by-endpoint', 'hls', 'fuzzer', 'ensure', 'memory-inspector', 'fake-data', 'slack' ]; for (const p of OFFICIAL_PLUGINS) { if (script.config.plugins[p]) { properties.officialPlugins.push(p); } } } // publish-metrics reporters if (script.config.plugins['publish-metrics']) { const OFFICIAL_REPORTERS = [ 'datadog', 'open-telemetry', 'newrelic', 'splunk', 'dynatrace', 'cloudwatch', 'honeycomb', 'mixpanel', 'prometheus' ]; properties.officialMonitoringReporters = script.config.plugins[ 'publish-metrics' ] .map((reporter) => { if (OFFICIAL_REPORTERS.includes(reporter.type)) { return reporter.type; } return undefined; }) .filter((type) => type !== undefined); } // before/after hooks if (script.before) { properties.beforeHook = true; } if (script.after) { properties.afterHook = true; } Object.assign(properties, extraProps); } catch (err) { debug(err); } finally { telemetry.capture('test run', properties); } } function checkDirExists(output) { if (!output) { return; } // If destination is a file check only path to its directory const exists = path.extname(output) ? fs.existsSync(path.dirname(output)) : fs.existsSync(output); return exists; } function getLogFilename(output, nameFormat) { let logfile; // is the destination a directory that exists? let isDir = false; if (output) { try { isDir = fs.statSync(output).isDirectory(); } catch (_err) { // ENOENT do nothing, handled in checkDirExists before test run } } const defaultFormat = '[artillery_report_]YMMDD_HHmmSS[.json]'; if (!isDir && output) { // -o is set with a filename (existing or not) logfile = output; } else if (!isDir && !output) { // no -o set } else { // -o is set with a directory logfile = path.join(output, moment().format(nameFormat || defaultFormat)); } return logfile; } function getPluginMetricsToSuppress(script) { if (!script.config.plugins) { return []; } const metrics = []; for (const [plugin, options] of Object.entries(script.config.plugins)) { if (options.suppressOutput) { metrics.push(`plugins.${plugin}`); } } return metrics; } module.exports = RunCommand; ================================================ FILE: packages/artillery/lib/console-capture.js ================================================ const debug = require('debug')('console-capture'); function setupConsoleCapture() { let outputLines = []; let truncated = false; let currentSize = 0; let sendFromIndex = 0; const MAX_RETAINED_LOG_SIZE_MB = Number( process.env.MAX_RETAINED_LOG_SIZE_MB || '50' ); const MAX_RETAINED_LOG_SIZE = MAX_RETAINED_LOG_SIZE_MB * 1024 * 1024; const interval = setInterval(() => { if (!truncated && outputLines.length - sendFromIndex > 0) { const newBatch = outputLines.slice(sendFromIndex, outputLines.length); sendFromIndex = outputLines.length; global.artillery.globalEvents.emit('logLines', newBatch, Date.now()); } }, 10 * 1000).unref(); global.artillery.ext({ ext: 'onShutdown', method: async () => { debug('onBeforeExit', sendFromIndex, outputLines.length); clearInterval(interval); if (!truncated && sendFromIndex < outputLines.length) { const ts = Date.now(); global.artillery.globalEvents.emit( 'logLines', outputLines.slice(sendFromIndex, outputLines.length), ts, true ); sendFromIndex = outputLines.length; } } }); console.log = (() => { const orig = console.log; return (...args) => { try { orig.apply(console, args); if (currentSize < MAX_RETAINED_LOG_SIZE) { outputLines = outputLines.concat([args]); for (const x of args) { currentSize += String(x).length; } } else { if (!truncated) { truncated = true; const msg = `[WARNING] Artillery: maximum retained log size exceeded, max size: ${MAX_RETAINED_LOG_SIZE_MB}MB. Further logs won't be retained.\n\n`; process.stdout.write(msg); outputLines = outputLines.concat([msg]); } } } catch (err) { debug(err); } }; })(); console.error = (() => { const orig = console.error; return (...args) => { try { orig.apply(console, args); if (currentSize < MAX_RETAINED_LOG_SIZE) { outputLines = outputLines.concat([args]); for (const x of args) { currentSize += String(x).length; } } else { if (!truncated) { truncated = true; const msg = `[WARNING] Artillery: maximum retained log size exceeded, max size: ${MAX_RETAINED_LOG_SIZE_MB}MB. Further logs won't be retained.\n\n`; process.stdout.write(msg); outputLines = outputLines.concat([msg]); } } } catch (err) { debug(err); } }; })(); } module.exports = setupConsoleCapture; ================================================ FILE: packages/artillery/lib/console-reporter.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const ora = require('ora'); const _ = require('lodash'); const moment = require('moment'); const chalk = require('chalk'); const Table = require('cli-table3'); const util = require('./util'); const SSMS = require('@artilleryio/int-core').ssms.SSMS; module.exports = createConsoleReporter; function createConsoleReporter(events, opts) { const reporter = new ConsoleReporter(opts); events.on('phaseStarted', reporter.phaseStarted.bind(reporter)); events.on('phaseCompleted', reporter.phaseCompleted.bind(reporter)); // TODO: Not firing - event not propagating? events.on('stats', reporter.stats.bind(reporter)); events.on('done', reporter.done.bind(reporter)); reporter.start(); return reporter; } function ConsoleReporter(opts) { this.opts = opts || {}; this.outputFormat = opts.outputFormat || process.env.OUTPUT_FORMAT || 'new'; this.quiet = opts.quiet; this.metricsToSuppress = opts.metricsToSuppress; this.spinner = ora({ spinner: 'dots' }); this.spinner.start(); this.reportScenarioLatency = !!opts.reportScenarioLatency; this.startTime = null; global.artillery.globalEvents.on('log', (opts, ...args) => { let logger; if (typeof opts.level !== 'undefined' && opts.level !== 'info') { logger = console.error; } else { logger = console.log; } if (opts.showTimestamp) { args.push(chalk.gray(`[${moment().format('HH:mm:ss')}]`)); } this.spinner.clear(); logger.apply(console, [...args]); this.spinner.start(); }); return this; } ConsoleReporter.prototype.cleanup = function (done) { this.spinner.clear(); return done(null); }; ConsoleReporter.prototype.start = function start() { if (this.quiet) { return this; } // artillery.log(`Artillery running - ${moment(Date.now()).toISOString()}\n`); return this; }; ConsoleReporter.prototype.phaseStarted = function phaseStarted(phase) { if (this.quiet) { return this; } const phaseDuration = phase.duration || phase.pause; //only append s when phaseDuration is a number or number-like string (like from env variables). otherwise it's a converted unit (e.g. 5min) const durationString = Number.isInteger(_.toNumber(phaseDuration)) ? `${phaseDuration}s` : `${phaseDuration}`; artillery.log( `Phase started: ${chalk.green( phase.name ? phase.name : 'unnamed' )} (index: ${phase.index}, duration: ${ durationString }) ${formatTimestamp(new Date())}\n` ); }; ConsoleReporter.prototype.phaseCompleted = function phaseCompleted(phase) { if (this.quiet) { return this; } const phaseDuration = phase.duration || phase.pause; //only append s when phaseDuration is a number or number-like string (like from env variables). otherwise it's a converted unit (e.g. 5min) const durationString = Number.isInteger(_.toNumber(phaseDuration)) ? `${phaseDuration}s` : `${phaseDuration}`; artillery.log( `Phase completed: ${chalk.green( phase.name ? phase.name : 'unnamed' )} (index: ${phase.index}, duration: ${ durationString }) ${formatTimestamp(new Date())}\n` ); return this; }; ConsoleReporter.prototype.stats = function stats(data) { if (this.quiet) { return this; } if (!this.startTime) { this.startTime = data.firstMetricAt || Date.now(); } // NOTE: histograms property is available and contains raw // histogram objects data.summaries = data.summaries || {}; data.counters = data.counters || {}; // data.rates = data.rates || {}; if (typeof data.report === 'function') { // Compatibility fix with Artillery Pro which uses 1.x // API for emitting reports to console-reporter. // TODO: Remove when support for 1x is dropped in Artillery Pro artillery.log( `Elapsed time: ${util.formatDuration(Date.now() - this.startTime)}` ); this.printReport(data.report(), this.opts); } else { this.printReport(data, this.opts); } artillery.log(); artillery.log(); }; ConsoleReporter.prototype.done = function done(data) { if (this.quiet) { return this; } if (this.startTime !== null) { artillery.log( `All VUs finished. Total time: ${util.formatDuration( Date.now() - this.startTime )}\n` ); } const txt = `Summary report @ ${formatTimestamp(new Date())}`; artillery.log(`${underline(txt)}\n${txt}\n${underline(txt)}\n`); // TODO: this is repeated in 'stats' handler data.summaries = data.summaries || {}; data.counters = data.counters || {}; if (typeof data.report === 'function') { // Compatibility fix with Artillery Pro which uses 1.x // API for emitting reports to console-reporter. // TODO: Remove when support for 1x is dropped in Artillery Pro this.printReport( data.report(), Object.assign({}, this.opts, { showScenarioCounts: true, printPeriod: false }) ); } else { this.printReport( data, Object.assign({}, this.opts, { showScenarioCounts: true, printPeriod: false }) ); } }; ConsoleReporter.prototype.printReport = function printReport(report, opts) { opts = opts || {}; if (opts.printPeriod !== false) { const timeWindowEnd = moment( new Date(Number(report.period) + 10 * 1000) ).format('HH:mm:ss(ZZ)'); if (typeof report.period !== 'undefined') { // FIXME: up to bound should be included in the report // Add underline const txt = `Metrics for period to: ${timeWindowEnd}`; artillery.log( underline(txt) + '\n' + txt + ' ' + chalk.gray( '(width: ' + (report.lastMetricAt - report.firstMetricAt) / 1000 + 's)' ) + '\n' + underline(txt) + '\n' ); // artillery.log(padded('time_window:', timeWindowEnd)); } else { artillery.log('Report @ %s', formatTimestamp(report.timestamp)); } } if (this.outputFormat === 'new') { report.rates = report.rates || {}; report.counters = report.counters || {}; report.summaries = report.summaries || {}; const sortedByLen = _( Object.keys(report.summaries) .concat(Object.keys(report.counters)) .concat(Object.keys(report.rates)) ) .sortBy([(x) => x.length]) .value(); if (sortedByLen.length === 0) { // No scenarios launched or completed, no requests made or completed etc. Nothing happened. artillery.log('No measurements recorded during this period'); return; } const sortedAlphabetically = sortedByLen.sort(); let result = []; for (const metricName of sortedAlphabetically) { if (shouldSuppressOutput(metricName, this.metricsToSuppress)) { continue; }; if (typeof report.counters?.[metricName] !== 'undefined') { result = result.concat(printCounters([metricName], report)); } if (typeof report.summaries?.[metricName] !== 'undefined') { result = result.concat(printSummaries([metricName], report)); } if (typeof report.rates?.[metricName] !== 'undefined') { result = result.concat(printRates([metricName], report)); } } artillery.log(result.join('\n')); } }; if (this.outputFormat === 'classic') { report = SSMS.legacyReport(report).report(); // TODO: Read new fields instead of the old ones artillery.log('Scenarios launched: %s', report.scenariosCreated); artillery.log('Scenarios completed: %s', report.scenariosCompleted); artillery.log('Requests completed: %s', report.requestsCompleted); artillery.log('Mean responses/sec: %s', report.rps.mean); artillery.log('Response time (msec):'); artillery.log(' min: %s', report.latency.min); artillery.log(' max: %s', report.latency.max); artillery.log(' median: %s', report.latency.median); artillery.log(' p95: %s', report.latency.p95); artillery.log(' p99: %s', report.latency.p99); if (this.reportScenarioLatency) { artillery.log('Scenario duration:'); artillery.log(' min: %s', report.scenarioDuration.min); artillery.log(' max: %s', report.scenarioDuration.max); artillery.log(' median: %s', report.scenarioDuration.median); artillery.log(' p95: %s', report.scenarioDuration.p95); artillery.log(' p99: %s', report.scenarioDuration.p99); } // We only want to show this for the aggregate report: if (opts.showScenarioCounts && report.scenarioCounts) { artillery.log('Scenario counts:'); _.each(report.scenarioCounts, (count, name) => { const percentage = Math.round((count / report.scenariosCreated) * 100 * 1000) / 1000; artillery.log(' %s: %s (%s%)', name, count, percentage); }); } if (_.keys(report.codes).length !== 0) { artillery.log('Codes:'); _.each(report.codes, (count, code) => { artillery.log(' %s: %s', code, count); }); } if (_.keys(report.errors).length !== 0) { artillery.log('Errors:'); _.each(report.errors, (count, code) => { artillery.log(' %s: %s', code, count); }); } if (_.size(report.summaries) > 0 || _.size(report.counters) > 0) { _.each(report.summaries, (r, n) => { if (excludeFromReporting(n)) return; artillery.log('%s:', n); artillery.log(' min: %s', r.min); artillery.log(' max: %s', r.max); artillery.log(' median: %s', r.median); artillery.log(' p95: %s', r.p95); artillery.log(' p99: %s', r.p99); }); } _.each(report.customStats, (r, n) => { artillery.log('%s:', n); artillery.log(' min: %s', r.min); artillery.log(' max: %s', r.max); artillery.log(' median: %s', r.median); artillery.log(' p95: %s', r.p95); artillery.log(' p99: %s', r.p99); }); _.each(report.counters, (value, name) => { // Only show user/custom metrics in this mode, but none of the internally generated ones: if (excludeFromReporting(name)) return; artillery.log('%s: %s', name, value); }); artillery.log(); } function isCollectionMetric(n) { const collectionMetrics = ['artillery.codes', 'errors']; return ( collectionMetrics.filter((m) => { return n.startsWith(m); }).length > 0 ); } if (this.outputFormat === 'table') { const t = new Table({ head: ['Metric', 'Value'] }); if (_.size(report.summaries) > 0 || _.size(report.counters) > 0) { _.sortBy(Object.keys(report.summaries)).forEach((n) => { const r = report.summaries[n]; const spaces = ' '.repeat(Math.min(8, n.length + 1)); t.push([`${n}`]); t.push([`${spaces}min`, r.min]); t.push([`${spaces}max`, r.max]); t.push([`${spaces}median`, r.median]); t.push([`${spaces}p95`, r.p95]); t.push([`${spaces}p99`, r.p99]); }); _.sortBy( Object.keys(report.counters).filter((name) => !isCollectionMetric(name)) ).forEach((name) => { const value = report.counters[name]; t.push([name, value]); }); } artillery.log(t.toString()); artillery.log(); } // TODO: Make smarter if date changes, ie. test runs over midnight function formatTimestamp(timestamp) { return moment(new Date(timestamp)).format('HH:mm:ss(ZZ)'); } function underline(text) { return '-'.repeat(text.length); } function excludeFromReporting(name) { return ( ['engine', 'core', 'artillery', 'errors', 'scenarios'].indexOf( name.split('.')[0] ) > -1 ); } function padded(str1, str2) { const defaultWidth = 79; // We need at least 50 const columnsAvailable = Math.max( process.stdout?.columns || defaultWidth, 50 ); // But no more than 79: const width = Math.min(columnsAvailable, defaultWidth); return util.padded(str1, str2, width); } function printRates(rates, report) { return rates.sort().map((name) => { return `${padded(`${name}:`, report.rates[name])}/sec`; }); } function printCounters(counters, report) { return counters.sort().map((name) => { const value = report.counters[name]; return padded(`${name}:`, value); }); } function printSummaries(summaries, report) { const result = []; for (const n of summaries) { const r = report.summaries[n]; result.push(`${n}:`); result.push(padded(' min:', r.min)); result.push(padded(' max:', r.max)); result.push(padded(' mean:', r.mean)); result.push(padded(' median:', r.median)); result.push(padded(' p95:', r.p95)); result.push(padded(' p99:', r.p99)); // TODO: Can work well if padded to look like a table: // result.push(padded(`${trimName(n)}:`, `min: ${r.min} | max: ${r.max} | p50: ${r.p50} | p95: ${r.p95} | p99: ${r.p99}`)); } return result; } function shouldSuppressOutput(currMetricName, suppressMetricsList) { if (!suppressMetricsList) { return; }; return suppressMetricsList.some((metric)=> currMetricName.includes(metric)); } ================================================ FILE: packages/artillery/lib/create-bom/built-in-plugins.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ module.exports = [ 'metrics-by-endpoint', 'ensure', 'publish-metrics', 'expect', 'apdex', 'slack' ]; ================================================ FILE: packages/artillery/lib/create-bom/create-bom.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // TODO: async-ify this const path = require('node:path'); const fs = require('node:fs'); const A = require('async'); const debug = require('debug')('bom'); const _ = require('lodash'); const Table = require('cli-table3'); const { getCustomJsDependencies } = require('../platform/aws-ecs/legacy/bom'); const { readScript, parseScript } = require('../util'); const BUILTIN_PLUGINS = require('./built-in-plugins'); // NOTE: Presumes ALL paths are absolute. async function createBOM(absoluteScriptPath, extraFiles, opts, callback) { A.waterfall( [ A.constant(absoluteScriptPath), readScript, parseScript, (scriptData, next) => { return next(null, { opts: { scriptData, absoluteScriptPath }, localFilePaths: [absoluteScriptPath], npmModules: [] }); }, getPlugins, getCustomEngines, getCustomJsDependencies, getVariableDataFiles, // getFileUploadPluginFiles, getExtraFiles // expandDirectories ], (err, context) => { if (err) { return callback(err, null); } context.localFilePaths = context.localFilePaths.concat(extraFiles); // TODO: Entries in localFilePaths may be directories // Handle case with only one entry, where the string itself // will be the common prefix, meaning that when we substring() on it later, we'll // get an empty string, ending up with a manifest like: // { files: // [ { orig: '/Users/h/tmp/artillery/hello.yaml', noPrefix: '' } ], // modules: [] } // let prefix = ''; if (context.localFilePaths.length === 1) { prefix = context.localFilePaths[0].substring( 0, context.localFilePaths[0].length - path.basename(context.localFilePaths[0]).length ); // This may still be an empty string if the script path is just 'hello.yml': prefix = prefix.length === 0 ? context.localFilePaths[0] : prefix; } else { prefix = commonPrefix(context.localFilePaths); } debug('prefix', prefix); // // include package.json / package-lock.json / yarn.lock // let packageDescriptionFiles = ['.npmrc']; if (opts.packageJsonPath) { packageDescriptionFiles.push(opts.packageJsonPath); } else { packageDescriptionFiles = packageDescriptionFiles.concat([ 'package.json', 'package-lock.json', 'yarn.lock' ]); } const dependencyFiles = packageDescriptionFiles.map((s) => path.join(prefix, s) ); debug(dependencyFiles); dependencyFiles.forEach((p) => { try { if (fs.statSync(p)) { context.localFilePaths.push(p); } } catch (_ignoredErr) {} }); const files = context.localFilePaths.map((p) => { return { orig: p, noPrefix: p.substring(prefix.length, p.length) }; }); const pkgPath = _.find(files, (f) => { return f.noPrefix === 'package.json'; }); if (pkgPath) { const pkg = JSON.parse(fs.readFileSync(pkgPath.orig, 'utf8')); const pkgDeps = [].concat( Object.keys(pkg.dependencies || {}), Object.keys(pkg.devDependencies || {}) ); context.pkgDeps = pkgDeps; context.npmModules = _.uniq(context.npmModules.concat(pkgDeps)).sort(); } else { context.pkgDeps = []; } return callback(null, { files: _.uniqWith(files, _.isEqual), modules: _.uniq(context.npmModules).filter( (m) => m !== 'artillery' && m !== 'playwright' && !m.startsWith('@playwright/') ), pkgDeps: context.pkgDeps }); } ); } function getPlugins(context, next) { const environmentPlugins = _.reduce( _.get(context, 'opts.scriptData.config.environments', {}), function getEnvironmentPlugins(acc, envSpec, _envName) { acc = acc.concat(Object.keys(envSpec.plugins || [])); return acc; }, [] ); const pluginNames = Object.keys( _.get(context, 'opts.scriptData.config.plugins', {}) ).concat(environmentPlugins); const pluginPackages = _.uniq( pluginNames .filter((p) => BUILTIN_PLUGINS.indexOf(p) === -1) .map((p) => `artillery-plugin-${p}`) ); debug(pluginPackages); context.npmModules = context.npmModules.concat(pluginPackages); return next(null, context); } function getCustomEngines(context, next) { // TODO: Environment-specific engines (see getPlugins()) const engineNames = _.uniq( Object.keys(_.get(context, 'opts.scriptData.config.engines', {})) ); const enginePackages = engineNames.map((x) => `artillery-engine-${x}`); context.npmModules = context.npmModules.concat(enginePackages); return next(null, context); } function getVariableDataFiles(context, next) { // NOTE: Presuming that the script has been run through the functions // that normalize the config.payload definition (presume it's an array). // Also assuming that context.opts.scriptData contains both the config and // the scenarios section. // Iterate over environments function resolvePayloadPaths(obj) { const result = []; if (obj.payload) { if (_.isArray(obj.payload)) { obj.payload.forEach((payloadSpec) => { result.push( path.resolve( path.dirname(context.opts.absoluteScriptPath), payloadSpec.path ) ); }); } else if (_.isObject(obj.payload)) { // NOTE: isObject returns true for arrays, so this branch must // come second. result.push( path.resolve( path.dirname(context.opts.absoluteScriptPath), obj.payload.path ) ); } } return result; } context.localFilePaths = context.localFilePaths.concat( resolvePayloadPaths(context.opts.scriptData.config) ); context.opts.scriptData.config.environments = context.opts.scriptData.config.environments || {}; Object.keys(context.opts.scriptData.config.environments).forEach( (envName) => { const envSpec = context.opts.scriptData.config.environments[envName]; context.localFilePaths = context.localFilePaths.concat( resolvePayloadPaths(envSpec) ); } ); return next(null, context); } function getExtraFiles(context, next) { if ( context.opts.scriptData.config?.includeFiles ) { const absPaths = _.map(context.opts.scriptData.config.includeFiles, (p) => { const includePath = path.resolve( path.dirname(context.opts.absoluteScriptPath), p ); debug('includeFile:', includePath); return includePath; }); context.localFilePaths = context.localFilePaths.concat(absPaths); return next(null, context); } else { return next(null, context); } } function commonPrefix(paths, separator) { if ( !paths || paths.length === 0 || paths.filter((s) => typeof s !== 'string').length > 0 ) { return ''; } if (paths.includes('/')) { return '/'; } const sep = separator ? separator : path.sep; const splitPaths = paths.map((p) => p.split(sep)); const shortestPath = splitPaths.reduce((a, b) => { return a.length < b.length ? a : b; }, splitPaths[0]); let furthestIndex = shortestPath.length; for (const p of splitPaths) { for (let i = 0; i < furthestIndex; i++) { if (p[i] !== shortestPath[i]) { furthestIndex = i; break; } } } const joined = shortestPath.slice(0, furthestIndex).join(sep); if (joined.length > 0) { // Check if joined path already ends with separator which // will happen when input is a root drive on Windows, e.g. "C:\" return joined.endsWith(sep) ? joined : joined + sep; } else { return ''; } } function prettyPrint(manifest) { const t = new Table({ head: ['Name', 'Type', 'Notes'] }); for (const f of manifest.files) { t.push([f.noPrefix, 'file']); } for (const m of manifest.modules) { t.push([ m, 'package', manifest.pkgDeps.indexOf(m) === -1 ? 'not in package.json' : '' ]); } artillery.log(t.toString()); artillery.log(); } module.exports = { createBOM, commonPrefix, prettyPrint }; ================================================ FILE: packages/artillery/lib/dispatcher.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const core = require('@artilleryio/int-core'); module.exports = core; ================================================ FILE: packages/artillery/lib/dist.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const assert = require('node:assert'); const L = require('lodash'); const isIdlePhase = require('@artilleryio/int-core').isIdlePhase; module.exports = divideWork; /** * * Create a number of scripts for workers from the script given to use by user. * * @param {Script} script * @param {number} numWorkers * @returns {Script[]} array of scripts distributed representing the work for each worker * * @todo: Distribute payload data to workers */ function divideWork(script, numWorkers) { const workerScripts = createWorkerScriptBases(numWorkers, script); for (const phase of script.config.phases) { // switching on phase type to determine how to distribute work switch (true) { case !!phase.rampTo: { handleRampToPhase(phase, numWorkers, workerScripts); break; } case !!phase.arrivalRate: { handleArrivalRatePhase(phase, numWorkers, workerScripts); break; } case !!phase.arrivalCount: { // arrivalCount is executed in the first worker // and replaced with a `pause` phase in the others handleArrivalCountPhase(workerScripts, phase, numWorkers); break; } case !!phase.pause: { // nothing to adjust here, pause is executed in all workers for (let i = 0; i < numWorkers; i++) { workerScripts[i].config.phases.push(L.cloneDeep(phase)); } break; } default: { console.log( 'Unknown phase spec definition, skipping.\n%j\n' + 'This should not happen', phase ); } } } // Filter out scripts which have only idle phases const result = workerScripts.filter( (workerScript) => !workerScript.config.phases.every(isIdlePhase) ); // Add worker and totalWorkers properties to phases const hasPayload = scriptHasPayload(script); for (let i = 0; i < result.length; i++) { for (const phase of result[i].config.phases) { phase.totalWorkers = result.length; phase.worker = i + 1; } // Distribute payload data to workers if (hasPayload) { for ( let payloadIdx = 0; payloadIdx < script.config.payload.length; payloadIdx++ ) { // If there are more workers than payload data, then we will repeat the payload data const scriptPayloadData = script.config.payload[payloadIdx].data; const idxToMatch = i % scriptPayloadData.length; result[i].config.payload[payloadIdx].data = scriptPayloadData.filter( (_, index) => index % result.length === idxToMatch ); } } } return result; } function scriptHasPayload(script) { return script.config.payload && script.config.payload.length > 0; } function handleArrivalCountPhase(workerScripts, phase, numWorkers) { workerScripts[0].config.phases.push(L.cloneDeep(phase)); for (let i = 1; i < numWorkers; i++) { workerScripts[i].config.phases.push({ name: phase.name, pause: phase.duration }); } } function handleArrivalRatePhase(phase, numWorkers, workerScripts) { const rates = distribute(phase.arrivalRate, numWorkers); const activeWorkers = rates.reduce( (acc, rate) => acc + (rate > 0 ? 1 : 0), 0 ); const maxVusers = phase.maxVusers ? distribute(phase.maxVusers, activeWorkers) : false; for (let i = 0; i < numWorkers; i++) { const newPhase = L.cloneDeep(phase); newPhase.arrivalRate = rates[i]; if (maxVusers) { newPhase.maxVusers = maxVusers[i]; } workerScripts[i].config.phases.push(newPhase); } } function handleRampToPhase(phase, numWorkers, workerScripts) { phase.arrivalRate = phase.arrivalRate || 0; const rate = phase.arrivalRate / numWorkers; const ramp = phase.rampTo / numWorkers; const activeWorkers = rate > 0 || ramp > 0 ? numWorkers : 0; const maxVusers = phase.maxVusers ? distribute(phase.maxVusers, activeWorkers) : false; for (let i = 0; i < numWorkers; i++) { const newPhase = L.cloneDeep(phase); newPhase.arrivalRate = rate; newPhase.rampTo = ramp; if (maxVusers) { newPhase.maxVusers = maxVusers[i]; } workerScripts[i].config.phases.push(newPhase); } } function createWorkerScriptBases(numWorkers, script) { const bases = []; for (let i = 0; i < numWorkers; i++) { const newScript = L.cloneDeep({ ...script, config: { ...script.config, phases: [], ...(scriptHasPayload(script) && { payload: script.config.payload.map((payload) => { return { ...payload, data: [] }; }) }) } }); // 'before' and 'after' hooks are executed in the main thread delete newScript.before; delete newScript.after; bases.push(newScript); } return bases; } function distribute(m, n) { m = Number(m); n = Number(n); const result = []; if (m < n) { for (let i = 0; i < n; i++) { result.push(i < m ? 1 : 0); } } else { const baseCount = Math.floor(m / n); let extraItems = m % n; for (let i = 0; i < n; i++) { result.push(baseCount); if (extraItems > 0) { result[i]++; extraItems--; } } } assert(m === sum(result), `${m} === ${sum(result)}`); return result; } function sum(a) { let result = 0; for (let i = 0; i < a.length; i++) { result += a[i]; } return result; } if (require.main === module) { console.log(distribute(1, 4)); console.log(distribute(1, 10)); console.log(distribute(4, 4)); console.log(distribute(87, 4)); console.log(distribute(50, 8)); console.log(distribute(39, 20)); console.log(distribute(20, 4)); console.log(distribute(19, 4)); console.log(distribute(20, 3)); console.log(distribute(61, 4)); console.log(distribute(121, 4)); console.log(distribute(32, 3)); console.log(distribute(700, 31)); console.log(distribute(700, 29)); } ================================================ FILE: packages/artillery/lib/index.js ================================================ const { getStash } = require('./stash'); module.exports = { getStash }; ================================================ FILE: packages/artillery/lib/launch-platform.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { SSMS } = require('@artilleryio/int-core').ssms; const { loadPlugins, loadPluginsConfig } = require('./load-plugins'); const EventEmitter = require('eventemitter3'); const debug = require('debug')('core'); const p = require('node:util').promisify; const _ = require('lodash'); const PlatformLocal = require('./platform/local'); const PlatformLambda = require('./platform/aws-lambda'); const PlatformAzureACI = require('./platform/az/aci'); async function createLauncher(script, payload, opts, launcherOpts) { launcherOpts = launcherOpts || { platform: 'local', mode: 'distribute' }; let l; try { l = new Launcher(script, payload, opts, launcherOpts); } catch (err) { console.log(err); return null; } return l; } class Launcher { constructor(script, payload, opts, launcherOpts) { this.script = script; this.payload = payload; this.opts = opts; this.exitedWorkersCount = 0; this.workerMessageBuffer = []; this.metricsByPeriod = {}; // individual intermediates by worker this.finalReportsByWorker = {}; this.events = new EventEmitter(); this.pluginEvents = new EventEmitter(); this.pluginEventsLegacy = new EventEmitter(); this.launcherOpts = launcherOpts; this.periodsReportedFor = []; if (launcherOpts.platform === 'local') { this.platform = new PlatformLocal(script, payload, opts, launcherOpts); } else if (launcherOpts.platform === 'aws:lambda') { this.platform = new PlatformLambda(script, payload, opts, launcherOpts); } else if (launcherOpts.platform === 'az:aci') { this.platform = new PlatformAzureACI(script, payload, opts, launcherOpts); } else { throw new Error(`Unknown platform: ${launcherOpts.platform}`); } this.phaseStartedEventsSeen = {}; this.phaseCompletedEventsSeen = {}; this.eventsByWorker = {}; } async initWorkerEvents(workerEvents) { workerEvents.on('workerError', (_workerId, message) => { const { id, error, level, aggregatable, logs } = message; if (level !== 'warn') { this.exitedWorkersCount++; } if (aggregatable) { this.workerMessageBuffer.push(message); } else { global.artillery.log(`[${id}]: ${error.message}`); if (logs) { global.artillery.log(logs); } } this.events.emit('workerError', message); }); workerEvents.on('phaseStarted', (_workerId, message) => { // Note - we send only the first event for a phase, not all of them if ( typeof this.phaseStartedEventsSeen[message.phase.index] === 'undefined' ) { this.phaseStartedEventsSeen[message.phase.index] = Date.now(); const fullPhase = { //get back original phase without any splitting for workers ...this.script.config.phases[message.phase.index], index: message.phase.index, id: message.phase.id, startTime: this.phaseStartedEventsSeen[message.phase.index] }; this.events.emit('phaseStarted', fullPhase); this.pluginEvents.emit('phaseStarted', fullPhase); this.pluginEventsLegacy.emit('phaseStarted', fullPhase); global.artillery.globalEvents.emit('phaseStarted', fullPhase); } }); workerEvents.on('phaseCompleted', (_workerId, message) => { if ( typeof this.phaseCompletedEventsSeen[message.phase.index] === 'undefined' ) { this.phaseCompletedEventsSeen[message.phase.index] = Date.now(); const fullPhase = { //get back original phase without any splitting for workers ...this.script.config.phases[message.phase.index], id: message.phase.id, index: message.phase.index, startTime: this.phaseStartedEventsSeen[message.phase.index], endTime: message.phase.endTime }; this.events.emit('phaseCompleted', fullPhase); this.pluginEvents.emit('phaseCompleted', fullPhase); this.pluginEventsLegacy.emit('phaseCompleted', fullPhase); global.artillery.globalEvents.emit('phaseCompleted', fullPhase); } }); // We are not going to receive stats events from workers // which have zero arrivals for a phase. (This can only happen // in "distribute" mode.) workerEvents.on('stats', (_workerId, message) => { const workerStats = SSMS.deserializeMetrics(message.stats); const period = workerStats.period; if (typeof this.metricsByPeriod[period] === 'undefined') { this.metricsByPeriod[period] = []; } // TODO: might want the full message here, with worker ID etc this.metricsByPeriod[period].push(workerStats); }); workerEvents.on('done', async (workerId, message) => { this.exitedWorkersCount++; this.finalReportsByWorker[workerId] = SSMS.deserializeMetrics( message.report ); }); workerEvents.on('log', async (_workerId, message) => { artillery.globalEvents.emit('log', ...message.args); }); workerEvents.on('setSuggestedExitCode', (_workerId, message) => { artillery.suggestedExitCode = message.code; }); } async initPlugins() { const plugins = await loadPlugins( this.script.config.plugins, this.script, this.opts ); // // init plugins // for (const [name, result] of Object.entries(plugins)) { if (result.isLoaded) { if (result.version === 3) { // TODO: load the plugin, subscribe to events // global.artillery.plugins[name] = result.plugin; } else { // global.artillery.log(`WARNING: Legacy plugin detected: ${name} // See https://artillery.io/docs/resources/core/v2.html for more details.`, // 'warn'); // NOTE: // We are giving v1 and v2 plugins a throw-away script // object because we only care about the plugin setting // up event handlers here. The plugins will be loaded // properly in individual workers where they will have the // opportunity to attach custom code, modify the script // object etc. // If we let a plugin access to the actual script object, // and it happens to attach code to it (with a custom // processor function for example) - spawning a worker // will fail. const dummyScript = JSON.parse(JSON.stringify(this.script)); dummyScript.config = { ...dummyScript.config, // Load additional plugins configuration from the environment plugins: loadPluginsConfig(this.script.config.plugins) }; if (result.version === 1) { result.plugin = new result.PluginExport( dummyScript.config, this.pluginEventsLegacy ); global.artillery.plugins.push(result); } else if (result.version === 2) { if (result.PluginExport.LEGACY_METRICS_FORMAT === false) { result.plugin = new result.PluginExport.Plugin( dummyScript, this.pluginEvents, this.opts ); } else { result.plugin = new result.PluginExport.Plugin( dummyScript, this.pluginEventsLegacy, this.opts ); } global.artillery.plugins.push(result); } else { // TODO: print warning } } } else { global.artillery.log(`WARNING: Could not load plugin: ${name}`, 'warn'); global.artillery.log(result.msg, 'warn'); // global.artillery.log(result.error, 'warn'); } } } async handleAllWorkersFinished() { const allWorkersDone = this.exitedWorkersCount === this.platform.getDesiredWorkerCount(); if (allWorkersDone) { clearInterval(this.i1); clearInterval(this.i2); // Flush messages from workers await this.flushWorkerMessages(0); await this.flushIntermediateMetrics(true); const pds = Object.keys(this.finalReportsByWorker).map( (k) => this.finalReportsByWorker[k] ); const statsByPeriod = Object.values(SSMS.mergeBuckets(pds)); const stats = SSMS.pack(statsByPeriod); stats.summaries = {}; for (const [name, value] of Object.entries(stats.histograms || {})) { const summary = SSMS.summarizeHistogram(value); stats.summaries[name] = summary; } clearInterval(this.workerExitWatcher); // Relay event to workers this.pluginEvents.emit('done', stats); global.artillery.globalEvents.emit('done', stats); this.pluginEventsLegacy.emit('done', SSMS.legacyReport(stats)); this.events.emit('done', stats); } } async flushWorkerMessages(maxAge = 9000) { // Collect messages older than maxAge msec and group by log message: const now = Date.now(); const okToPrint = this.workerMessageBuffer.filter( (m) => now - m.ts > maxAge ); this.workerMessageBuffer = this.workerMessageBuffer.filter( (m) => now - m.ts <= maxAge ); const readyMessages = okToPrint.reduce((acc, message) => { const { error } = message; // TODO: Take event type and level into account if (typeof acc[error.message] === 'undefined') { acc[error.message] = []; } acc[error.message].push(message); return acc; }, {}); for (const [_logMessage, messageObjects] of Object.entries(readyMessages)) { if (messageObjects[0].error) { global.artillery.log( `[${messageObjects[0].id}] ${messageObjects[0].error.message}`, messageObjects[0].level ); } else { // Expect a msg property: global.artillery.log( `[${messageObjects[0].id}] ${messageObjects[0].msg}`, messageObjects[0].level ); } } } async flushIntermediateMetrics(flushAll = false) { if (Object.keys(this.metricsByPeriod).length === 0) { debug('No metrics received yet'); return; } // We always look at the earliest period available so that reports come in chronological order const unreportedPeriods = Object.keys(this.metricsByPeriod) .filter((x) => this.periodsReportedFor.indexOf(x) === -1) .sort(); const earliestPeriodAvailable = unreportedPeriods[0]; // TODO: better name. One above is earliestNotAlreadyReported const earliest = Object.keys(this.metricsByPeriod).sort()[0]; if (this.periodsReportedFor.indexOf(earliest) > -1) { global.artillery.log( 'Warning: multiple batches of metrics for period', earliest, new Date(Number(earliest)) ); delete this.metricsByPeriod[earliest]; // FIXME: need to merge them in for the final report } // Dynamically adjust the duration we're willing to wait for. This matters on SQS where messages are received // in batches of 10 and more workers => need to wait longer. const MAX_WAIT_FOR_PERIOD_MS = (Math.ceil(this.platform.getDesiredWorkerCount() / 10) * 3 + 30) * 1000; debug({ now: Date.now(), count: this.platform.getDesiredWorkerCount(), earliestPeriodAvailable, earliest, MAX_WAIT_FOR_PERIOD_MS, numReports: this.metricsByPeriod[earliestPeriodAvailable]?.length, periodsReportedFor: this.periodsReportedFor, metricsByPeriod: Object.keys(this.metricsByPeriod) }); const allWorkersReportedForPeriod = this.metricsByPeriod[earliestPeriodAvailable]?.length === this.platform.getDesiredWorkerCount(); const waitedLongEnough = Date.now() - Number(earliestPeriodAvailable) > MAX_WAIT_FOR_PERIOD_MS; if (flushAll) { for (const period of unreportedPeriods) { this.emitIntermediatesForPeriod(period); } } else if ( typeof earliestPeriodAvailable !== 'undefined' && (allWorkersReportedForPeriod || waitedLongEnough) ) { this.emitIntermediatesForPeriod(earliestPeriodAvailable); // TODO: autoscaling. Handle workers that drop off or join, and update count } else { debug('Waiting for more workerStats before emitting stats event'); } } emitIntermediatesForPeriod(period) { debug( 'Report @', new Date(Number(period)), 'made up of items:', this.metricsByPeriod[String(period)].length ); // TODO: Track how many workers provided metrics in the metrics report // summarize histograms for console reporter: const merged = SSMS.mergeBuckets(this.metricsByPeriod[String(period)]); const stats = merged[String(period)]; stats.summaries = {}; for (const [name, value] of Object.entries(stats.histograms || {})) { const summary = SSMS.summarizeHistogram(value); stats.summaries[name] = summary; } delete this.metricsByPeriod[String(period)]; this.periodsReportedFor.push(period); this.pluginEvents.emit('stats', stats); global.artillery.globalEvents.emit('stats', stats); this.pluginEventsLegacy.emit('stats', SSMS.legacyReport(stats)); this.events.emit('stats', stats); } async run() { await this.initPlugins(); this.i1 = setInterval(async () => { await this.flushWorkerMessages(); }, 1 * 1000).unref(); this.i2 = setInterval(async () => { this.flushIntermediateMetrics(); }, 2 * 1000).unref(); this.workerExitWatcher = setInterval(async () => { await this.handleAllWorkersFinished(); }, 2 * 1000); await this.initWorkerEvents(this.platform.events); await this.platform.startJob(); debug('workers running'); } async shutdown() { await this.platform.shutdown(); // TODO: flush worker messages, and intermediate stats // Unload plugins // TODO: v3 plugins if (global.artillery?.plugins) { for (const o of global.artillery.plugins) { if (o.plugin.cleanup) { try { await p(o.plugin.cleanup.bind(o.plugin))(); debug('plugin unloaded:', o.name); } catch (cleanupErr) { global.artillery.log(cleanupErr, 'error'); } } } } } } module.exports = createLauncher; ================================================ FILE: packages/artillery/lib/load-plugins.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('core'); const path = require('node:path'); // Additional paths to load plugins can be set via ARTILLERY_PLUGIN_PATH // Additional plugin config mafy be set via ARTILLERY_PLUGINS (as JSON) // Version may be: v1, v2, v3 or any function loadPluginsConfig(pluginSpecs) { let additionalPlugins = {}; if (process.env.ARTILLERY_PLUGINS) { try { additionalPlugins = JSON.parse(process.env.ARTILLERY_PLUGINS); } catch (ignoreErr) { debug(ignoreErr); } } return Object.assign({}, pluginSpecs, additionalPlugins); } async function loadPlugins(pluginSpecs, testScript) { let requirePaths = ['']; // requirePaths = requirePaths.concat(pro.getPluginPath()); if (process.env.ARTILLERY_PLUGIN_PATH) { requirePaths = requirePaths.concat( process.env.ARTILLERY_PLUGIN_PATH.split(':') ); } pluginSpecs = loadPluginsConfig(pluginSpecs); const results = {}; for (const [name, config] of Object.entries(pluginSpecs)) { const result = await loadPlugin(name, config, requirePaths, testScript); results[name] = result; } return results; } async function loadPlugin(name, config, requirePaths, testScript) { // TODO: Take scope in directly - don't need the full script const pluginConfigScope = config.scope || testScript.config.pluginsScope; const pluginPrefix = pluginConfigScope ? pluginConfigScope : 'artillery-plugin-'; const requireString = pluginPrefix + name; let PluginExport, pluginErr, loadedFrom, version; for (const p of requirePaths) { debug('Looking for plugin in:', p); try { loadedFrom = path.join(p, requireString); PluginExport = require(loadedFrom); if (typeof PluginExport === 'function') { version = 1; } else if ( typeof PluginExport === 'object' && typeof PluginExport.Plugin === 'function' ) { version = 2; } // TODO: Add v3 } catch (err) { debug(err); pluginErr = err; } if (typeof PluginExport !== 'undefined') { break; } } if (!PluginExport) { let msg; if (!pluginErr) { msg = `WARNING: Could not initialize plugin: ${name}`; } else { if (pluginErr.code === 'MODULE_NOT_FOUND') { msg = `WARNING: Plugin ${name} specified but module ${requireString} could not be found (${pluginErr.code})`; } else { msg = `WARNING: Could not initialize plugin: ${name} (${pluginErr.message})`; } } return { name, isLoaded: false, isInitialized: false, msg: msg, error: pluginErr }; } else { debug('Plugin %s loaded from %s', name, requireString); return { name, isLoaded: true, isInitialized: false, PluginExport, loadedFrom, version }; } } module.exports = { loadPlugins, loadPlugin, loadPluginsConfig }; ================================================ FILE: packages/artillery/lib/platform/aws/aws-cloudwatch.js ================================================ const { CloudWatchLogsClient, PutRetentionPolicyCommand } = require('@aws-sdk/client-cloudwatch-logs'); const debug = require('debug')('artillery:aws-cloudwatch'); const allowedRetentionDays = [ 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653 ]; async function _putCloudwatchRetentionPolicy( logGroupName, retentionInDays, region ) { const cloudwatchlogs = new CloudWatchLogsClient({ apiVersion: '2014-11-06', region }); const putRetentionPolicyParams = { logGroupName, retentionInDays }; return cloudwatchlogs.send( new PutRetentionPolicyCommand(putRetentionPolicyParams) ); } function setCloudwatchRetention( logGroupName, retentionInDays, region, options = { maxRetries: 5, waitPerRetry: 1000 } ) { if (!allowedRetentionDays.includes(retentionInDays)) { console.log( `WARNING: Skipping setting CloudWatch retention, as invalid value specified: ${retentionInDays}. Allowed values are: ${allowedRetentionDays.join( ', ' )}` ); return; } const interval = setInterval( async (opts) => { debug( `Trying to set CloudWatch Log group ${logGroupName} retention policy to ${retentionInDays} days` ); opts.incr = (opts.incr || 0) + 1; try { const _res = await _putCloudwatchRetentionPolicy( logGroupName, retentionInDays, region ); debug( `Successfully set CloudWatch Logs retention policy to ${retentionInDays} days` ); clearInterval(interval); } catch (error) { const resumeTestMessage = 'The test will continue without setting the retention policy.'; if (error?.code === 'AccessDeniedException') { console.log(`\n${error.message}`); console.log( '\nWARNING: Missing logs:PutRetentionPolicy permission to set CloudWatch retention policy. Please ensure the IAM role has the necessary permissions:\nhttps://docs.art/fargate#iam-permissions' ); console.log(`${resumeTestMessage}\n`); clearInterval(interval); return; } if (error?.code !== 'ResourceNotFoundException') { console.log(`\n${error.message}`); console.log( '\nWARNING: Unexpected error setting CloudWatch retention policy\n' ); console.log(`${resumeTestMessage}\n`); clearInterval(interval); return; } if (opts.incr >= opts.maxRetries) { console.log(`\n${error.message}`); console.log( `\nWARNING: Cannot find log group ${logGroupName}\nMax retries exceeded setting CloudWatch retention policy:` ); console.log(`${resumeTestMessage}\n`); clearInterval(interval); return; } } }, options.waitPerRetry, options ); return interval; } module.exports = { setCloudwatchRetention }; ================================================ FILE: packages/artillery/lib/platform/aws/aws-create-sqs-queue.js ================================================ const { SQSClient, CreateQueueCommand, ListQueuesCommand } = require('@aws-sdk/client-sqs'); const debug = require('debug')('artillery:aws-create-sqs-queue'); const sleep = require('../../util/sleep'); // TODO: Add timestamp to SQS queue name for automatic GC async function createSQSQueue(region, queueName) { const sqs = new SQSClient({ region }); const params = { QueueName: queueName, Attributes: { FifoQueue: 'true', ContentBasedDeduplication: 'false', MessageRetentionPeriod: '1800', VisibilityTimeout: '600' } }; const result = await sqs.send(new CreateQueueCommand(params)); const sqsQueueUrl = result.QueueUrl; // Wait for the queue to be available: let waited = 0; let ok = false; while (waited < 120 * 1000) { try { const results = await sqs.send( new ListQueuesCommand({ QueueNamePrefix: queueName }) ); if (results.QueueUrls && results.QueueUrls.length === 1) { debug('SQS queue created:', queueName); ok = true; break; } else { await sleep(10 * 1000); waited += 10 * 1000; } } catch (_err) { await sleep(10 * 1000); waited += 10 * 1000; } } if (!ok) { debug('Time out waiting for SQS queue:', queueName); throw new Error('SQS queue could not be created'); } return sqsQueueUrl; } module.exports = createSQSQueue; ================================================ FILE: packages/artillery/lib/platform/aws/aws-ensure-s3-bucket-exists.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('util:aws:ensureS3BucketExists'); const { S3Client, PutBucketLifecycleConfigurationCommand, CreateBucketCommand, NoSuchBucket } = require('@aws-sdk/client-s3'); const getAWSAccountId = require('./aws-get-account-id'); const createS3Client = require('../aws-ecs/legacy/create-s3-client'); const { S3_BUCKET_NAME_PREFIX } = require('./constants'); const { getBucketRegion } = require('./aws-get-bucket-region'); const setBucketLifecyclePolicy = async ( bucketName, lifecycleConfigurationRules, region ) => { const s3 = createS3Client({ region }); const params = { Bucket: bucketName, LifecycleConfiguration: { Rules: lifecycleConfigurationRules } }; try { await s3.send(new PutBucketLifecycleConfigurationCommand(params)); } catch (err) { debug('Error setting lifecycle policy'); debug(err); } }; // Create an S3 bucket in the given region if it doesn't already exist. // By default, the bucket will be created without specifying a specific region. // Sometimes we need to use region-specific buckets, e.g. when // creating Lambda functions from a zip file in S3 the region of the // Lambda and the region of the S3 bucket must match. module.exports = async function ensureS3BucketExists( region, lifecycleConfigurationRules = [], withRegionSpecificName = false ) { const accountId = await getAWSAccountId(); let bucketName = `${S3_BUCKET_NAME_PREFIX}-${accountId}`; if (withRegionSpecificName) { bucketName = `${S3_BUCKET_NAME_PREFIX}-${accountId}-${region}`; } const s3 = new S3Client({ region }); let location; try { location = await getBucketRegion(bucketName); } catch (err) { if (err instanceof NoSuchBucket) { await s3.send(new CreateBucketCommand({ Bucket: bucketName })); } else { throw err; } } if (lifecycleConfigurationRules.length > 0) { await setBucketLifecyclePolicy( bucketName, lifecycleConfigurationRules, location ); } debug(bucketName); return bucketName; }; ================================================ FILE: packages/artillery/lib/platform/aws/aws-get-account-id.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('util:aws:getAccountId'); const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts'); module.exports = async function getAccountId(stsOpts = {}) { if (!stsOpts.region) { stsOpts.region = global.artillery.awsRegion || 'us-east-1'; } if (process.env.ARTILLERY_STS_OPTS) { stsOpts = Object.assign( stsOpts, JSON.parse(process.env.ARTILLERY_STS_OPTS) ); } const sts = new STSClient(stsOpts); const result = await sts.send(new GetCallerIdentityCommand({})); const awsAccountId = result.Account; debug(awsAccountId); return awsAccountId; }; ================================================ FILE: packages/artillery/lib/platform/aws/aws-get-bucket-region.js ================================================ const { S3Client, GetBucketLocationCommand } = require('@aws-sdk/client-s3'); async function getBucketRegion(bucketName) { const c = new S3Client({ region: global.artillery.awsRegion || 'us-east-1' }); const command = new GetBucketLocationCommand({ Bucket: bucketName }); const response = await c.send(command); // Buckets is us-east-1 have a LocationConstraint of null const location = response.LocationConstraint || 'us-east-1'; return location; } module.exports = { getBucketRegion }; ================================================ FILE: packages/artillery/lib/platform/aws/aws-get-credentials.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('util:aws:getSSOCredentials'); const { fromSSO } = require('@aws-sdk/credential-providers'); module.exports = getSSOCredentials; // If SSO is in use and we can acquire fresh credentials, return [true, credentials object] // If SSO is in use, but the session is stale, we return [true, {}] // If SSO is not in use we return [false, null] async function getSSOCredentials() { debug('Trying AWS SSO'); try { const credentials = await fromSSO()(); return [true, credentials]; } catch (err) { debug(err); if (/SSO.+expired/.test(err.message)) { return [true, null]; } else { return [false, null]; } } } ================================================ FILE: packages/artillery/lib/platform/aws/aws-get-default-region.js ================================================ const { loadConfig } = require('@smithy/node-config-provider'); const { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } = require('@smithy/config-resolver'); const debug = require('debug')('util:aws:get-default-region'); let defaultRegionAlreadyChecked = false; let currentDefaultRegion = null; module.exports = async function getDefaultRegion() { if (!defaultRegionAlreadyChecked) { try { currentDefaultRegion = await loadConfig( NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS )(); } catch (err) { debug('default region check:', err); } finally { defaultRegionAlreadyChecked = true; } } return currentDefaultRegion; }; ================================================ FILE: packages/artillery/lib/platform/aws/aws-whoami.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts'); module.exports = async function whoami() { const sts = new STSClient(); try { const response = await sts.send(new GetCallerIdentityCommand({})); return response; } catch (stsErr) { return stsErr; } }; ================================================ FILE: packages/artillery/lib/platform/aws/constants.js ================================================ module.exports = { SQS_QUEUES_NAME_PREFIX: 'artilleryio_test_metrics', S3_BUCKET_NAME_PREFIX: 'artilleryio-test-data', ECS_WORKER_ROLE_NAME: 'artilleryio-ecs-worker-role', ARTILLERY_CLUSTER_NAME: 'artilleryio-cluster' }; ================================================ FILE: packages/artillery/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml ================================================ AWSTemplateFormatVersion: "2010-09-09" Description: "Template to create an IAM Role with an attached policy that provides all necessary permissions for Artillery.io to run distributed tests on AWS Fargate. By default the IAM role is configured to trust your AWS account, meaning it will allow any IAM User, Role or service from your account to assume it. You can restrict the role to allow only by a specific IAM user or role to assume it by filling out the appropriate parameter value below." Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "Restrict to specific IAM User (optional)" Parameters: - User - Label: default: "Restrict to specific IAM Role (optional)" Parameters: - Role ParameterLabels: User: default: "IAM user name or ARN" Role: default: "IAM role name or ARN" Parameters: User: Type: String Default: "" Description: Use when you want to allow the created role to be assumed only by a specific IAM user (by default any user, role or service from your account will be allowed to assume it). Provide the user name or ARN. Role: Type: String Default: "" Description: Use when you want to allow the created role to be assumed only by a specific IAM role (by default any user, role or service from your account will be allowed to assume it). Provide the role name or ARN. Conditions: ShouldTrustAccount: !And - !Equals [!Ref User, ""] - !Equals [!Ref Role, ""] ShouldTrustUser: !Not [!Equals [!Ref User, ""]] IsUserArn: !Equals [!Select [0, !Split [":", !Ref User]], "arn"] ShouldTrustRole: !Not [!Equals [!Ref Role, ""]] IsRoleArn: !Equals [!Select [0, !Split [":", !Ref Role]], "arn"] Resources: ArtilleryDistributedTestingFargateRole: Type: "AWS::IAM::Role" Properties: RoleName: "ArtilleryDistributedTestingFargateRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: AWS: [ !If [ShouldTrustAccount, !Ref "AWS::AccountId", !Ref "AWS::NoValue"], !If [ShouldTrustUser, !If [IsUserArn, !Ref User, !Sub "arn:aws:iam::${AWS::AccountId}:user/${User}"], !Ref "AWS::NoValue"], !If [ShouldTrustRole, !If [IsRoleArn, !Ref Role, !Sub "arn:aws:iam::${AWS::AccountId}:role/${Role}"], !Ref "AWS::NoValue"] ] Action: [ "sts:AssumeRole" ] Path: "/" Policies: - PolicyName: "ArtilleryDistributedTestingFargatePolicy" PolicyDocument: Version: "2012-10-17" Statement: - Sid: "CreateOrGetECSRole" Effect: "Allow" Action: - "iam:CreateRole" - "iam:GetRole" - "iam:AttachRolePolicy" - "iam:PassRole" Resource: Fn::Sub: "arn:aws:iam::${AWS::AccountId}:role/artilleryio-ecs-worker-role" - Sid: "CreateECSPolicy" Effect: "Allow" Action: - "iam:CreatePolicy" Resource: Fn::Sub: "arn:aws:iam::${AWS::AccountId}:policy/artilleryio-ecs-worker-policy" - Effect: "Allow" Action: - "iam:CreateServiceLinkedRole" Resource: - "arn:aws:iam::*:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS*" Condition: StringLike: iam:AWSServiceName: "ecs.amazonaws.com" - Effect: "Allow" Action: - "iam:PassRole" Resource: - Fn::Sub: "arn:aws:iam::${AWS::AccountId}:role/artilleryio-ecs-worker-role" - Sid: "SQSPermissions" Effect: "Allow" Action: - "sqs:*" Resource: Fn::Sub: "arn:aws:sqs:*:${AWS::AccountId}:artilleryio*" - Sid: "SQSListQueues" Effect: "Allow" Action: - "sqs:ListQueues" Resource: "*" - Sid: "ECSPermissionsGeneral" Effect: "Allow" Action: - "ecs:ListClusters" - "ecs:CreateCluster" - "ecs:RegisterTaskDefinition" - "ecs:DeregisterTaskDefinition" Resource: "*" - Sid: "ECSPermissionsScopedToCluster" Effect: "Allow" Action: - "ecs:DescribeClusters" - "ecs:ListContainerInstances" Resource: Fn::Sub: "arn:aws:ecs:*:${AWS::AccountId}:cluster/*" - Sid: "ECSPermissionsScopedWithCondition" Effect: "Allow" Action: - "ecs:SubmitTaskStateChange" - "ecs:DescribeTasks" - "ecs:ListTasks" - "ecs:ListTaskDefinitions" - "ecs:DescribeTaskDefinition" - "ecs:StartTask" - "ecs:StopTask" - "ecs:RunTask" Condition: ArnEquals: ecs:cluster: Fn::Sub: "arn:aws:ecs:*:${AWS::AccountId}:cluster/*" Resource: "*" - Sid: "S3Permissions" Effect: "Allow" Action: - "s3:CreateBucket" - "s3:DeleteObject" - "s3:GetObject" - "s3:GetObjectAcl" - "s3:GetObjectTagging" - "s3:GetObjectVersion" - "s3:PutObject" - "s3:PutObjectAcl" - "s3:ListBucket" - "s3:GetBucketLocation" - "s3:GetBucketLogging" - "s3:GetBucketPolicy" - "s3:GetBucketTagging" - "s3:PutBucketPolicy" - "s3:PutBucketTagging" - "s3:PutMetricsConfiguration" - "s3:GetLifecycleConfiguration" - "s3:PutLifecycleConfiguration" Resource: - "arn:aws:s3:::artilleryio-test-data-*" - "arn:aws:s3:::artilleryio-test-data-*/*" - Sid: "LogsPermissions" Effect: "Allow" Action: - "logs:PutRetentionPolicy" Resource: - Fn::Sub: "arn:aws:logs:*:${AWS::AccountId}:log-group:artilleryio-log-group/*" - Effect: "Allow" Action: - "secretsmanager:GetSecretValue" Resource: - Fn::Sub: "arn:aws:secretsmanager:*:${AWS::AccountId}:secret:artilleryio/*" - Effect: "Allow" Action: - "ssm:PutParameter" - "ssm:GetParameter" - "ssm:GetParameters" - "ssm:DeleteParameter" - "ssm:DescribeParameters" - "ssm:GetParametersByPath" Resource: - Fn::Sub: "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:us-east-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:us-west-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:us-west-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ca-central-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-west-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-west-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-west-3:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-central-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-north-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-south-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-east-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-northeast-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-northeast-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-southeast-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-southeast-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:me-south-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:sa-east-1:${AWS::AccountId}:parameter/artilleryio/*" - Effect: "Allow" Action: - "ec2:DescribeRouteTables" - "ec2:DescribeVpcs" - "ec2:DescribeSubnets" Resource: "*" Outputs: RoleArn: Description: "ARN of the created IAM Role" Value: Fn::GetAtt: - "ArtilleryDistributedTestingFargateRole" - "Arn" ================================================ FILE: packages/artillery/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml ================================================ AWSTemplateFormatVersion: "2010-09-09" Description: Template to create an IAM Role with an attached policy that provides all necessary permissions for Artillery.io to run distributed tests on AWS Lambda. By default the IAM role is configured to trust your AWS account, meaning it will allow any AWS principal (e.g. IAM User, IAM Role) to assume it. You can restrict the role to allow only by a specific IAM user or role to assume it by filling out the appropriate parameter value below. Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "Restrict to specific IAM User (optional)" Parameters: - User - Label: default: "Restrict to specific IAM Role (optional)" Parameters: - Role ParameterLabels: User: default: "IAM user name or ARN" Role: default: "IAM role name or ARN" Parameters: User: Type: String Default: "" Description: Use when you want to allow the created role to be assumed only by a specific IAM user (by default any user, role or service from your account will be allowed to assume it). Provide the user name or ARN. Role: Type: String Default: "" Description: Use when you want to allow the created role to be assumed only by a specific IAM role (by default any user, role or service from your account will be allowed to assume it). Provide the role name or ARN. Conditions: ShouldTrustAccount: !And - !Equals [!Ref User, ""] - !Equals [!Ref Role, ""] ShouldTrustUser: !Not [!Equals [!Ref User, ""]] IsUserArn: !Equals [!Select [0, !Split [":", !Ref User]], "arn"] ShouldTrustRole: !Not [!Equals [!Ref Role, ""]] IsRoleArn: !Equals [!Select [0, !Split [":", !Ref Role]], "arn"] Resources: ArtilleryDistributedTestingLambdaRole: Type: "AWS::IAM::Role" Properties: RoleName: "ArtilleryDistributedTestingLambdaRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: AWS: [ !If [ShouldTrustAccount, !Ref "AWS::AccountId", !Ref "AWS::NoValue"], !If [ShouldTrustUser, !If [IsUserArn, !Ref User, !Sub "arn:aws:iam::${AWS::AccountId}:user/${User}"], !Ref "AWS::NoValue"], !If [ShouldTrustRole, !If [IsRoleArn, !Ref Role, !Sub "arn:aws:iam::${AWS::AccountId}:role/${Role}"], !Ref "AWS::NoValue"] ] Action: ["sts:AssumeRole"] Path: "/" Policies: - PolicyName: ArtilleryDistributedTestingLambdaPolicy PolicyDocument: Version: "2012-10-17" Statement: - Sid: CreateOrGetLambdaRole Effect: Allow Action: - iam:CreateRole - iam:GetRole - iam:PassRole - iam:AttachRolePolicy Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/artilleryio-default-lambda-role-*" - Sid: CreateLambdaPolicy Effect: Allow Action: - iam:CreatePolicy Resource: !Sub "arn:aws:iam::${AWS::AccountId}:policy/artilleryio-lambda-policy-*" - Sid: SQSPermissions Effect: Allow Action: - sqs:* Resource: !Sub "arn:aws:sqs:*:${AWS::AccountId}:artilleryio*" - Sid: SQSListQueues Effect: Allow Action: - sqs:ListQueues Resource: "*" - Sid: LambdaPermissions Effect: Allow Action: - lambda:InvokeFunction - lambda:CreateFunction - lambda:DeleteFunction - lambda:GetFunctionConfiguration Resource: !Sub "arn:aws:lambda:*:${AWS::AccountId}:function:artilleryio-*" - Sid: EcrPullImagePermissions Effect: Allow Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage Resource: "arn:aws:ecr:*:248481025674:repository/artillery-worker" - Sid: S3Permissions Effect: Allow Action: - s3:CreateBucket - s3:DeleteObject - s3:GetObject - s3:PutObject - s3:ListBucket - s3:GetLifecycleConfiguration - s3:PutLifecycleConfiguration Resource: - !Sub "arn:aws:s3:::artilleryio-test-data-*" - !Sub "arn:aws:s3:::artilleryio-test-data-*/*" Outputs: RoleArn: Description: ARN of the IAM Role for Artillery.io Lambda functions Value: !GetAtt ArtilleryDistributedTestingLambdaRole.Arn ================================================ FILE: packages/artillery/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml ================================================ AWSTemplateFormatVersion: '2010-09-09' Description: Creates an ArtilleryGitHubOIDCForFargateRole IAM role with permissions needed to run Artillery Fargate tests from a specified GitHub repository. An OIDC identity provider for Github will also be created if it is not already present in the account. Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "GitHub" Parameters: - GitHubRepository - GitHubBranch - Label: default: "AWS IAM" Parameters: - GitHubOIDCProviderExists ParameterLabels: GitHubRepository: default: "GitHub repository" GitHubBranch: default: "GitHub branch" GitHubOIDCProviderExists: default: "GitHub OIDC identity provider already created for the account?" Parameters: GitHubRepository: Type: String Default: "" Description: The GitHub repository (orgname/reponame) to be allowed to assume the created IAM role using OIDC (e.g. "artilleryio/artillery"). GitHubBranch: Type: String Default: "*" Description: (Optional) Use when you want to allow only a specific branch within the specified Github repository to assume this IAM role using OIDC (e.g. "main"). If not set, defaults to "*" (all branches). GitHubOIDCProviderExists: Type: String Default: 'No' AllowedValues: - 'Yes' - 'No' Description: This will let CloudFormation know whether it needs to create the provider. (If it exists, can be found at Services -> IAM -> Identity providers as 'token.actions.githubusercontent.com'). Conditions: IsGHRepoSet: !Not [!Equals [!Ref GitHubRepository, ""]] CreateOIDCProvider: !Equals [!Ref GitHubOIDCProviderExists, "No"] Resources: GitHubOIDCProvider: Type: AWS::IAM::OIDCProvider Condition: CreateOIDCProvider Properties: Url: "https://token.actions.githubusercontent.com" ClientIdList: - "sts.amazonaws.com" ThumbprintList: - "6938fd4d98bab03faadb97b34396831e3780ee11" ArtilleryGitHubOIDCForFargateRole: Type: "AWS::IAM::Role" Properties: RoleName: "ArtilleryGitHubOIDCForFargateRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Federated: Fn::If: - CreateOIDCProvider - !Ref GitHubOIDCProvider - !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" Action: "sts:AssumeRoleWithWebIdentity" Condition: { StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, StringLike: { "token.actions.githubusercontent.com:sub": !Sub "repo:${GitHubRepository}:${GitHubBranch}" } } Path: "/" Policies: - PolicyName: "ArtilleryGitHubOIDCForFargatePolicy" PolicyDocument: Version: "2012-10-17" Statement: - Sid: "CreateOrGetECSRole" Effect: "Allow" Action: - "iam:CreateRole" - "iam:GetRole" - "iam:AttachRolePolicy" - "iam:PassRole" Resource: Fn::Sub: "arn:aws:iam::${AWS::AccountId}:role/artilleryio-ecs-worker-role" - Sid: "CreateECSPolicy" Effect: "Allow" Action: - "iam:CreatePolicy" Resource: Fn::Sub: "arn:aws:iam::${AWS::AccountId}:policy/artilleryio-ecs-worker-policy" - Effect: "Allow" Action: - "iam:CreateServiceLinkedRole" Resource: - "arn:aws:iam::*:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS*" Condition: StringLike: iam:AWSServiceName: "ecs.amazonaws.com" - Effect: "Allow" Action: - "iam:PassRole" Resource: - Fn::Sub: "arn:aws:iam::${AWS::AccountId}:role/artilleryio-ecs-worker-role" - Sid: "SQSPermissions" Effect: "Allow" Action: - "sqs:*" Resource: Fn::Sub: "arn:aws:sqs:*:${AWS::AccountId}:artilleryio*" - Sid: "SQSListQueues" Effect: "Allow" Action: - "sqs:ListQueues" Resource: "*" - Sid: "ECSPermissionsGeneral" Effect: "Allow" Action: - "ecs:ListClusters" - "ecs:CreateCluster" - "ecs:RegisterTaskDefinition" - "ecs:DeregisterTaskDefinition" Resource: "*" - Sid: "ECSPermissionsScopedToCluster" Effect: "Allow" Action: - "ecs:DescribeClusters" - "ecs:ListContainerInstances" Resource: Fn::Sub: "arn:aws:ecs:*:${AWS::AccountId}:cluster/*" - Sid: "ECSPermissionsScopedWithCondition" Effect: "Allow" Action: - "ecs:SubmitTaskStateChange" - "ecs:DescribeTasks" - "ecs:ListTasks" - "ecs:ListTaskDefinitions" - "ecs:DescribeTaskDefinition" - "ecs:StartTask" - "ecs:StopTask" - "ecs:RunTask" Condition: ArnEquals: ecs:cluster: Fn::Sub: "arn:aws:ecs:*:${AWS::AccountId}:cluster/*" Resource: "*" - Sid: "S3Permissions" Effect: "Allow" Action: - "s3:CreateBucket" - "s3:DeleteObject" - "s3:GetObject" - "s3:GetObjectAcl" - "s3:GetObjectTagging" - "s3:GetObjectVersion" - "s3:PutObject" - "s3:PutObjectAcl" - "s3:ListBucket" - "s3:GetBucketLocation" - "s3:GetBucketLogging" - "s3:GetBucketPolicy" - "s3:GetBucketTagging" - "s3:PutBucketPolicy" - "s3:PutBucketTagging" - "s3:PutMetricsConfiguration" - "s3:GetLifecycleConfiguration" - "s3:PutLifecycleConfiguration" Resource: - "arn:aws:s3:::artilleryio-test-data-*" - "arn:aws:s3:::artilleryio-test-data-*/*" - Sid: "LogsPermissions" Effect: "Allow" Action: - "logs:PutRetentionPolicy" Resource: - Fn::Sub: "arn:aws:logs:*:${AWS::AccountId}:log-group:artilleryio-log-group/*" - Effect: "Allow" Action: - "secretsmanager:GetSecretValue" Resource: - Fn::Sub: "arn:aws:secretsmanager:*:${AWS::AccountId}:secret:artilleryio/*" - Effect: "Allow" Action: - "ssm:PutParameter" - "ssm:GetParameter" - "ssm:GetParameters" - "ssm:DeleteParameter" - "ssm:DescribeParameters" - "ssm:GetParametersByPath" Resource: - Fn::Sub: "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:us-east-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:us-west-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:us-west-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ca-central-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-west-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-west-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-west-3:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-central-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:eu-north-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-south-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-east-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-northeast-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-northeast-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-southeast-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:ap-southeast-2:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:me-south-1:${AWS::AccountId}:parameter/artilleryio/*" - Fn::Sub: "arn:aws:ssm:sa-east-1:${AWS::AccountId}:parameter/artilleryio/*" - Effect: "Allow" Action: - "ec2:DescribeRouteTables" - "ec2:DescribeVpcs" - "ec2:DescribeSubnets" Resource: "*" Outputs: RoleArn: Description: "ARN of the created IAM Role" Value: Fn::GetAtt: - "ArtilleryGitHubOIDCForFargateRole" - "Arn" OIDCProviderArn: Condition: CreateOIDCProvider Description: "ARN of the newly created OIDC provider" Value: !Ref GitHubOIDCProvider ================================================ FILE: packages/artillery/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml ================================================ AWSTemplateFormatVersion: '2010-09-09' Description: Creates an ArtilleryGitHubOIDCForLambdaRole IAM role with permissions needed to run Artillery Lambda tests from a specified GitHub repository. An OIDC identity provider for Github will also be created if it is not already present in the account. Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "GitHub" Parameters: - GitHubRepository - GitHubBranch - Label: default: "AWS IAM" Parameters: - GitHubOIDCProviderExists ParameterLabels: GitHubRepository: default: "GitHub repository" GitHubBranch: default: "GitHub branch" GitHubOIDCProviderExists: default: "GitHub OIDC identity provider already created for the account?" Parameters: GitHubRepository: Type: String Default: "" Description: The GitHub repository (orgname/reponame) to be allowed to assume the created IAM role using OIDC (e.g. "artilleryio/artillery"). GitHubBranch: Type: String Default: "*" Description: (Optional) Use when you want to allow only a specific branch within the specified Github repository to assume this IAM role using OIDC (e.g. "main"). If not set, defaults to "*" (all branches). GitHubOIDCProviderExists: Type: String Default: 'No' AllowedValues: - 'Yes' - 'No' Description: This will let CloudFormation know whether it needs to create the provider. (If it exists, can be found at Services -> IAM -> Identity providers as 'token.actions.githubusercontent.com'). Conditions: IsGHRepoSet: !Not [!Equals [!Ref GitHubRepository, ""]] CreateOIDCProvider: !Equals [!Ref GitHubOIDCProviderExists, "No"] Resources: GitHubOIDCProvider: Type: AWS::IAM::OIDCProvider Condition: CreateOIDCProvider Properties: Url: "https://token.actions.githubusercontent.com" ClientIdList: - "sts.amazonaws.com" ThumbprintList: - "6938fd4d98bab03faadb97b34396831e3780ee11" ArtilleryGitHubOIDCForLambdaRole: Type: "AWS::IAM::Role" Properties: RoleName: "ArtilleryGitHubOIDCForLambdaRole" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Federated: Fn::If: - CreateOIDCProvider - !Ref GitHubOIDCProvider - !Ref GitHubOIDCProviderArn Action: "sts:AssumeRoleWithWebIdentity" Condition: { StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" }, StringLike: { "token.actions.githubusercontent.com:sub": !Sub "repo:${GitHubRepository}:${GitHubBranch}" } } Path: "/" Policies: - PolicyName: ArtilleryDistributedTestingLambdaPolicy PolicyDocument: Version: "2012-10-17" Statement: - Sid: CreateOrGetLambdaRole Effect: Allow Action: - iam:CreateRole - iam:GetRole - iam:PassRole - iam:AttachRolePolicy Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/artilleryio-default-lambda-role-*" - Sid: CreateLambdaPolicy Effect: Allow Action: - iam:CreatePolicy Resource: !Sub "arn:aws:iam::${AWS::AccountId}:policy/artilleryio-lambda-policy-*" - Sid: SQSPermissions Effect: Allow Action: - sqs:* Resource: !Sub "arn:aws:sqs:*:${AWS::AccountId}:artilleryio*" - Sid: SQSListQueues Effect: Allow Action: - sqs:ListQueues Resource: "*" - Sid: LambdaPermissions Effect: Allow Action: - lambda:InvokeFunction - lambda:CreateFunction - lambda:DeleteFunction - lambda:GetFunctionConfiguration Resource: !Sub "arn:aws:lambda:*:${AWS::AccountId}:function:artilleryio-*" - Sid: EcrPullImagePermissions Effect: Allow Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage Resource: "arn:aws:ecr:*:248481025674:repository/artillery-worker" - Sid: S3Permissions Effect: Allow Action: - s3:CreateBucket - s3:DeleteObject - s3:GetObject - s3:PutObject - s3:ListBucket - s3:GetLifecycleConfiguration - s3:PutLifecycleConfiguration Resource: - !Sub "arn:aws:s3:::artilleryio-test-data-*" - !Sub "arn:aws:s3:::artilleryio-test-data-*/*" Outputs: RoleArn: Description: ARN of the IAM Role for Artillery.io Lambda functions Value: !GetAtt ArtilleryGitHubOIDCForLambdaRole.Arn OIDCProviderArn: Condition: CreateOIDCProvider Description: "ARN of the newly created OIDC provider" Value: !Ref GitHubOIDCProvider ================================================ FILE: packages/artillery/lib/platform/aws-ecs/ecs.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('platform:aws-ecs'); const ensureS3BucketExists = require('../aws/aws-ensure-s3-bucket-exists'); const { IAMClient, GetRoleCommand, CreateRoleCommand, CreatePolicyCommand, AttachRolePolicyCommand } = require('@aws-sdk/client-iam'); const { ensureParameterExists } = require('./legacy/aws-util'); const { S3_BUCKET_NAME_PREFIX } = require('../aws/constants'); const getAccountId = require('../aws/aws-get-account-id'); const sleep = require('../../util/sleep'); const { getBucketRegion } = require('../aws/aws-get-bucket-region'); const awsGetDefaultRegion = require('../aws/aws-get-default-region'); class PlatformECS { constructor(_script, _payload, opts, platformOpts) { this.opts = opts; this.platformOpts = platformOpts; this.arnPrefx = this.platformOpts.region.startsWith('cn-') ? 'arn:aws-cn' : 'arn:aws'; this.testRunId = platformOpts.testRunId; if (!this.testRunId) { throw new Error('testRunId is required'); } this.s3LifecycleConfigurationRules = [ { Expiration: { Days: 2 }, Filter: { Prefix: 'tests/' }, ID: 'RemoveAdHocTestData', Status: 'Enabled' }, { Expiration: { Days: 7 }, Filter: { Prefix: 'test-runs/' }, ID: 'RemoveTestRunMetadata', Status: 'Enabled' } ]; } async init() { global.artillery.awsRegion = (await awsGetDefaultRegion()) || this.platformOpts.region; this.accountId = await getAccountId(); await ensureSSMParametersExist(this.platformOpts.region); const bucketName = await ensureS3BucketExists( this.platformOpts.region, this.s3LifecycleConfigurationRules, false ); global.artillery.s3BucketRegion = await getBucketRegion(bucketName); await this.createIAMResources( this.accountId, this.platformOpts.taskRoleName ); } async createIAMResources(accountId, taskRoleName) { const workerRoleArn = await this.createWorkerRole(accountId, taskRoleName); return { workerRoleArn }; } async createWorkerRole(accountId, taskRoleName) { const iam = new IAMClient({ region: global.artillery.awsRegion }); try { const res = await iam.send( new GetRoleCommand({ RoleName: taskRoleName }) ); return res.Role.Arn; } catch (err) { debug(err); } const createRoleResp = await iam.send( new CreateRoleCommand({ AssumeRolePolicyDocument: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { Service: ['ecs-tasks.amazonaws.com', 'ecs.amazonaws.com'] }, Action: 'sts:AssumeRole' } ] }), Path: '/', RoleName: taskRoleName }) ); const policyDocument = { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: ['ssm:DescribeParameters'], Resource: ['*'] }, { Effect: 'Allow', Action: [ 'ssm:GetParameters', 'ssm:GetParameter', 'ssm:PutParameter', 'ssm:DeleteParameter', 'ssm:DescribeParameters', 'ssm:GetParametersByPath' ], Resource: [ `${this.arnPrefx}:ssm:*:${accountId}:parameter/artilleryio/*` ] }, { Effect: 'Allow', Action: ['ecr:GetAuthorizationToken'], Resource: ['*'] }, { Effect: 'Allow', Action: ['logs:*'], Resource: [ `${this.arnPrefx}:logs:*:${accountId}:log-group:artilleryio-log-group*:*` ] }, { Effect: 'Allow', Action: ['sqs:*'], Resource: [`${this.arnPrefx}:sqs:*:${accountId}:artilleryio*`] }, { Effect: 'Allow', Action: ['s3:*'], Resource: [ `${this.arnPrefx}:s3:::${S3_BUCKET_NAME_PREFIX}-${accountId}`, `${this.arnPrefx}:s3:::${S3_BUCKET_NAME_PREFIX}-${accountId}/*` ] }, { Effect: 'Allow', Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'], Resource: ['*'] } ] }; const createPolicyResp = await iam.send( new CreatePolicyCommand({ PolicyName: 'artilleryio-ecs-worker-policy', Path: '/', PolicyDocument: JSON.stringify(policyDocument) }) ); await iam.send( new AttachRolePolicyCommand({ PolicyArn: createPolicyResp.Policy.Arn, RoleName: taskRoleName }) ); debug('Waiting for IAM role to be ready'); await sleep(30 * 1000); return createRoleResp.Role.Arn; } async createWorker() {} async prepareWorker() {} async runWorker() {} async stopWorker() {} async shutdown() {} } async function ensureSSMParametersExist(region) { await ensureParameterExists( '/artilleryio/NPM_TOKEN', 'null', 'SecureString', region ); await ensureParameterExists( '/artilleryio/NPM_REGISTRY', 'null', 'String', region ); await ensureParameterExists( '/artilleryio/NPM_SCOPE', 'null', 'String', region ); await ensureParameterExists( '/artilleryio/ARTIFACTORY_AUTH', 'null', 'SecureString', region ); await ensureParameterExists( '/artilleryio/ARTIFACTORY_EMAIL', 'null', 'String', region ); await ensureParameterExists( '/artilleryio/NPMRC', 'null', 'SecureString', region ); await ensureParameterExists( '/artilleryio/NPM_SCOPE_REGISTRY', 'null', 'String', region ); } module.exports = PlatformECS; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/aws-util.js ================================================ const { ECSClient, DescribeTasksCommand } = require('@aws-sdk/client-ecs'); const { SSMClient, GetParameterCommand, PutParameterCommand, DeleteParameterCommand } = require('@aws-sdk/client-ssm'); const debug = require('debug')('util'); module.exports = { // ECS: ecsDescribeTasks, // AWS SSM: ensureParameterExists, parameterExists, putParameter, getParameter, deleteParameter }; // Wraps ecs.describeTasks to support more than 100 task ARNs in params.tasks async function ecsDescribeTasks(params, region) { const ecs = new ECSClient({ apiVersion: '2014-11-13', region }); const taskArnChunks = splitIntoSublists(params.tasks, 100); const results = { tasks: [], failures: [] }; for (let i = 0; i < taskArnChunks.length; i++) { const params2 = Object.assign({}, params, { tasks: taskArnChunks[i] }); const ecsData = await ecs.send(new DescribeTasksCommand(params2)); results.tasks = results.tasks.concat(ecsData.tasks); results.failures = results.failures.concat(ecsData.failures); } return results; } // Slice input list into several lists, where each list has no more than maxGroupSize elements function splitIntoSublists(list, maxGroupSize) { const result = []; const numGroups = Math.ceil(list.length / maxGroupSize); for (let i = 0; i < numGroups; i++) { result.push(list.slice(i * maxGroupSize, i * maxGroupSize + maxGroupSize)); } return result; } // ******************** // AWS SSM helpers // In future these will be parameter-store agnostic, and work with Kubernetes // ConfigMaps or Azure/GCP native equivalents. // ******************** // If parameter exists, do nothing; otherwise set the value async function ensureParameterExists(ssmPath, defaultValue, type, region) { const exists = await parameterExists(ssmPath, region); if (exists) { return; } return putParameter(ssmPath, defaultValue, type, region); } async function parameterExists(path, region) { const ssm = new SSMClient({ apiVersion: '2014-11-06', region }); const getParams = { Name: path, WithDecryption: true }; try { await ssm.send(new GetParameterCommand(getParams)); return true; } catch (ssmErr) { if (ssmErr.name === 'ParameterNotFound') { return false; } else { throw ssmErr; } } } async function putParameter(path, value, type, region) { const ssm = new SSMClient({ apiVersion: '2014-11-06', region }); const putParams = { Name: path, Type: type, Value: value, Overwrite: true }; await ssm.send(new PutParameterCommand(putParams)); } async function getParameter(path, region) { const ssm = new SSMClient({ apiVersion: '2014-11-06', region }); try { const ssmResponse = await ssm.send( new GetParameterCommand({ Name: path, WithDecryption: true }) ); debug({ ssmResponse }); return ssmResponse.Parameter?.Value; } catch (ssmErr) { if (ssmErr.name === 'ParameterNotFound') { return false; } else { throw ssmErr; } } } async function deleteParameter(path, region) { const ssm = new SSMClient({ apiVersion: '2014-11-06', region }); try { const ssmResponse = await ssm.send( new DeleteParameterCommand({ Name: path }) ); debug({ ssmResponse }); return ssmResponse; } catch (ssmErr) { if (ssmErr.name === 'ParameterNotFound') { return false; } else { throw ssmErr; } } } ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/bom.js ================================================ const path = require('node:path'); const fs = require('node:fs'); const A = require('async'); const { isBuiltin } = require('node:module'); const detective = require('detective-es6'); const depTree = require('dependency-tree'); const walkSync = require('walk-sync'); const debug = require('debug')('bom'); const _ = require('lodash'); const BUILTIN_PLUGINS = require('./plugins').getAllPluginNames(); const BUILTIN_ENGINES = require('./plugins').getOfficialEngines(); const Table = require('cli-table3'); const { resolveConfigTemplates } = require('../../../../util'); const prepareTestExecutionPlan = require('../../../../lib/util/prepare-test-execution-plan'); const { readScript, parseScript } = require('../../../../util'); // NOTE: Code below presumes that all paths are absolute //Tests in Fargate run on ubuntu, which uses posix paths //This function converts a path to posix path, in case the original path was not posix (e.g. windows runs) function _convertToPosixPath(p) { return p.split(path.sep).join(path.posix.sep); } // NOTE: absoluteScriptPath here is actually the absolute path to the config file function createBOM(absoluteScriptPath, extraFiles, opts, callback) { A.waterfall( [ A.constant(absoluteScriptPath), async (scriptPath) => { let scriptData; if (scriptPath.toLowerCase().endsWith('.ts')) { scriptData = await prepareTestExecutionPlan( [scriptPath], opts.flags, [] ); scriptData.config.processor = scriptPath; } else { const data = await readScript(scriptPath); scriptData = await parseScript(data); } return scriptData; }, (scriptData, next) => { return next(null, { opts: { scriptData, absoluteScriptPath, flags: opts.flags, scenarioPath: opts.scenarioPath // Absolute path to the file that holds scenarios }, localFilePaths: [absoluteScriptPath], npmModules: [] }); }, applyScriptChanges, getPlugins, getCustomEngines, getCustomJsDependencies, getVariableDataFiles, getFileUploadPluginFiles, getExtraFiles, getDotEnv, expandDirectories ], (err, context) => { if (err) { return callback(err, null); } context.localFilePaths = context.localFilePaths.concat(extraFiles); // TODO: Entries in localFilePaths may be directories // How many entries do we have here? If we have only one entry, the string itself // will be the common prefix, meaning that when we substring() on it later, we'll // get an empty string, ending up with a manifest like: // { files: // [ { orig: '/Users/h/tmp/artillery/hello.yaml', noPrefix: '' } ], // modules: [] } // let prefix = ''; if (context.localFilePaths.length === 1) { prefix = context.localFilePaths[0].substring( 0, context.localFilePaths[0].length - path.basename(context.localFilePaths[0]).length ); // This may still be an empty string if the script path is just 'hello.yml': prefix = prefix.length === 0 ? context.localFilePaths[0] : prefix; } else { prefix = commonPrefix(context.localFilePaths); } prefix = _convertToPosixPath(prefix); debug('prefix', prefix); // // include package.json / package-lock.json / yarn.lock // let packageDescriptionFiles = ['.npmrc']; if (opts.packageJsonPath) { packageDescriptionFiles.push(opts.packageJsonPath); } else { packageDescriptionFiles = packageDescriptionFiles.concat([ 'package.json', 'package-lock.json', 'yarn.lock' ]); } const dependencyFiles = packageDescriptionFiles.map((s) => path.join(prefix, s) ); debug(dependencyFiles); dependencyFiles.forEach((p) => { try { if (fs.statSync(p)) { context.localFilePaths.push(p); } } catch (_ignoredErr) {} }); const files = context.localFilePaths.map((p) => { return { orig: p, noPrefix: p.substring(prefix.length, p.length), origPosix: _convertToPosixPath(p), noPrefixPosix: _convertToPosixPath(p).substring( prefix.length, p.length ) }; }); const pkgPath = _.find(files, (f) => { return f.noPrefix === 'package.json'; }); if (pkgPath) { const pkg = JSON.parse(fs.readFileSync(pkgPath.orig, 'utf8')); const pkgDeps = [].concat( Object.keys(pkg.dependencies || {}), Object.keys(pkg.devDependencies || {}) ); context.pkgDeps = pkgDeps; context.npmModules = _.uniq(context.npmModules.concat(pkgDeps)).sort(); } else { context.pkgDeps = []; } return callback(null, { files: _.uniqWith(files, _.isEqual), modules: _.uniq(context.npmModules).filter( (m) => m !== 'artillery' && m !== 'playwright' && !m.startsWith('@playwright/') ), pkgDeps: context.pkgDeps, fullyResolvedConfig: context.opts.scriptData.config }); } ); } function isLocalModule(modName) { // NOTE: Absolute paths not supported return modName.startsWith('.'); } function applyScriptChanges(context, next) { resolveConfigTemplates( context.opts.scriptData, context.opts.flags, context.opts.absoluteScriptPath, context.opts.scenarioPath ).then((resolvedConfig) => { context.opts.scriptData = resolvedConfig; return next(null, context); }); } function getPlugins(context, next) { const environmentPlugins = _.reduce( _.get(context, 'opts.scriptData.config.environments', {}), function getEnvironmentPlugins(acc, envSpec, _envName) { acc = acc.concat(Object.keys(envSpec.plugins || [])); return acc; }, [] ); const pluginNames = Object.keys( _.get(context, 'opts.scriptData.config.plugins', {}) ).concat(environmentPlugins); const pluginPackages = _.uniq( pluginNames .filter((p) => BUILTIN_PLUGINS.indexOf(p) === -1) .map((p) => `artillery-plugin-${p}`) ); debug(pluginPackages); context.npmModules = context.npmModules.concat(pluginPackages); return next(null, context); } function getCustomEngines(context, next) { const environmentEngines = _.reduce( _.get(context, 'opts.scriptData.config.environments', {}), function getEnvironmentEngines(acc, envSpec, _envName) { acc = acc.concat(Object.keys(envSpec.engines || [])); return acc; }, [] ); const engineNames = Object.keys( _.get(context, 'opts.scriptData.config.engines', {}) ).concat(environmentEngines); const enginePackages = _.uniq( engineNames .filter((p) => BUILTIN_ENGINES.indexOf(p) === -1) .map((p) => `artillery-engine-${p}`) ); context.npmModules = context.npmModules.concat(enginePackages); return next(null, context); } function getCustomJsDependencies(context, next) { if (context.opts.scriptData.config?.processor) { // // Path to the main processor file: // const procPath = path.resolve( path.dirname(context.opts.absoluteScriptPath), context.opts.scriptData.config.processor ); context.localFilePaths.push(procPath); // Get the tree of requires from the main processor file: const tree = depTree.toList({ filename: procPath, directory: path.dirname(context.opts.absoluteScriptPath), filter: (path) => path.indexOf('node_modules') === -1 // optional }); debug('tree'); debug(tree); function getNpmDependencies(filename) { const src = fs.readFileSync(filename); const requires = detective(src); const npmPackages = requires .filter( (requireString) => !isBuiltin(requireString) && !isLocalModule(requireString) ) .map((requireString) => { return requireString.startsWith('@') ? `${requireString.split('/')[0]}/${requireString.split('/')[1]}` : requireString.split('/')[0]; }); return npmPackages; } const allNpmDeps = tree.map(getNpmDependencies); debug(allNpmDeps); const reduced = allNpmDeps.reduce((acc, deps) => { deps.forEach((d) => { if (acc.indexOf(d) === -1) { acc.push(d); } }); return acc; }, []); debug(reduced); // // Any other local JS files and npm packages: // const procSrc = fs.readFileSync(procPath); const _processorRequires = detective(procSrc); // TODO: Look for and load dir/index.js and get its dependencies, // rather than just grabbing the entire directory. // NOTE: Some of these may be directories (with an index.js inside) // Could be JSON files too. context.localFilePaths = context.localFilePaths.concat(tree); context.npmModules = context.npmModules.concat(reduced); // Remove duplicate entries for the same file when invoked on a single .ts script // See line 44 - the config.processor property is always set on .ts files, which leads to // multiple entries in the localFilePaths array for the same file context.localFilePaths = _.uniq(context.localFilePaths); debug('got custom JS dependencies'); return next(null, context); } else { debug('no custom JS dependencies'); return next(null, context); } } function getVariableDataFiles(context, next) { // NOTE: assuming that context.opts.scriptData contains both the config and // the scenarios section here. // Iterate over environments function resolvePayloadPaths(obj) { const result = []; if (obj.payload) { // When using a separate config file, resolve paths relative to the scenario file // Otherwise, resolve relative to the config file const baseDir = context.opts.scenarioPath ? path.dirname(context.opts.scenarioPath) : path.dirname(context.opts.absoluteScriptPath); if (_.isArray(obj.payload)) { obj.payload.forEach((payloadSpec) => { result.push(path.resolve(baseDir, payloadSpec.path)); }); } else if (_.isObject(obj.payload)) { // isObject returns true for arrays, so this branch must come second result.push(path.resolve(baseDir, obj.payload.path)); } } return result; } context.localFilePaths = context.localFilePaths.concat( resolvePayloadPaths(context.opts.scriptData.config) ); context.opts.scriptData.config.environments = context.opts.scriptData.config.environments || {}; Object.keys(context.opts.scriptData.config.environments).forEach( (envName) => { const envSpec = context.opts.scriptData.config.environments[envName]; context.localFilePaths = context.localFilePaths.concat( resolvePayloadPaths(envSpec) ); } ); return next(null, context); } function getFileUploadPluginFiles(context, next) { if (context.opts.scriptData.config?.plugins?.['http-file-uploads']) { // Append filePaths array if it's there: if (context.opts.scriptData.config.plugins['http-file-uploads'].filePaths) { // When using a separate config file, resolve paths relative to the scenario file // Otherwise, resolve relative to the config file const baseDir = context.opts.scenarioPath ? path.dirname(context.opts.scenarioPath) : path.dirname(context.opts.absoluteScriptPath); const absPaths = context.opts.scriptData.config.plugins[ 'http-file-uploads' ].filePaths.map((p) => { return path.resolve(baseDir, p); }); context.localFilePaths = context.localFilePaths.concat(absPaths); } return next(null, context); } else { return next(null, context); } } function getExtraFiles(context, next) { if (context.opts.scriptData.config?.includeFiles) { // When using a separate config file, resolve paths relative to the scenario file // Otherwise, resolve relative to the config file const baseDir = context.opts.scenarioPath ? path.dirname(context.opts.scenarioPath) : path.dirname(context.opts.absoluteScriptPath); const absPaths = _.map(context.opts.scriptData.config.includeFiles, (p) => { const includePath = path.resolve(baseDir, p); debug('includeFile:', includePath); return includePath; }); context.localFilePaths = context.localFilePaths.concat(absPaths); return next(null, context); } else { return next(null, context); } } function getDotEnv(context, next) { const flags = context.opts.flags; if (!flags.dotenv || flags.platform === 'aws:ecs') { return next(null, context); } const dotEnvPath = path.resolve(process.cwd(), flags.dotenv); try { if (fs.statSync(dotEnvPath)) { context.localFilePaths.push(dotEnvPath); } } catch (_ignoredErr) { console.log(`WARNING: could not find dotenv file: ${flags.dotenv}`); } return next(null, context); } function expandDirectories(context, next) { // This can potentially lead to VERY unexpected behaviour, when used // without due care with the file upload plugin (if filePaths is pointed at // a directory that contains files OTHER than those to be used with the // plugin) // // TODO: Warn if there are too many files in the directory // TODO: Only allow specific filenames or globs, not directories debug(context.localFilePaths); // FIXME: Don't need to scan twice: const dirs = context.localFilePaths.filter((p) => { let result = false; try { result = fs.statSync(p).isDirectory(); } catch (_fsErr) {} return result; }); // Remove directories from the list: context.localFilePaths = context.localFilePaths.filter((p) => { let result = true; try { result = !fs.statSync(p).isDirectory(); } catch (_fsErr) {} return result; }); debug('Dirs to expand'); debug(dirs); dirs.forEach((d) => { const entries = walkSync.entries(d, { directories: false }); debug(entries); context.localFilePaths = context.localFilePaths.concat( entries.map((e) => { return path.resolve(d, e.relativePath); }) ); }); return next(null, context); } function commonPrefix(paths, separator) { if ( !paths || paths.length === 0 || paths.filter((s) => typeof s !== 'string').length > 0 ) { return ''; } if (paths.includes('/')) { return '/'; } const sep = separator ? separator : path.sep; const splitPaths = paths.map((p) => p.split(sep)); const shortestPath = splitPaths.reduce((a, b) => { return a.length < b.length ? a : b; }, splitPaths[0]); let furthestIndex = shortestPath.length; for (const p of splitPaths) { for (let i = 0; i < furthestIndex; i++) { if (p[i] !== shortestPath[i]) { furthestIndex = i; break; } } } const joined = shortestPath.slice(0, furthestIndex).join(sep); if (joined.length > 0) { // Check if joined path already ends with separator which // will happen when input is a root drive on Windows, e.g. "C:\" return joined.endsWith(sep) ? joined : joined + sep; } else { return ''; } } function prettyPrint(manifest) { artillery.logger({ showTimestamp: true }).log('Test bundle prepared...'); artillery.log('Test bundle contents:'); const t = new Table({ head: ['Name', 'Type', 'Notes'] }); for (const f of manifest.files) { t.push([f.noPrefix, 'file']); } for (const m of manifest.modules) { t.push([ m, 'package', manifest.pkgDeps.indexOf(m) === -1 ? 'not in package.json' : '' ]); } artillery.log(t.toString()); artillery.log(); } module.exports = { createBOM, commonPrefix, prettyPrint, applyScriptChanges, getCustomJsDependencies }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/constants.js ================================================ const pkgJson = require('../../../../package.json'); const DEFAULT_IMAGE_TAG = pkgJson.version; // Default wait timeout for cloud workers to start let WAIT_TIMEOUT_SEC = 600; // Legacy override if (process.env.ECS_WAIT_TIMEOUT) { WAIT_TIMEOUT_SEC = parseInt(process.env.ECS_WAIT_TIMEOUT, 10); } // Override if (process.env.WORKER_WAIT_TIMEOUT_SEC) { WAIT_TIMEOUT_SEC = parseInt(process.env.WORKER_WAIT_TIMEOUT_SEC, 10); } module.exports = { ARTILLERY_CLUSTER_NAME: 'artilleryio-cluster', TASK_NAME: 'artilleryio-loadgen-worker', SQS_QUEUES_NAME_PREFIX: 'artilleryio_test_metrics', S3_BUCKET_NAME_PREFIX: 'artilleryio-test-data', LOGGROUP_NAME: 'artilleryio-log-group', LOGGROUP_RETENTION_DAYS: process.env.ARTILLERY_LOGGROUP_RETENTION_DAYS || 180, IMAGE_VERSION: process.env.ECR_IMAGE_VERSION || DEFAULT_IMAGE_TAG, WAIT_TIMEOUT: WAIT_TIMEOUT_SEC, TEST_RUNS_MAX_TAGS: parseInt(process.env.TEST_RUNS_MAX_TAGS, 10) || 8 }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/create-s3-client.js ================================================ const { S3Client } = require('@aws-sdk/client-s3'); module.exports = createS3Client; function createS3Client(opts = {}) { const defaultOpts = { apiVersion: '2006-03-01' }; let clientOpts = Object.assign(defaultOpts, opts); if (process.env.ARTILLERY_S3_OPTS) { clientOpts = Object.assign( defaultOpts, JSON.parse(process.env.ARTILLERY_S3_OPTS) ); } if (!opts.region) { clientOpts.region = global.artillery.s3BucketRegion; } return new S3Client(clientOpts); } ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/create-test.js ================================================ const A = require('async'); const debug = require('debug')('commands:create-test'); const { getBucketName } = require('./util'); const createS3Client = require('./create-s3-client'); const path = require('node:path'); const fs = require('node:fs'); const { createBOM, prettyPrint } = require('./bom'); const { PutObjectCommand } = require('@aws-sdk/client-s3'); function tryCreateTest(scriptPath, options) { createTest(scriptPath, options); } async function createTest(scriptPath, options, callback) { const absoluteScriptPath = path.resolve(process.cwd(), scriptPath); const contextPath = options.context ? path.resolve(options.context) : path.dirname(absoluteScriptPath); debug('script:', absoluteScriptPath); debug('root:', contextPath); const context = { contextDir: contextPath, scriptPath: absoluteScriptPath, originalScriptPath: scriptPath, name: options.name, // test name, eg simple-bom or aht_$UUID manifestPath: options.manifestPath, packageJsonPath: options.packageJsonPath, flags: options.flags }; if (typeof options.config === 'string') { const absoluteConfigPath = path.resolve(process.cwd(), options.config); context.configPath = absoluteConfigPath; } if (options.customSyncClient) { context.customSyncClient = options.customSyncClient; } return new Promise((resolve, reject) => { A.waterfall( [ A.constant(context), async (context) => { if (!context.customSyncClient) { context.s3Bucket = await getBucketName(); return context; } else { context.s3Bucket = 'S3_BUCKET_ARGUMENT_NOT_USED_ON_AZURE'; return context; } }, prepareManifest, printManifest, syncS3, writeTestMetadata ], (err, context) => { if (err) { console.log(err); return; } if (callback) { callback(err, context); } else if (err) { reject(err); } else { resolve(context); } } ); }); } function prepareManifest(context, callback) { let fileToAnalyse = context.scriptPath; const extraFiles = []; if (context.configPath) { debug('context has been provided; extraFiles =', extraFiles); fileToAnalyse = context.configPath; extraFiles.push(context.scriptPath); } createBOM( fileToAnalyse, extraFiles, { packageJsonPath: context.packageJsonPath, flags: context.flags, scenarioPath: context.scriptPath }, (err, bom) => { debug(err); debug(bom); context.manifest = bom; return callback(err, context); } ); } function printManifest(context, callback) { prettyPrint(context.manifest); return callback(null, context); } async function syncS3(context) { let s3; if (context.customSyncClient) { s3 = context.customSyncClient; } else { s3 = createS3Client(); } const prefix = `tests/${context.name}`; context.s3Prefix = prefix; debug('Will try syncing to:', context.s3Bucket); debug('Manifest: ', context.manifest); // Iterate through manifest, for each element: has orig (local source) and noPrefix (S3 // destination) properties return new Promise((resolve, reject) => { A.eachLimit( context.manifest.files, 3, async (item, eachDone) => { // If we can't read the file, it may have been specified with a // template in its name, e.g. a payload file like: // {{ $environment }}-users.csv // If so, ignore it, hope config.includeFiles was used, and let // "artillery run" in the worker deal with it. let body; try { body = fs.readFileSync(item.orig); } catch (fsErr) { debug(fsErr); } if (!body) { return eachDone(null, context); } // Filter bundled packages from package.json before upload if (item.noPrefix === 'package.json') { const pkg = JSON.parse(body.toString()); const filterBundled = (deps) => { if (!deps) return deps; const filtered = {}; for (const [name, version] of Object.entries(deps)) { if ( name !== 'artillery' && name !== 'playwright' && !name.startsWith('@playwright/') ) { filtered[name] = version; } } return filtered; }; pkg.dependencies = filterBundled(pkg.dependencies); pkg.devDependencies = filterBundled(pkg.devDependencies); body = Buffer.from(JSON.stringify(pkg, null, 2)); } const key = `${context.s3Prefix}/${item.noPrefixPosix}`; await s3.send( new PutObjectCommand({ Bucket: context.s3Bucket, Key: key, Body: body }) ); }, (err) => { if (err) { reject(err); } else { resolve(context); } } ); }); } // create just overwrites an existing test for now async function writeTestMetadata(context) { const metadata = { createdOn: Date.now(), name: context.name, modules: context.manifest.modules }; // Here we need to provide config information (if given) -- so that the worker knows how to load it if (context.configPath) { const res = context.manifest.files.filter((o) => { return o.orig === context.configPath; }); const newConfigPath = res[0].noPrefixPosix; // if we have been given a config, we must have an entry metadata.configPath = newConfigPath; } const newScriptPath = context.manifest.files.filter((o) => { return o.orig === context.scriptPath; })[0].noPrefixPosix; metadata.scriptPath = newScriptPath; debug('metadata', metadata); let s3 = null; if (context.customSyncClient) { s3 = context.customSyncClient; } else { s3 = createS3Client(); } const key = `${context.s3Prefix}/metadata.json`; // TODO: Rename to something less likely to clash debug('metadata location:', `${context.s3Bucket}/${key}`); await s3.send( new PutObjectCommand({ Body: JSON.stringify(metadata), Bucket: context.s3Bucket, Key: key }) ); return context; } module.exports = { tryCreateTest, createTest, syncS3, prepareManifest }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/errors.js ================================================ class TestNotFoundError extends Error { constructor(message) { super(message); this.name = 'TestNotFoundError'; } } class NoAvailableQueueError extends Error { constructor(message) { super(message); this.name = 'NoAvailableQueueError'; } } class ClientServerVersionMismatchError extends Error { constructor(message) { super(message); this.name = 'ClientServerMismatchError'; } } class ConsoleOutputSerializeError extends Error { constructor(message) { super(message); this.name = 'OutputSerializeError'; } } module.exports = { TestNotFoundError, NoAvailableQueueError, ClientServerVersionMismatchError, ConsoleOutputSerializeError }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/find-public-subnets.js ================================================ const assert = require('node:assert').strict; const { EC2Client, DescribeRouteTablesCommand, DescribeVpcsCommand, DescribeSubnetsCommand } = require('@aws-sdk/client-ec2'); class VPCSubnetFinder { constructor(opts) { this.ec2 = new EC2Client(opts); } async getRouteTables(vpcId) { const rts = await this.ec2.send( new DescribeRouteTablesCommand({ Filters: [ { Name: 'vpc-id', Values: [vpcId] } ] }) ); return rts.RouteTables; } async findDefaultVpc() { const vpcRes = await this.ec2.send( new DescribeVpcsCommand({ Filters: [ { Name: 'isDefault', Values: ['true'] } ] }) ); assert.ok(vpcRes.Vpcs.length <= 1); if (vpcRes.Vpcs.length !== 1) { return null; } else { return vpcRes.Vpcs[0].VpcId; } } async getSubnets(vpcId) { const subRes = await this.ec2.send( new DescribeSubnetsCommand({ Filters: [ { Name: 'vpc-id', Values: [vpcId] } ] }) ); return subRes.Subnets; } isSubnetPublic(routeTables, subnetId) { // // Inspect associations of each route table (of a specific VPC). A route // table record has an Associations field, which is a list of association // objects. There are two types of those: // // 1. An implicit association, which is indicated by field Main set to // true and no explicit subnet id. // 2. An explicit association, which is indicated by field Main set to // false, and a SubnetId field containing a subnet id. // // Route table for the subnet - can there only be one? let subnetTable = routeTables.filter((rt) => { const explicitAssoc = rt.Associations.filter((assoc) => { return assoc.SubnetId && assoc.SubnetId === subnetId; }); assert.ok(explicitAssoc.length <= 1); return explicitAssoc.length === 1; }); if (subnetTable.length === 0) { // There is no explicit association for this subnet so it will be implicitly // associated with the VPC's main routing table. subnetTable = routeTables.filter((rt) => { const implicitAssoc = rt.Associations.filter((assoc) => { return assoc.Main === true; }); assert.ok(implicitAssoc.length <= 1); return implicitAssoc.length === 1; }); } if (subnetTable.length !== 1) { throw new Error( `Could not locate routing table for subnet: subnet id: ${subnetId}` ); } const igwRoutes = subnetTable[0].Routes.filter((route) => { // NOTE: there may be no IGW attached to route return route.GatewayId?.startsWith('igw-'); }); return igwRoutes.length > 0; } // TODO: Distinguish between there being no default VPC, // or being given an invalid VPC ID, and no public subnets // existing in a VPC that definitely exists. async findPublicSubnets(vpcId) { if (!vpcId) { vpcId = await this.findDefaultVpc(); } const rts = await this.getRouteTables(vpcId); const subnets = await this.getSubnets(vpcId); const publicSubnets = subnets.filter((subnet) => { return this.isSubnetPublic(rts, subnet.SubnetId); }); return publicSubnets; } } async function main() { const f = new VPCSubnetFinder({ region: process.env.REGION }); try { const publicSubnets = await f.findPublicSubnets(process.env.VPC_ID); console.log(publicSubnets.map((s) => s.SubnetId).join('\n')); } catch (err) { console.log(err); } } if (require.main === module) { main(); } module.exports = { VPCSubnetFinder }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ module.exports.Plugin = ArtilleryInspectScriptPlugin; const { btoa } = require('../../util'); function ArtilleryInspectScriptPlugin(script, events) { this.script = script; this.events = events; const checksConfig = script.config?.ensure || script.config?.plugins?.ensure; if (checksConfig) { console.log( `inspect-script.config.ensure=${btoa(JSON.stringify(checksConfig))}` ); } return this; } ArtilleryInspectScriptPlugin.prototype.cleanup = (done) => { done(null); }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js ================================================ // Copyright (c) Artillery Software Inc. // SPDX-License-Identifier: BUSL-1.1 // // Non-evaluation use of Artillery on Azure requires a commercial license const { QueueClient } = require('@azure/storage-queue'); const { BlobServiceClient } = require('@azure/storage-blob'); const { DefaultAzureCredential } = require('@azure/identity'); const { randomUUID } = require('node:crypto'); function getAQS() { return new QueueClient( process.env.AZURE_STORAGE_QUEUE_URL, new DefaultAzureCredential() ); } // Azure Queue Storage has a 64KB message limit // Use 60KB threshold to leave margin for encoding overhead const AQS_SIZE_LIMIT = 60 * 1024; let blobContainerClient = null; function getBlobClient() { if (!blobContainerClient) { const storageAccount = process.env.AZURE_STORAGE_ACCOUNT; const containerName = process.env.AZURE_STORAGE_BLOB_CONTAINER; if (!storageAccount || !containerName) { throw new Error( 'AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_BLOB_CONTAINER must be set' ); } const blobServiceClient = new BlobServiceClient( `https://${storageAccount}.blob.core.windows.net`, new DefaultAzureCredential() ); blobContainerClient = blobServiceClient.getContainerClient(containerName); } return blobContainerClient; } async function sendMessage(queue, body, tags) { const payload = JSON.stringify({ payload: body, attributes: tags.reduce((acc, tag) => { acc[tag.key] = tag.value; return acc; }, {}) }); // Check if payload exceeds Azure Queue Storage limit if (Buffer.byteLength(payload, 'utf8') > AQS_SIZE_LIMIT) { // Upload to blob storage and send reference const testId = tags.find((t) => t.key === 'testId')?.value; const workerId = tags.find((t) => t.key === 'workerId')?.value; const messageId = randomUUID(); const blobName = `tests/${testId}/overflow/${workerId}/${messageId}.json`; const blobClient = getBlobClient().getBlockBlobClient(blobName); await blobClient.upload(payload, Buffer.byteLength(payload, 'utf8')); // Send reference message const refPayload = JSON.stringify({ payload: { _overflowRef: blobName, event: body.event }, attributes: tags.reduce((acc, tag) => { acc[tag.key] = tag.value; return acc; }, {}) }); return queue.sendMessage(refPayload); } return queue.sendMessage(payload); } module.exports = { getAQS, sendMessage }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs'); const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const _debug = require('debug')('plugin:sqsReporter'); const uuid = require('node:crypto').randomUUID; const { getAQS, sendMessage } = require('./azure-aqs'); // SQS has 1MB message limit. Use 950KB threshold for safety margin. const SQS_SIZE_LIMIT = 950 * 1024; module.exports = { Plugin: ArtillerySQSPlugin, LEGACY_METRICS_FORMAT: false }; function ArtillerySQSPlugin(script, events) { this.script = script; this.events = events; this.unsent = 0; // List of objects: [{key: 'SomeKey', value: 'SomeValue'}, ...] this.tags = process.env.SQS_TAGS ? JSON.parse(process.env.SQS_TAGS) : []; this.testId = null; const messageAttributes = {}; this.tags.forEach((tag) => { if (tag.key === 'testId') { this.testId = tag.value; } messageAttributes[tag.key] = { DataType: 'String', StringValue: tag.value }; }); this.messageAttributes = messageAttributes; this.sqs = null; this.aqs = null; if (process.env.SQS_QUEUE_URL) { this.sqs = new SQSClient({ region: process.env.SQS_REGION || script.config.plugins['sqs-reporter'].region }); this.queueUrl = process.env.SQS_QUEUE_URL || script.config.plugins['sqs-reporter'].queueUrl; } this.s3 = null; this.s3Bucket = process.env.ARTILLERY_S3_BUCKET || null; if (this.sqs && this.s3Bucket) { this.s3 = new S3Client({ region: process.env.SQS_REGION || script.config.plugins['sqs-reporter'].region }); } if (process.env.AZURE_STORAGE_QUEUE_URL) { this.aqs = getAQS(); } events.on('stats', (statsOriginal) => { const serialized = global.artillery.__SSMS.serializeMetrics(statsOriginal); const body = { event: 'workerStats', stats: serialized }; this.sendMessage(body); }); //TODO: reconcile some of this code with how lambda does sqs reporting events.on('phaseStarted', (phaseContext) => { const body = { event: 'phaseStarted', phase: phaseContext }; this.sendMessage(body); }); //TODO: reconcile some of this code with how lambda does sqs reporting events.on('phaseCompleted', (phaseContext) => { const body = { event: 'phaseCompleted', phase: phaseContext }; this.sendMessage(body); }); events.on('done', (_stats) => { const body = { event: 'done', stats: global.artillery.__SSMS.serializeMetrics(_stats) }; this.sendMessage(body); }); global.artillery.globalEvents.on('log', (opts, ...args) => { if (process.env.SHIP_LOGS) { const body = { event: 'artillery.log', log: { opts, args: [...args] } }; this.sendMessage(body); } }); return this; } ArtillerySQSPlugin.prototype.cleanup = function (done) { const interval = setInterval(() => { if (this.unsent <= 0) { clearInterval(interval); done(null); } }, 200).unref(); }; ArtillerySQSPlugin.prototype.sendMessage = function (body) { if (this.sqs) { this.sendSQS(body); } else { this.sendAQS(body); } }; ArtillerySQSPlugin.prototype.sendSQS = async function (body) { this.unsent++; const payload = JSON.stringify(body); const payloadSize = Buffer.byteLength(payload, 'utf8'); try { let messageBody = payload; // Upload to S3 if payload exceeds SQS limit if (payloadSize > SQS_SIZE_LIMIT && this.s3 && this.s3Bucket) { const workerId = this.tags.find((t) => t.key === 'workerId')?.value; const messageId = uuid(); const s3Key = `tests/${this.testId}/overflow/${workerId}/${messageId}.json`; await this.s3.send( new PutObjectCommand({ Bucket: this.s3Bucket, Key: s3Key, Body: payload, ContentType: 'application/json' }) ); messageBody = JSON.stringify({ event: body.event, _overflowRef: s3Key }); _debug( 'Payload %d bytes exceeded limit, uploaded to S3: %s', payloadSize, s3Key ); } const params = { MessageBody: messageBody, QueueUrl: this.queueUrl, MessageAttributes: this.messageAttributes, MessageDeduplicationId: uuid(), MessageGroupId: this.testId }; await this.sqs.send(new SendMessageCommand(params)); } catch (err) { console.error(err); } finally { this.unsent--; } }; ArtillerySQSPlugin.prototype.sendAQS = async function (body) { this.unsent++; sendMessage(this.aqs, body, this.tags) .then((_res) => { this.unsent--; }) .catch((err) => { console.error(err); this.unsent--; }); }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/plugins.js ================================================ module.exports.getAllPluginNames = function () { return [...this.getOfficialPlugins(), ...this.getProPlugins()]; }; module.exports.getOfficialPlugins = () => [ 'ensure', 'expect', 'metrics-by-endpoint', 'publish-metrics', 'apdex', 'slack' ]; module.exports.getOfficialEngines = () => ['playwright']; module.exports.getProPlugins = () => ['http-ssl-auth', 'http-file-uploads', 'sqs-reporter']; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/run-cluster.js ================================================ const { ECSClient, CreateClusterCommand, DescribeClustersCommand, RunTaskCommand, StopTaskCommand, DescribeTaskDefinitionCommand, RegisterTaskDefinitionCommand, DeregisterTaskDefinitionCommand, InvalidParameterException, ThrottlingException } = require('@aws-sdk/client-ecs'); const { SQSClient, CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, GetQueueAttributesCommand } = require('@aws-sdk/client-sqs'); const { GetObjectCommand, NoSuchKey } = require('@aws-sdk/client-s3'); const { IAMClient, GetRoleCommand } = require('@aws-sdk/client-iam'); // Normal debugging for messages, summaries, and errors: const debug = require('debug')('commands:run-test'); // Verbose debugging for responses from AWS API calls, large objects etc: const debugVerbose = require('debug')('commands:run-test:v'); const debugErr = require('debug')('commands:run-test:errors'); const _A = require('async'); const path = require('node:path'); const fs = require('node:fs'); const chalk = require('chalk'); const defaultOptions = require('rc')('artillery'); const moment = require('moment'); const EnsurePlugin = require('artillery-plugin-ensure'); const SlackPlugin = require('artillery-plugin-slack'); const { getADOTRelevantReporterConfigs, resolveADOTConfigSettings } = require('artillery-plugin-publish-metrics'); const EventEmitter = require('node:events'); const _ = require('lodash'); const pkg = require('../../../../package.json'); const { parseTags } = require('./tags'); const { Timeout, sleep, timeStringToMs } = require('./time'); const { SqsReporter } = require('./sqs-reporter'); const awaitOnEE = require('../../../../lib/util/await-on-ee'); const { VPCSubnetFinder } = require('./find-public-subnets'); const awsUtil = require('./aws-util'); const { createTest } = require('./create-test'); const createS3Client = require('./create-s3-client'); const { getBucketName } = require('./util'); const getAccountId = require('../../aws/aws-get-account-id'); const { setCloudwatchRetention } = require('../../aws/aws-cloudwatch'); const dotenv = require('dotenv'); const util = require('./util'); module.exports = runCluster; let consoleReporter = { toggleSpinner: () => {} }; const { TASK_NAME, SQS_QUEUES_NAME_PREFIX, LOGGROUP_NAME, LOGGROUP_RETENTION_DAYS, IMAGE_VERSION, WAIT_TIMEOUT, ARTILLERY_CLUSTER_NAME, TEST_RUNS_MAX_TAGS } = require('./constants'); const { TestNotFoundError, NoAvailableQueueError, ClientServerVersionMismatchError } = require('./errors'); let IS_FARGATE = false; const TEST_RUN_STATUS = require('./test-run-status'); const prepareTestExecutionPlan = require('../../../util/prepare-test-execution-plan'); const { PutObjectCommand } = require('@aws-sdk/client-s3'); const awsGetDefaultRegion = require('../../aws/aws-get-default-region'); function setupConsoleReporter(quiet) { const reporterOpts = { outputFormat: 'classic', printPeriod: false, quiet: quiet }; if ( global.artillery?.version?.startsWith('2') ) { delete reporterOpts.outputFormat; delete reporterOpts.printPeriod; } const reporterEvents = new EventEmitter(); consoleReporter = global.artillery.__createReporter( reporterEvents, reporterOpts ); // // Disable spinner on v1 if ( global.artillery?.version && !global.artillery.version.startsWith('2') ) { consoleReporter.spinner.stop(); consoleReporter.spinner.clear(); consoleReporter.spinner = { start: () => {}, stop: () => {}, clear: () => {} }; } return { reporterEvents }; } function runCluster(scriptPath, options) { const artilleryReporter = setupConsoleReporter(options.quiet); // camelCase all flag names, e.g. `launch-config` becomes launchConfig const options2 = {}; for (const [k, v] of Object.entries(options)) { options2[_.camelCase(k)] = v; } tryRunCluster(scriptPath, options2, artilleryReporter); } function logProgress(msg, opts = {}) { if (typeof opts.showTimestamp === 'undefined') { opts.showTimestamp = true; } if (global.artillery?.log) { artillery.logger(opts).log(msg); } else { consoleReporter.toggleSpinner(); artillery.log( `${msg} ${chalk.gray(`[${moment().format('HH:mm:ss')}]`)}` ); consoleReporter.toggleSpinner(); } } async function tryRunCluster(scriptPath, options, artilleryReporter) { global.artillery.awsRegion = (await awsGetDefaultRegion()) || options.region; let context = {}; const inputFiles = [].concat(scriptPath, options.config || []); const runnableScript = await prepareTestExecutionPlan(inputFiles, options); context.runnableScript = runnableScript; let absoluteScriptPath; if (typeof scriptPath !== 'undefined') { absoluteScriptPath = path.resolve(process.cwd(), scriptPath); context.namedTest = false; try { fs.statSync(absoluteScriptPath); } catch (_statErr) { artillery.log('Could not read file:', scriptPath); process.exit(1); } } if (options.dotenv) { const dotEnvPath = path.resolve(process.cwd(), options.dotenv); const contents = fs.readFileSync(dotEnvPath); context.dotenv = dotenv.parse(contents); } const cloudKey = options.key || process.env.ARTILLERY_CLOUD_API_KEY; if (cloudKey) { const cloudEndpoint = process.env.ARTILLERY_CLOUD_ENDPOINT; // Explicitly make Artillery Cloud API key available to workers (if it's set) // Relying on the fact that contents of context.dotenv gets passed onto workers // for it context.dotenv = { ...context.dotenv, ARTILLERY_CLOUD_API_KEY: cloudKey }; // Explicitly make Artillery Cloud endpoint available to workers (if it's set) if (cloudEndpoint) { context.dotenv = { ...context.dotenv, ARTILLERY_CLOUD_ENDPOINT: cloudEndpoint }; } } if (options.bundle) { context.namedTest = true; } if (options.maxDuration) { const maxDurationMs = timeStringToMs(options.maxDuration); context.maxDurationMs = maxDurationMs; } context.tags = parseTags(options.tags); if (context.tags.length > TEST_RUNS_MAX_TAGS) { console.error( chalk.red( `A maximum of ${TEST_RUNS_MAX_TAGS} tags is allowed per test run` ) ); process.exit(1); } // Set name tag if not already set: if (context.tags.filter((t) => t.name === 'name').length === 0) { if (typeof scriptPath !== 'undefined') { context.tags.push({ name: 'name', value: path.basename(scriptPath) }); } else { context.tags.push({ name: 'name', value: options.bundle }); } } if (options.name) { for (const t of context.tags) { if (t.name === 'name') { t.value = options.name; } } } context.extraSecrets = options.secret || []; context.testId = global.artillery.testRunId; if (context.namedTest) { context.s3Prefix = options.bundle; debug(`Trying to run a named test: ${context.s3Prefix}`); } if (!context.namedTest) { const contextPath = options.context ? path.resolve(options.context) : path.dirname(absoluteScriptPath); debugVerbose('script:', absoluteScriptPath); debugVerbose('root:', contextPath); const containerScriptPath = path.join( path.relative(contextPath, path.dirname(absoluteScriptPath)), path.basename(absoluteScriptPath) ); if (containerScriptPath.indexOf('..') !== -1) { artillery.log( chalk.red( 'Test script must reside inside the context dir. See Artillery docs for more details.' ) ); process.exit(1); } // FIXME: These need clearer names. dir vs path and local vs container. context.contextDir = contextPath; context.newScriptPath = containerScriptPath; debug('container script path:', containerScriptPath); } const count = Number(options.count) || 1; if (typeof options.taskRoleName !== 'undefined') { let customRoleName = options.taskRoleName; // Allow ARNs for convenience // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html // We split by :role because role names may contain slash characters (subpaths) if ( customRoleName.startsWith('arn:aws:iam') || customRoleName.startsWith('arn:aws-cn:iam') ) { customRoleName = customRoleName.split(':role/')[1]; } context.customTaskRoleName = customRoleName; } const clusterName = options.cluster || ARTILLERY_CLUSTER_NAME; if (options.launchConfig) { let launchConfig; try { launchConfig = JSON.parse(options.launchConfig); } catch (parseErr) { debug(parseErr); } if (!launchConfig) { artillery.log( chalk.red( "Launch config could not be parsed. Please check that it's valid JSON." ) ); process.exit(1); } if (launchConfig.ulimits && !Array.isArray(launchConfig.ulimits)) { // TODO: Proper schema validation for the object artillery.log(chalk.red('ulimits must be an array of objects')); artillery.log( 'Please see AWS documentation for more information:\nhttps://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Ulimit.html' ); process.exit(1); } options.launchConfig = launchConfig; } else { options.launchConfig = {}; } if (options.cpu) { const n = Number(options.cpu); if (Number.isNaN(n)) { artillery.log('The value of --cpu must be a number'); process.exit(1); } // Allow specifying 16 vCPU as either "16" or "16384". The actual value is // validated later. const MAX_VCPUS = 16; if (n <= MAX_VCPUS) { options.launchConfig.cpu = n * 1024; } else { options.launchConfig.cpu = n; } } if (options.memory) { const n = Number(options.memory); if (Number.isNaN(n)) { artillery.log('The value of --memory must be a number'); process.exit(1); } const MAX_MEMORY_IN_GB = 120; if (n <= MAX_MEMORY_IN_GB) { options.launchConfig.memory = String(parseInt(options.memory, 10) * 1024); } else { options.launchConfig.memory = options.memory; } } // check launch type is valid: if (typeof options.launchType !== 'undefined') { if ( options.launchType !== 'ecs:fargate' && options.launchType !== 'ecs:ec2' ) { artillery.log( 'Invalid launch type - the value of --launch-type needs to be ecs:fargate or ecs:ec2' ); process.exit(1); } } if (typeof options.fargate !== 'undefined') { console.error( 'The --fargate flag is deprecated, use --launch-type ecs:fargate instead' ); } if (options.fargate && options.launchType) { console.error( 'Either --fargate or --launch-type flag should be set, not both' ); process.exit(1); } if ( typeof options.fargate === 'undefined' && typeof options.launchType === 'undefined' ) { options.launchType = 'ecs:fargate'; } IS_FARGATE = typeof options.fargate !== 'undefined' || // --fargate set typeof options.publicSubnetIds !== 'undefined' || // --public-subnet-ids set (typeof options.launchType !== 'undefined' && options.launchType === 'ecs:fargate') || // --launch-type ecs:fargate typeof options.launchType === 'undefined'; global.artillery.globalEvents.emit('test:init', { flags: options, testRunId: context.testId, tags: context.tags, metadata: { testId: context.testId, startedAt: Date.now(), count, tags: context.tags, launchType: options.launchType } }); let packageJsonPath; if (options.packages) { packageJsonPath = path.resolve(process.cwd(), options.packages); try { // TODO: Check that filename is package.json JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } catch (err) { console.error('Could not load package dependency list'); console.error('Trying to read from:', packageJsonPath); console.error(err); } } context = Object.assign(context, { scriptPath: absoluteScriptPath, originalScriptPath: scriptPath, count: count, region: options.region, arnPrefix: options.region.startsWith('cn-') ? 'arn:aws-cn' : 'arn:aws', taskName: `${TASK_NAME}_${ IS_FARGATE ? 'fargate' : '' }_${clusterName}_${IMAGE_VERSION.replace(/\./g, '-')}_${Math.floor( Math.random() * 1e6 )}`, clusterName: clusterName, logGroupName: LOGGROUP_NAME, cliOptions: options, isFargate: IS_FARGATE, isCapacitySpot: typeof options.spot !== 'undefined', configTableName: '', status: TEST_RUN_STATUS.INITIALIZING, packageJsonPath, taskArns: [] }); let subnetIds = []; if (options.publicSubnetIds) { console.error( `${chalk.yellow( 'Warning' )}: --public-subnet-ids will be deprecated. Use --subnet-ids instead.` ); subnetIds = options.publicSubnetIds.split(','); } if (options.subnetIds) { subnetIds = options.subnetIds.split(','); } if (IS_FARGATE) { context.fargatePublicSubnetIds = subnetIds; context.fargateSecurityGroupIds = typeof options.securityGroupIds !== 'undefined' ? options.securityGroupIds.split(',') : []; } if (global.artillery?.telemetry) { global.artillery.telemetry.capture('run-test', { version: global.artillery.version, proVersion: pkg.version, count: count, launchPlatform: IS_FARGATE ? 'ecs:fargate' : 'ecs:ec2', usesTags: context.tags.length > 0, region: context.region, crossRegion: context.region !== context.backendRegion }); } async function newWaterfall(artilleryReporter) { let testRunCompletedSuccessfully = true; let shuttingDown = false; async function gracefulShutdown(opts = { earlyStop: false, exitCode: 0 }) { if (shuttingDown) { return; } shuttingDown = true; if (context.heartbeatIntervalId) { clearInterval(context.heartbeatIntervalId); context.heartbeatIntervalId = null; } if (opts.earlyStop) { if (context.status !== TEST_RUN_STATUS.ERROR) { // Retain ERROR status if already set elsewhere context.status = TEST_RUN_STATUS.EARLY_STOP; } } await cleanupResources(context); global.artillery.globalEvents.emit('shutdown:start', { exitCode: opts.exitCode, earlyStop: opts.earlyStop }); const ps = []; for (const e of global.artillery.extensionEvents) { const testInfo = { endTime: Date.now() }; if (e.ext === 'beforeExit') { ps.push( e.method({ report: context.aggregateReport, flags: context.cliOptions, runnerOpts: { environment: context.cliOptions?.environment, scriptPath: '', absoluteScriptPath: '' }, testInfo }) ); } } await Promise.allSettled(ps); const ps2 = []; const shutdownOpts = { earlyStop: opts.earlyStop, exitCode: opts.exitCode }; for (const e of global.artillery.extensionEvents) { if (e.ext === 'onShutdown') { ps2.push(e.method(shutdownOpts)); } } await Promise.allSettled(ps2); process.exit(global.artillery.suggestedExitCode || opts.exitCode); } global.artillery.shutdown = gracefulShutdown; process.on('SIGINT', async () => { if (shuttingDown) { return; } console.log('Stopping test run (SIGINT received)...'); await gracefulShutdown({ exitCode: 1, earlyStop: true }); }); process.on('SIGTERM', async () => { if (shuttingDown) { return; } console.log('Stopping test run (SIGTERM received)...'); await gracefulShutdown({ exitCode: 1, earlyStop: true }); }); // Messages from SQS reporter created later will be relayed via this EE context.reporterEvents = artilleryReporter.reporterEvents; try { logProgress('Checking AWS connectivity...'); context.accountId = await getAccountId({ region: context.region }); await Promise.all([ (async (context) => { const bucketName = await getBucketName(); context.s3Bucket = bucketName; return context; })(context) ]); logProgress('Checking cluster...'); const clusterExists = await checkTargetCluster(context); if (!clusterExists) { if (typeof context.cliOptions.cluster === 'undefined') { // User did not specify a cluster with --cluster, and ARTILLERY_CLUSTER_NAME // does not exist, so create it await createArtilleryCluster(context); } else { // User specified a cluster, but it's not there throw new Error( `Could not find cluster ${context.clusterName} in ${context.region}` ); } } if (context.tags.length > 0) { logProgress( `Tags: ${context.tags.map((t) => `${t.name}:${t.value}`).join(', ')}` ); } logProgress(`Test run ID: ${context.testId}`); logProgress('Preparing launch platform...'); await maybeGetSubnetIdsForFargate(context); logProgress( `Environment: Account: ${context.accountId} Region: ${context.region} Count: ${context.count} Cluster: ${context.clusterName} Launch type: ${context.cliOptions.launchType} ${ context.isFargate && context.isCapacitySpot ? '(Spot)' : '(On-demand)' } `, { showTimestamp: false } ); await createQueue(context); await checkCustomTaskRole(context); logProgress('Preparing test bundle...'); await createTestBundle(context); await createADOTDefinitionIfNeeded(context); await ensureTaskExists(context); await getManifest(context); await generateTaskOverrides(context); logProgress('Launching workers...'); await setupDefaultECSParams(context); if ( context.status !== TEST_RUN_STATUS.EARLY_STOP && context.status !== TEST_RUN_STATUS.TERMINATING ) { // Set up SQS listener: listen(context, artilleryReporter.reporterEvents); await launchLeadTask(context); } setCloudwatchRetention( `${LOGGROUP_NAME}/${context.clusterName}`, LOGGROUP_RETENTION_DAYS, context.region, { maxRetries: 10, waitPerRetry: 2 * 1000 } ); if ( context.status !== TEST_RUN_STATUS.EARLY_STOP && context.status !== TEST_RUN_STATUS.TERMINATING ) { logProgress( context.isFargate ? 'Waiting for Fargate...' : 'Waiting for ECS...' ); await ecsRunTask(context); } if ( context.status !== TEST_RUN_STATUS.EARLY_STOP && context.status !== TEST_RUN_STATUS.TERMINATING ) { await waitForTasks2(context); } if ( context.status !== TEST_RUN_STATUS.EARLY_STOP && context.status !== TEST_RUN_STATUS.TERMINATING ) { // Start heartbeat before waitForWorkerSync so that workers that are // already up can see heartbeats while the rest of the pool provisions. // With 500+ Fargate tasks this window can be 10+ minutes. context.heartbeatIntervalId = startHeartbeat(context); logProgress('Waiting for workers to come online...'); await waitForWorkerSync(context); await sendGoSignal(context); logProgress('Workers are running, waiting for reports...'); if (context.maxDurationMs && context.maxDurationMs > 0) { logProgress( `Max duration for test run is set to: ${context.cliOptions.maxDuration}` ); const testDurationTimeout = new Timeout(context.maxDurationMs); testDurationTimeout.start(); testDurationTimeout.on('timeout', async () => { artillery.log( `Max duration of test run exceeded: ${context.cliOptions.maxDuration}\n` ); await gracefulShutdown({ earlyStop: true }); }); } context.status = TEST_RUN_STATUS.RECEIVING_REPORTS; } // Need to wait for all reports to be over here, not exit const workerState = await awaitOnEE( artilleryReporter.reporterEvents, 'workersDone' ); debug(workerState); logProgress(`Test run completed: ${context.testId}`); context.status = TEST_RUN_STATUS.COMPLETED; let checks = []; global.artillery.globalEvents.once('checks', async (results) => { checks = results; }); if (context.ensureSpec) { new EnsurePlugin.Plugin({ config: { ensure: context.ensureSpec } }); } if (context.fullyResolvedConfig?.plugins?.slack) { new SlackPlugin.Plugin({ config: context.fullyResolvedConfig }); } if (context.cliOptions.output) { const logfile = getLogFilename( context.cliOptions.output, defaultOptions.logFilenameFormat ); for (const ix of context.intermediateReports) { delete ix.histograms; ix.histograms = ix.summaries; } delete context.aggregateReport.histograms; context.aggregateReport.histograms = context.aggregateReport.summaries; const jsonReport = { intermediate: context.intermediateReports, aggregate: context.aggregateReport, testId: context.testId, metadata: { tags: context.tags, count: context.count, region: context.region, cluster: context.clusterName, artilleryVersion: { core: global.artillery.version, pro: pkg.version } }, ensure: checks.map((c) => { return { condition: c.original, success: c.result === 1, strict: c.strict }; }) }; fs.writeFileSync(logfile, JSON.stringify(jsonReport, null, 2), { flag: 'w' }); } debug(context.testId, 'done'); } catch (err) { debug(err); if (err instanceof InvalidParameterException) { if ( err.message .toLowerCase() .indexOf('no container instances were found') !== -1 ) { artillery.log( chalk.yellow('The ECS cluster has no active EC2 instances') ); } else { artillery.log(err); } } else if (err instanceof TestNotFoundError) { artillery.log(`Test ${context.s3Prefix} not found`); } else if ( err instanceof NoAvailableQueueError || err instanceof ClientServerVersionMismatchError ) { artillery.log(chalk.red('Error:', err.message)); } else { artillery.log(util.formatError(err)); artillery.log(err); artillery.log(err.stack); } testRunCompletedSuccessfully = false; global.artillery.suggestedExitCode = 1; } finally { if (!testRunCompletedSuccessfully) { logProgress('Cleaning up...'); context.status = TEST_RUN_STATUS.ERROR; await gracefulShutdown({ earlyStop: true, exitCode: 1 }); } else { context.status = TEST_RUN_STATUS.COMPLETED; await gracefulShutdown({ earlyStop: false, exitCode: 0 }); } } } await newWaterfall(artilleryReporter); } async function cleanupResources(context) { try { if (context.sqsReporter) { context.sqsReporter.stop(); } if (context.adot?.SSMParameterPath) { await awsUtil.deleteParameter( context.adot.SSMParameterPath, context.region ); } if (context.taskArns && context.taskArns.length > 0) { for (const taskArn of context.taskArns) { try { const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); await ecs.send( new StopTaskCommand({ task: taskArn, cluster: context.clusterName, reason: 'Test cleanup' }) ); } catch (err) { // TODO: Retry if appropriate, give the user more information // to be able to fall back to manual intervention if possible. // TODO: Consumer has no idea if this succeeded or not debug(err); } } } // TODO: Should either retry, or not throw in any of these await Promise.all([ deleteQueue(context), deregisterTaskDefinition(context), gcQueues(context) ]); } catch (err) { artillery.log(err); } } function checkFargateResourceConfig(cpu, memory) { function generateListOfOptionsMiB(minGB, maxGB, incrementGB) { const result = []; for (let i = 0; i <= (maxGB - minGB) / incrementGB; i++) { result.push((minGB + incrementGB * i) * 1024); } return result; } // Based on https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html const FARGATE_VALID_CONFIGS = { 256: [512, 1024, 2048], 512: [1024, 2048, 3072, 4096], 1024: [2048, 3072, 4096, 5120, 6144, 7168, 8192], 2048: generateListOfOptionsMiB(4, 16, 1), 4096: generateListOfOptionsMiB(8, 30, 1), 8192: generateListOfOptionsMiB(16, 60, 4), 16384: generateListOfOptionsMiB(32, 120, 8) }; if (!FARGATE_VALID_CONFIGS[cpu]) { return new Error( `Unsupported cpu override for Fargate. Must be one of: ${Object.keys( FARGATE_VALID_CONFIGS ).join(', ')}` ); } if (FARGATE_VALID_CONFIGS[cpu].indexOf(memory) < 0) { return new Error( `Fargate memory override for cpu = ${cpu} must be one of: ${FARGATE_VALID_CONFIGS[ cpu ].join(', ')}` ); } return null; } async function createArtilleryCluster(context) { const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); await ecs.send( new CreateClusterCommand({ clusterName: ARTILLERY_CLUSTER_NAME, capacityProviders: ['FARGATE_SPOT'] }) ); let retries = 0; while (retries < 12) { const clusterActive = await checkTargetCluster(context); if (clusterActive) { break; } retries++; await sleep(10 * 1000); } } // // Check that ECS cluster exists: // async function checkTargetCluster(context) { const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); try { const response = await ecs.send( new DescribeClustersCommand({ clusters: [context.clusterName] }) ); debug(response); if (response.clusters.length === 0 || response.failures.length > 0) { debugVerbose(response); return false; } else { const activeClusters = response.clusters.filter( (c) => c.status === 'ACTIVE' ); return activeClusters.length > 0; } } catch (err) { debugVerbose(err); return false; } } async function maybeGetSubnetIdsForFargate(context) { if (!context.isFargate) { return context; } // TODO: Sanity check that subnets actually exist before trying to use them in test definitions if (context.fargatePublicSubnetIds.length > 0) { return context; } debug('Subnet IDs not provided, looking up default VPC'); const f = new VPCSubnetFinder({ region: context.region }); const publicSubnets = await f.findPublicSubnets(); if (publicSubnets.length === 0) { throw new Error('Could not find public subnets in default VPC'); } context.fargatePublicSubnetIds = publicSubnets.map((s) => s.SubnetId); debug('Found public subnets:', context.fargatePublicSubnetIds.join(', ')); return context; } async function createTestBundle(context) { const result = await createTest(context.scriptPath, { name: context.testId, config: context.cliOptions.config, packageJsonPath: context.packageJsonPath, flags: context.cliOptions }); context.fullyResolvedConfig = result.manifest.fullyResolvedConfig; return context; } async function createADOTDefinitionIfNeeded(context) { const publishMetricsConfig = context.fullyResolvedConfig.plugins?.['publish-metrics']; if (!publishMetricsConfig) { debug('No publish-metrics plugin set, skipping ADOT configuration'); return context; } const adotRelevantConfigs = getADOTRelevantReporterConfigs(publishMetricsConfig); if (adotRelevantConfigs.length === 0) { debug('No ADOT relevant reporter configs set, skipping ADOT configuration'); return context; } try { const { adotEnvVars, adotConfig } = resolveADOTConfigSettings({ configList: adotRelevantConfigs, dotenv: { ...context.dotenv } }); context.dotenv = Object.assign(context.dotenv || {}, adotEnvVars); context.adot = { SSMParameterPath: `/artilleryio/OTEL_CONFIG_${context.testId}` }; await awsUtil.putParameter( context.adot.SSMParameterPath, JSON.stringify(adotConfig), 'String', context.region ); context.adot.taskDefinition = { name: 'adot-collector', image: 'amazon/aws-otel-collector:v0.39.0', command: [ '--config=/etc/ecs/container-insights/otel-task-metrics-config.yaml' ], secrets: [ { name: 'AOT_CONFIG_CONTENT', valueFrom: `${context.arnPrefix}:ssm:${context.region}:${context.accountId}:parameter${context.adot.SSMParameterPath}` } ], logConfiguration: { logDriver: 'awslogs', options: { 'awslogs-group': `${context.logGroupName}/${context.clusterName}`, 'awslogs-region': context.region, 'awslogs-stream-prefix': `artilleryio/${context.testId}`, 'awslogs-create-group': 'true' } } }; } catch (err) { throw new Error(err); } return context; } async function ensureTaskExists(context) { const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); // Note: these are integers for container definitions, and strings for task definitions (on Fargate) // Defaults have to be Fargate-compatible // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size let cpu = 4096; let memory = 8192; const defaultUlimits = { nofile: { softLimit: 8192, hardLimit: 8192 } }; let ulimits = []; if (context.cliOptions.launchConfig) { const lc = context.cliOptions.launchConfig; if (lc.cpu) { cpu = parseInt(lc.cpu, 10); } if (lc.memory) { memory = parseInt(lc.memory, 10); } if (lc.ulimits) { lc.ulimits.forEach((u) => { if (!defaultUlimits[u.name]) { defaultUlimits[u.name] = {}; } defaultUlimits[u.name] = { softLimit: u.softLimit, hardLimit: typeof u.hardLimit === 'number' ? u.hardLimit : u.softLimit }; }); } // TODO: Check this earlier to return an error faster. if (context.isFargate) { const configErr = checkFargateResourceConfig(cpu, memory); if (configErr) { throw configErr; } } } ulimits = Object.keys(defaultUlimits).map((name) => { return { name: name, softLimit: defaultUlimits[name].softLimit, hardLimit: defaultUlimits[name].hardLimit }; }); const defaultArchitecture = 'x86_64'; const imageUrl = process.env.WORKER_IMAGE_URL || `public.ecr.aws/d8a4z9o5/artillery-worker:${IMAGE_VERSION}-${defaultArchitecture}`; const secrets = [ 'NPM_TOKEN', 'NPM_REGISTRY', 'NPM_SCOPE', 'NPM_SCOPE_REGISTRY', 'NPMRC', 'ARTIFACTORY_AUTH', 'ARTIFACTORY_EMAIL' ] .concat(context.extraSecrets) .map((secretName) => { return { name: secretName, valueFrom: `${context.arnPrefix}:ssm:${context.region}:${context.accountId}:parameter/artilleryio/${secretName}` }; }); const artilleryContainerDefinition = { name: 'artillery', image: imageUrl, cpu: cpu, command: [], entryPoint: ['/artillery/loadgen-worker'], memory: memory, secrets: secrets, ulimits: ulimits, essential: true, logConfiguration: { logDriver: 'awslogs', options: { 'awslogs-group': `${context.logGroupName}/${context.clusterName}`, 'awslogs-region': context.region, 'awslogs-stream-prefix': `artilleryio/${context.testId}`, 'awslogs-create-group': 'true', mode: 'non-blocking' } } }; if (context.cliOptions.containerDnsServers) { artilleryContainerDefinition.dnsServers = context.cliOptions.containerDnsServers.split(','); } const taskDefinition = { family: context.taskName, containerDefinitions: [artilleryContainerDefinition], executionRoleArn: context.taskRoleArn }; if (typeof context.adot !== 'undefined') { taskDefinition.containerDefinitions.push(context.adot.taskDefinition); } context.taskDefinition = taskDefinition; if (!context.isFargate && taskDefinition.containerDefinitions.length > 1) { // Limits for sidecar have to be set explicitly on ECS EC2 taskDefinition.containerDefinitions[1].memory = 1024; taskDefinition.containerDefinitions[1].cpu = 1024; } if (context.isFargate) { taskDefinition.networkMode = 'awsvpc'; taskDefinition.requiresCompatibilities = ['FARGATE']; taskDefinition.cpu = String(cpu); taskDefinition.memory = String(memory); // NOTE: This role must exist. // This value cannot be an override, meaning it's hardcoded into the task definition. // That in turn means that if the role is updated then the task definition needs to be // recreated too taskDefinition.executionRoleArn = context.taskRoleArn; // TODO: A separate role for Fargate } const params = { taskDefinition: context.taskName }; debug('Task definition\n', JSON.stringify(taskDefinition, null, 4)); try { await ecs.send(new DescribeTaskDefinitionCommand(params)); debug('OK: ECS task exists'); if (process.env.ECR_IMAGE_VERSION) { debug( 'ECR_IMAGE_VERSION is set, but the task definition was already in place.' ); } return context; } catch (_err) { try { const response = await ecs.send( new RegisterTaskDefinitionCommand(taskDefinition) ); debug('OK: ECS task registered'); debugVerbose(JSON.stringify(response, null, 4)); context.taskDefinitionArn = response.taskDefinition.taskDefinitionArn; debug(`Task definition ARN: ${context.taskDefinitionArn}`); return context; } catch (registerErr) { artillery.log(registerErr); artillery.log('Could not create ECS task, please try again'); throw registerErr; } } } async function checkCustomTaskRole(context) { if (!context.customTaskRoleName) { return; } const iam = new IAMClient({ region: global.artillery.awsRegion }); const roleData = await iam.send( new GetRoleCommand({ RoleName: context.customTaskRoleName }) ); context.customRoleArn = roleData.Role.Arn; context.taskRoleArn = roleData.Role.Arn; debug(roleData); } async function gcQueues(context) { const sqs = new SQSClient({ region: context.region }); let data; try { data = await sqs.send( new ListQueuesCommand({ QueueNamePrefix: SQS_QUEUES_NAME_PREFIX, MaxResults: 1000 }) ); } catch (err) { debug(err); } if (data?.QueueUrls && data.QueueUrls.length > 0) { for (const qu of data.QueueUrls) { try { const data = await sqs.send( new GetQueueAttributesCommand({ QueueUrl: qu, AttributeNames: ['CreatedTimestamp'] }) ); const ts = Number(data.Attributes.CreatedTimestamp) * 1000; // Delete after 96 hours if (Date.now() - ts > 96 * 60 * 60 * 1000) { await sqs.send(new DeleteQueueCommand({ QueueUrl: qu })); } } catch (err) { // TODO: Filter on errors which may be ignored, e.g.: // AWS.SimpleQueueService.NonExistentQueue: The specified queue does not exist // which can happen if another test ends between calls to listQueues and getQueueAttributes. // Sometimes SQS returns recently deleted queues to ListQueues too. debug(err); } } } } async function deleteQueue(context) { if (!context.sqsQueueUrl) { return; } const sqs = new SQSClient({ region: context.region }); try { await sqs.send(new DeleteQueueCommand({ QueueUrl: context.sqsQueueUrl })); } catch (err) { console.error(`Unable to clean up SQS queue. URL: ${context.sqsQueueUrl}`); debug(err); } } async function createQueue(context) { const sqs = new SQSClient({ region: context.region }); const queueName = `${SQS_QUEUES_NAME_PREFIX}_${context.testId.slice( 0, 30 )}.fifo`; const params = { QueueName: queueName, Attributes: { FifoQueue: 'true', ContentBasedDeduplication: 'false', MessageRetentionPeriod: '1800', VisibilityTimeout: '600' // 10 minutes } }; const result = await sqs.send(new CreateQueueCommand(params)); context.sqsQueueUrl = result.QueueUrl; // Wait for the queue to be available: let waited = 0; let ok = false; while (waited < 120 * 1000) { try { const results = await sqs.send( new ListQueuesCommand({ QueueNamePrefix: queueName }) ); if (results.QueueUrls && results.QueueUrls.length === 1) { debug('SQS queue created:', queueName); ok = true; break; } else { await sleep(10 * 1000); waited += 10 * 1000; } } catch (_err) { await sleep(10 * 1000); waited += 10 * 1000; } } if (!ok) { debug('Time out waiting for SQS queue:', queueName); throw new Error('SQS queue could not be created'); } } async function getManifest(context) { try { const s3 = createS3Client({ region: global.artillery.s3BucketRegion }); const params = { Bucket: context.s3Bucket, Key: `tests/${context.testId}/metadata.json` }; const data = await s3.send(new GetObjectCommand(params)); const metadata = JSON.parse(await data.Body.transformToString()); context.newScriptPath = metadata.scriptPath; if (metadata.configPath) { context.configPath = metadata.configPath; } return context; } catch (err) { if (err instanceof NoSuchKey) { throw new TestNotFoundError(); } else { throw err; } } } async function generateTaskOverrides(context) { const cliArgs = ['run'].concat( context.cliOptions.environment ? ['--environment', context.cliOptions.environment] : [], context.cliOptions['scenarioName'] ? ['--scenario-name', context.cliOptions['scenarioName']] : [], context.cliOptions.insecure ? ['-k'] : [], context.cliOptions.target ? ['-t', context.cliOptions.target] : [], context.cliOptions.overrides ? ['--overrides', context.cliOptions.overrides] : [], context.cliOptions.variables ? ['--variables', context.cliOptions.variables] : [], context.configPath ? ['--config', context.configPath] : [] ); // NOTE: This MUST come last: cliArgs.push(context.newScriptPath); debug('cliArgs', cliArgs, cliArgs.join(' ')); const s3path = `s3://${context.s3Bucket}/tests/${ context.namedTest ? context.s3Prefix : context.testId }`; const adotOverride = [ { name: 'adot-collector', environment: [] } ]; const overrides = { containerOverrides: [ { name: 'artillery', command: [ '-p', s3path, '-a', util.btoa(JSON.stringify(cliArgs)), '-r', context.region, '-q', process.env.SQS_QUEUE_URL || context.sqsQueueUrl, '-i', context.testId, '-d', `s3://${context.s3Bucket}/test-runs`, '-t', String(WAIT_TIMEOUT) ], environment: [ { name: 'AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE', value: '1' }, { name: 'ARTILLERY_TEST_RUN_ID', value: global.artillery.testRunId }, { name: 'ARTILLERY_S3_BUCKET', value: context.s3Bucket } ] }, ...(context.adot ? adotOverride : []) ], taskRoleArn: context.taskRoleArn }; if (context.customRoleArn) { overrides.taskRoleArn = context.customRoleArn; } if (context.cliOptions.taskEphemeralStorage) { overrides.ephemeralStorage = { sizeInGiB: context.cliOptions.taskEphemeralStorage }; } overrides.containerOverrides[0].environment.push({ name: 'USE_V2', value: 'true' }); if (context.dotenv) { const extraEnv = []; for (const [name, value] of Object.entries(context.dotenv)) { extraEnv.push({ name, value }); } overrides.containerOverrides[0].environment = overrides.containerOverrides[0].environment.concat(extraEnv); if (overrides.containerOverrides[1]) { overrides.containerOverrides[1].environment = overrides.containerOverrides[1].environment.concat(extraEnv); } } if (context.cliOptions.launchConfig) { const lc = context.cliOptions.launchConfig; if (lc.environment) { overrides.containerOverrides[0].environment = overrides.containerOverrides[0].environment.concat(lc.environment); if (overrides.containerOverrides[1]) { overrides.containerOverrides[1].environment = overrides.containerOverrides[1].environment.concat(lc.environment); } } // // Not officially supported: // if (lc.taskRoleArn) { overrides.taskRoleArn = lc.taskRoleArn; } if (lc.command) { overrides.containerOverrides[0].command = lc.command; } } debug('OK: Overrides generated'); debugVerbose(JSON.stringify(overrides, null, 4)); context.taskOverrides = overrides; return context; } async function setupDefaultECSParams(context) { const defaultParams = { taskDefinition: context.taskName, cluster: context.clusterName, overrides: context.taskOverrides, startedBy: context.testId }; if (context.isFargate) { if (context.isCapacitySpot) { defaultParams.capacityProviderStrategy = [ { capacityProvider: 'FARGATE_SPOT', weight: 1, base: 0 } ]; } else { // On-demand capacity defaultParams.launchType = 'FARGATE'; } // Networking config: private subnets of the VPC that the ECS cluster // is in. Don't need public subnets. defaultParams.networkConfiguration = { awsvpcConfiguration: { // https://github.com/aws/amazon-ecs-agent/issues/1128 assignPublicIp: context.cliOptions.noAssignPublicIp ? 'DISABLED' : 'ENABLED', securityGroups: context.fargateSecurityGroupIds, subnets: context.fargatePublicSubnetIds } }; } else { defaultParams.launchType = 'EC2'; } context.defaultECSParams = defaultParams; return context; } async function launchLeadTask(context) { const metadata = { testId: context.testId, startedAt: Date.now(), cluster: context.clusterName, region: context.region, launchType: context.cliOptions.launchType, isFargateSpot: context.isCapacitySpot, count: context.count, sqsQueueUrl: context.sqsQueueUrl, tags: context.tags, secrets: JSON.stringify( Array.isArray(context.extraSecrets) ? context.extraSecrets : [context.extraSecrets] ), platformConfig: JSON.stringify({ memory: context.taskDefinition.containerDefinitions[0].memory, cpu: context.taskDefinition.containerDefinitions[0].cpu }), artilleryVersion: JSON.stringify({ core: global.artillery.version }), // Properties from the runnable script object: testConfig: { target: context.runnableScript.config.target, phases: context.runnableScript.config.phases, plugins: context.runnableScript.config.plugins, environment: context.runnableScript._environment, scriptPath: context.runnableScript._scriptPath, configPath: context.runnableScript._configPath } }; artillery.globalEvents.emit('metadata', metadata); context.status = TEST_RUN_STATUS.LAUNCHING_WORKERS; const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); const leaderParams = Object.assign( { count: 1 }, JSON.parse(JSON.stringify(context.defaultECSParams)) ); leaderParams.overrides.containerOverrides[0].environment.push({ name: 'IS_LEADER', value: 'true' }); const runData = await ecs.send(new RunTaskCommand(leaderParams)); if (runData.failures.length > 0) { if (runData.failures.length === context.count) { artillery.log('ERROR: Worker start failure'); const uniqueReasons = [ ...new Set(runData.failures.map((f) => f.reason)) ]; artillery.log('Reason:', uniqueReasons); throw new Error('Could not start workers'); } else { artillery.log('WARNING: Some workers failed to start'); artillery.log(chalk.red(JSON.stringify(runData.failures, null, 4))); throw new Error('Not enough capacity - terminating'); } } context.taskArns = context.taskArns.concat( runData.tasks.map((task) => task.taskArn) ); artillery.globalEvents.emit('metadata', { platformMetadata: { taskArns: context.taskArns } }); return context; } // TODO: When launching >20 containers on Fargate, adjust WAIT_TIMEOUT dynamically to // add extra time spent in waiting between runTask calls: WAIT_TIMEOUT + worker_count. async function ecsRunTask(context) { const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); let tasksRemaining = context.count - 1; let retries = 0; while ( tasksRemaining > 0 && context.status !== TEST_RUN_STATUS.TERMINATING && context.status !== TEST_RUN_STATUS.EARLY_STOP ) { if (retries >= 10) { artillery.log('Max retries for ECS (10) exceeded'); throw new Error('Max retries exceeded'); } const launchCount = tasksRemaining <= 10 ? tasksRemaining : 10; const params = Object.assign( { count: launchCount }, JSON.parse(JSON.stringify(context.defaultECSParams)) ); params.overrides.containerOverrides[0].environment.push({ name: 'IS_LEADER', value: 'false' }); try { const runData = await ecs.send(new RunTaskCommand(params)); const launchedTasksCount = runData.tasks?.length || 0; tasksRemaining -= launchedTasksCount; if (launchedTasksCount > 0) { const newTaskArns = runData.tasks.map((task) => task.taskArn); context.taskArns = context.taskArns.concat(newTaskArns); artillery.globalEvents.emit('metadata', { platformMetadata: { taskArns: newTaskArns } }); debug(`Launched ${launchedTasksCount} tasks`); } if (runData.failures.length > 0) { artillery.log('Some workers failed to start'); const uniqueReasons = [ ...new Set(runData.failures.map((f) => f.reason)) ]; artillery.log(chalk.red(uniqueReasons)); artillery.log('Retrying...'); await sleep(10 * 1000); throw new Error('Not enough ECS capacity'); } } catch (runErr) { if (runErr instanceof ThrottlingException) { artillery.log('ThrottlingException returned from ECS, retrying'); await sleep(2000 * retries); debug('runTask throttled, retrying'); debug(runErr); } else if (runErr.message.match(/Not enough ECS capacity/gi)) { // Do nothing } else { artillery.log(runErr); } retries++; if (retries >= 10) { artillery.log('Max retries for ECS (10) exceeded'); throw runErr; } } } return context; } async function waitForTasks2(context) { const params = { tasks: context.taskArns, cluster: context.clusterName }; let failedTasks = []; let stoppedTasks = []; let maybeErr = null; const silentWaitTimeout = new Timeout(30 * 1000).start(); // wait this long before updating the user const waitTimeout = new Timeout(60 * 1000).start(); // wait for up to 1 minute while (context.status !== TEST_RUN_STATUS.TERMINATING) { let ecsData; try { ecsData = await awsUtil.ecsDescribeTasks(params, context.region); } catch (err) { // TODO: Inspect err for any conditions in which we may want to abort immediately. // Otherwise, let the timeout run to completion. debug(err); await sleep(5000); continue; } // All tasks are RUNNING, proceed: if (_.every(ecsData.tasks, (s) => s.lastStatus === 'RUNNING')) { logProgress('All workers started...'); debug('All tasks in RUNNING state'); break; } // If there are STOPPED tasks, we need to stop: stoppedTasks = ecsData.tasks.filter((t) => t.lastStatus === 'STOPPED'); if (stoppedTasks.length > 0) { debug('Some tasks in STOPPED state'); debugErr(stoppedTasks); // TODO: Stop RUNNING tasks and clean up (release queue lock, deregister task definition) // TODO: Provide more information here, e.g. task ARNs, or CloudWatch log group ID maybeErr = new Error('Worker init failure, aborting test'); break; } // If some tasks failed to start altogether, abort: if (ecsData.failures.length > 0) { failedTasks = ecsData.failures; debug('Some tasks failed to start'); debugErr(ecsData.failures); maybeErr = new Error('Worker start up failure, aborting test'); break; } // If there are PENDING, update progress bar debug('Waiting on pending tasks'); if (silentWaitTimeout.timedout()) { const statusCounts = _.countBy(ecsData.tasks, 'lastStatus'); const statusSummary = _.map(statusCounts, (count, status) => { const displayStatus = status === 'RUNNING' ? 'ready' : status.toLowerCase(); let displayStatusChalked = displayStatus; if (displayStatus === 'ready') { displayStatusChalked = chalk.green(displayStatus); } else if (displayStatus === 'pending') { displayStatusChalked = chalk.yellow(displayStatus); } return `${displayStatusChalked}: ${count}`; }).join(' / '); logProgress(`Waiting for workers to start: ${statusSummary}`); } if (waitTimeout.timedout()) { // TODO: Clean up RUNNING tasks etc break; } await sleep(10 * 1000); } // while waitTimeout.stop(); if (maybeErr) { if (stoppedTasks.length > 0) { artillery.log(stoppedTasks); } if (failedTasks.length > 0) { artillery.log(failedTasks); } throw maybeErr; } return context; } async function waitForWorkerSync(context) { const MAGIC_PREFIX = 'synced_'; const prefix = `test-runs/${context.testId}/${MAGIC_PREFIX}`; const intervalSec = 10; const times = WAIT_TIMEOUT / intervalSec; let attempts = 0; let synced = false; while (attempts < times) { try { const objects = await util.listAllObjectsWithPrefix( context.s3Bucket, prefix ); if (objects.length !== context.count) { attempts++; } else { synced = true; break; } } catch (_err) { attempts++; } await sleep(intervalSec * 1000); } if (synced) { return context; } else { throw new Error('Timed out waiting for worker sync'); } } async function sendGoSignal(context) { const s3 = createS3Client(); const params = { Body: Buffer.from(context.testId), Bucket: context.s3Bucket, Key: `test-runs/${context.testId}/go.json` }; await s3.send(new PutObjectCommand(params)); return context; } async function writeHeartbeat(context) { const s3 = createS3Client(); const params = { Body: Buffer.from(String(Date.now())), Bucket: context.s3Bucket, Key: `test-runs/${context.testId}/heartbeat.json` }; try { await s3.send(new PutObjectCommand(params)); debug('Heartbeat written: %s', params.Body.toString()); } catch (err) { debug('Heartbeat write failed: %s', err.message); // Non-fatal. Workers tolerate missed heartbeats via 180s threshold. } } function startHeartbeat(context) { writeHeartbeat(context).catch(debug); const intervalId = setInterval(() => { writeHeartbeat(context).catch(debug); }, 60 * 1000); return intervalId; } async function listen(context, ee) { return new Promise((resolve, _reject) => { context.intermediateReports = []; context.aggregateReport = null; const r = new SqsReporter(context); context.sqsReporter = r; r.on('workersDone', (state) => { ee.emit('workersDone', state); return resolve(context); }); r.on('done', (stats) => { if (stats.report) { context.aggregateReport = stats.report(); } else { context.aggregateReport = stats; } global.artillery.globalEvents.emit('done', stats); ee.emit('done', stats); }); r.on('error', (err) => { // Ignore SQS errors // ee.emit('error', err); // return reject(err); debug(err); }); r.on('workerDone', (body, attrs) => { if (process.env.LOG_WORKER_MESSAGES) { artillery.log( chalk.green( `[${attrs.workerId.StringValue} ${JSON.stringify(body, null, 4)}]` ) ); } }); r.on('workerError', (body, attrs) => { if (process.env.LOG_WORKER_MESSAGES) { artillery.log( chalk.red( `[${attrs.workerId.StringValue} ${JSON.stringify(body, null, 4)}]` ) ); } if (body.exitCode !== 21) { artillery.log( chalk.yellow( `Worker exited with an error, worker ID = ${attrs.workerId.StringValue}` ) ); } // TODO: Copy log over and print path to log file so that user may inspect it - in a temporary location global.artillery.suggestedExitCode = body.exitCode || 1; }); r.on('workerMessage', (body, attrs) => { if (process.env.LOG_WORKER_MESSAGES) { artillery.log( chalk.yellow( `[${attrs.workerId.StringValue}] ${body.msg} ${body.type}` ) ); } if (body.type === 'stopped') { if (context.status !== TEST_RUN_STATUS.EARLY_STOP) { artillery.log('Test run has been requested to stop'); } context.status = TEST_RUN_STATUS.EARLY_STOP; } if (body.type === 'ensure') { try { context.ensureSpec = JSON.parse(util.atob(body.msg)); } catch (_parseErr) { console.error('Error processing ensure directive'); } } if (body.type === 'leader' && body.msg === 'prepack_end') { ee.emit('prepack_end'); } }); r.on('stats', async (stats) => { let report; if (stats.report) { report = stats.report(); context.intermediateReports.push(report); } else { context.intermediateReports.push(stats); report = stats; } global.artillery.globalEvents.emit('stats', stats); ee.emit('stats', stats); }); r.on('phaseStarted', (phase) => { global.artillery.globalEvents.emit('phaseStarted', phase); }); r.on('phaseCompleted', (phase) => { global.artillery.globalEvents.emit('phaseCompleted', phase); }); r.start(); }); } async function deregisterTaskDefinition(context) { if (!context.taskDefinitionArn) { return; } const ecs = new ECSClient({ apiVersion: '2014-11-13', region: context.region }); try { await ecs.send( new DeregisterTaskDefinitionCommand({ taskDefinition: context.taskDefinitionArn }) ); debug(`Deregistered ${context.taskDefinitionArn}`); } catch (err) { artillery.log(err); debug(err); } return context; } // TODO: Remove - duplicated in run.js function getLogFilename(output, userDefaultFilenameFormat) { let logfile; // is the destination a directory that exists? let isDir = false; if (output) { try { isDir = fs.statSync(output).isDirectory(); } catch (_err) { // ENOENT, don't need to do anything } } const defaultFormat = '[artillery_report_]YMMDD_HHmmSS[.json]'; if (!isDir && output) { // -o is set with a filename (existing or not) logfile = output; } else if (!isDir && !output) { // no -o set } else { // -o is set with a directory logfile = path.join( output, moment().format(userDefaultFilenameFormat || defaultFormat) ); } return logfile; } ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/sqs-reporter.js ================================================ const EventEmitter = require('node:events'); const { Consumer } = require('sqs-consumer'); const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); const driftless = require('driftless'); const debug = require('debug')('sqs-reporter'); const debugV = require('debug')('sqs-reporter:v'); const _ = require('lodash'); class SqsReporter extends EventEmitter { constructor(opts) { super(); this.sqsQueueUrl = opts.sqsQueueUrl; this.region = opts.region; this.testId = opts.testId; this.count = opts.count; this.periodsReportedFor = []; this.ee = new EventEmitter(); this.workerState = {}; this.lastIntermediateReportAt = 0; this.taskWatcher = null; this.metricsByPeriod = {}; // individual intermediates by worker this.mergedPeriodMetrics = []; // merged intermediates for a period //TODO: this code is repeated from `launch-platform.js` - refactor later this.phaseStartedEventsSeen = {}; this.phaseCompletedEventsSeen = {}; // Debug info: this.messagesProcessed = {}; this.metricsMessagesFromWorkers = {}; this.poolSize = typeof process.env.SQS_CONSUMER_POOL_SIZE !== 'undefined' ? parseInt(process.env.SQS_CONSUMER_POOL_SIZE, 10) : Math.max(Math.ceil(this.count / 10), 75); this.s3 = null; this.s3Bucket = process.env.ARTILLERY_S3_BUCKET || null; if (this.s3Bucket) { this.s3 = new S3Client({ region: opts.region }); } } _allWorkersDone() { return Object.keys(this.workerState).length === this.count; } async _fetchFromS3(s3Key) { const response = await this.s3.send( new GetObjectCommand({ Bucket: this.s3Bucket, Key: s3Key }) ); return JSON.parse(await response.Body.transformToString()); } stop() { debug('stopping'); for (const sqsConsumer of this.sqsConsumers) { sqsConsumer.stop(); } } start() { debug('starting'); this.sqsDebugInterval = driftless.setDriftlessInterval(() => { debug(this.messagesProcessed); let total = 0; for (const [_k, v] of Object.entries(this.messagesProcessed)) { total += v; } debug('total:', total); }, 10 * 1000); this.intermediateReporterInterval = driftless.setDriftlessInterval(() => { if (Object.keys(this.metricsByPeriod).length === 0) { return; // nothing received yet } // We always look at the earliest period available so that reports come in chronological order const earliestPeriodAvailable = Object.keys(this.metricsByPeriod) .filter((x) => this.periodsReportedFor.indexOf(x) === -1) .sort()[0]; // TODO: better name. One above is earliestNotAlreadyReported const earliest = Object.keys(this.metricsByPeriod).sort()[0]; if (this.periodsReportedFor.indexOf(earliest) > -1) { global.artillery.log( 'Warning: multiple batches of metrics for period', earliest, new Date(Number(earliest)) ); delete this.metricsByPeriod[earliest]; // FIXME: need to merge them in for the final report } // We can process SQS messages in batches of 10 at a time, so // when there are more workers, we need to wait longer: const MAX_WAIT_FOR_PERIOD_MS = (Math.ceil(this.count / 10) * 2 + 20) * 1000; if ( typeof earliestPeriodAvailable !== 'undefined' && (this.metricsByPeriod[earliestPeriodAvailable].length === this.count || Date.now() - Number(earliestPeriodAvailable) > MAX_WAIT_FOR_PERIOD_MS) ) { // TODO: autoscaling. Handle workers that drop off as the first case - self.count needs to be updated dynamically debug( 'have metrics from all workers for period or MAX_WAIT_FOR_PERIOD reached', earliestPeriodAvailable ); debug( 'Report @', new Date(Number(earliestPeriodAvailable)), 'made up of items:', this.metricsByPeriod[String(earliestPeriodAvailable)].length ); // TODO: Track how many workers provided metrics in the metrics report const stats = global.artillery.__SSMS.mergeBuckets( this.metricsByPeriod[String(earliestPeriodAvailable)] )[String(earliestPeriodAvailable)]; this.mergedPeriodMetrics.push(stats); // summarize histograms for console reporter stats.summaries = {}; for (const [name, value] of Object.entries(stats.histograms || {})) { const summary = global.artillery.__SSMS.summarizeHistogram(value); stats.summaries[name] = summary; delete this.metricsByPeriod[String(earliestPeriodAvailable)]; } this.periodsReportedFor.push(earliestPeriodAvailable); debug('Emitting stats event'); this.emit('stats', stats); } else { debug('Waiting for more workerStats before emitting stats event'); } }, 5 * 1000); this.workersDoneWatcher = driftless.setDriftlessInterval(() => { if (!this._allWorkersDone()) { return; } // Have we received and processed all intermediate metrics? if (Object.keys(this.metricsByPeriod).length > 0) { debug( 'All workers done but still waiting on some intermediate reports' ); return; } debug('ready to emit done event'); debug('mergedPeriodMetrics'); debug(this.mergedPeriodMetrics); // Merge by period, then compress and emit const stats = global.artillery.__SSMS.pack(this.mergedPeriodMetrics); stats.summaries = {}; for (const [name, value] of Object.entries(stats.histograms || {})) { const summary = global.artillery.__SSMS.summarizeHistogram(value); stats.summaries[name] = summary; } if (process.env.DEBUG === 'sqs-reporter:v') { for (const [workerId, metrics] of Object.entries( this.metricsMessagesFromWorkers )) { debugV('worker', workerId, '->', metrics.length, 'items'); } // fs.writeFileSync('worker-metrics-dump.json', JSON.stringify(self.metricsMessagesFromWorkers)); } this.emit('done', stats); driftless.clearDriftless(this.intermediateReporterInterval); driftless.clearDriftless(this.workersDoneWatcher); driftless.clearDriftless(this.sqsDebugInterval); for (const sqsConsumer of this.sqsConsumers) { sqsConsumer.stop(); } this.emit('workersDone', this.workerState); }, 5 * 1000); this.ee.on('message', (body, attrs) => { const workerId = attrs.workerId?.StringValue; if (!workerId) { debug('Got message with no workerId'); debug(body); return; } if (body.event === 'workerDone' || body.event === 'workerError') { this.workerState[workerId] = body.event; this.emit(body.event, body, attrs); debug(workerId, body.event); return; } //TODO: this code is repeated from `launch-platform.js` - refactor later if (body.event === 'phaseStarted') { if ( typeof this.phaseStartedEventsSeen[body.phase.index] === 'undefined' ) { this.phaseStartedEventsSeen[body.phase.index] = Date.now(); this.emit(body.event, body.phase); } return; } //TODO: this code is repeated from `launch-platform.js` - refactor later if (body.event === 'phaseCompleted') { if ( typeof this.phaseCompletedEventsSeen[body.phase.index] === 'undefined' ) { this.phaseCompletedEventsSeen[body.phase.index] = Date.now(); this.emit(body.event, body.phase); } return; } // 'done' event is from SQS Plugin - unused for now if (body.event === 'done') { return; } if (body.msg) { this.emit('workerMessage', body, attrs); return; } if (body.event === 'workerStats') { // v2 SSMS stats const workerStats = global.artillery.__SSMS.deserializeMetrics( body.stats ); const period = workerStats.period; debug( 'processing workerStats event, worker:', workerId, 'period', period ); debugV(workerStats); if (typeof this.metricsByPeriod[period] === 'undefined') { this.metricsByPeriod[period] = []; } this.metricsByPeriod[period].push(workerStats); if (process.env.DEBUG === 'sqs-reporter:v') { if ( typeof this.metricsMessagesFromWorkers[workerId] === 'undefined' ) { this.metricsMessagesFromWorkers[workerId] = []; } this.metricsMessagesFromWorkers[workerId].push(workerStats); } debugV('metricsByPeriod:'); debugV(this.metricsByPeriod); debug('number of periods processed'); debug(Object.keys(this.metricsByPeriod)); debug('number of metrics collections for period:', period, ':'); debug(this.metricsByPeriod[period].length, 'expecting:', this.count); } }); this.ee.on('messageReceiveTimeout', () => { // TODO: 10 polls with no results, e.g. if all workers crashed }); const createConsumer = (i) => Consumer.create({ queueUrl: process.env.SQS_QUEUE_URL || this.sqsQueueUrl, region: this.region, waitTimeSeconds: 10, messageAttributeNames: ['testId', 'workerId'], visibilityTimeout: 60, batchSize: 10, handleMessage: async (message) => { let body = null; try { body = JSON.parse(message.Body); } catch (err) { console.error(err); console.log(message.Body); } // // Ignore any messages that are invalid or not tagged properly. // if (process.env.LOG_SQS_MESSAGES) { console.log(message); } if (!body) { throw new Error(); } // Handle overflow messages stored in S3 if (body._overflowRef && this.s3 && this.s3Bucket) { try { debug('Fetching overflow payload from S3: %s', body._overflowRef); body = await this._fetchFromS3(body._overflowRef); } catch (s3Err) { console.error('Failed to fetch overflow message from S3:', s3Err); throw new Error( `Failed to fetch overflow message: ${body._overflowRef}` ); } } const attrs = message.MessageAttributes; if (!attrs || !attrs.testId) { throw new Error(); } if (this.testId !== attrs.testId.StringValue) { throw new Error(); } if (!this.messagesProcessed[i]) { this.messagesProcessed[i] = 0; } this.messagesProcessed[i] += 1; process.nextTick(() => { this.ee.emit('message', body, attrs); }); } }); this.sqsConsumers = []; for (let i = 0; i < this.poolSize; i++) { const sqsConsumer = createConsumer(i); sqsConsumer.on('error', (err) => { // TODO: Ignore "SQSError: SQS delete message failed:" errors if (err.message?.match(/ReceiptHandle.+expired/i)) { debug(err.name, err.message); } else { artillery.log(err); sqsConsumer.stop(); this.emit('error', err); } }); let empty = 0; sqsConsumer.on('empty', () => { empty++; if (empty > 10) { this.ee.emit('messageReceiveTimeout'); // TODO: } }); sqsConsumer.start(); this.sqsConsumers.push(sqsConsumer); } } // Given a (combined) stats object, what's the difference between the // time of earliest and latest requests made? calculateSpread(stats) { const period = _.reduce( stats._requestTimestamps, (acc, ts) => { acc.min = Math.min(acc.min, ts); acc.max = Math.max(acc.max, ts); return acc; }, { min: Infinity, max: 0 } ); const spread = round((period.max - period.min) / 1000, 1); return spread; } } function round(number, decimals) { const m = 10 ** decimals; return Math.round(number * m) / m; } module.exports = { SqsReporter }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/tags.js ================================================ function parseTags(input) { const tags = []; if (input) { const tagList = input.split(',').map((x) => x.trim()); for (const t of tagList) { const cs = t.split(':'); if (cs.length !== 2) { console.error(`Invalid tag, skipping: ${t}`); } else { tags.push({ name: cs[0].trim(), value: cs[1].trim() }); } } } return tags; } module.exports = { parseTags }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/test-run-status.js ================================================ module.exports = { INITIALIZING: 1, LAUNCHING_WORKERS: 2, RECEIVING_REPORTS: 3, TERMINATING: 4, EARLY_STOP: 5, COMPLETED: 6, ERROR: 7 }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/time.js ================================================ const EventEmitter = require('node:events'); const driftless = require('driftless'); async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } class Timeout extends EventEmitter { constructor(duration) { super(); this._startedAt = null; this._duration = duration; } start() { this._startedAt = Date.now(); this._timeout = driftless.setDriftlessTimeout(() => { this.emit('timeout'); }, this._duration); return this; } stop() { driftless.clearDriftless(this._timeout); return this; } timedout() { return Date.now() - this._startedAt > this._duration; } } // Turn a string like 2m into number of milliseconds // Supported units: ms, s, m, h function timeStringToMs(timeStr) { const rx = /^([0-9]+).+$/i; if (!rx.test(timeStr)) { throw new Error(`Invalid time string: ${timeStr}`); } let multiplier = 0; if (timeStr.endsWith('ms')) { multiplier = 1; } else if (timeStr.endsWith('s')) { multiplier = 1000; } else if (timeStr.endsWith('m')) { multiplier = 60 * 1000; } else if (timeStr.endsWith('h')) { multiplier = 60 * 60 * 1000; } else { throw new Error( `Unknown unit suffix in ${timeStr}. Supported units: ms, s, m, h` ); } const n = parseInt(timeStr.match(rx)[0], 10); return n * multiplier; } module.exports = { Timeout, sleep, timeStringToMs }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/legacy/util.js ================================================ const _debug = require('debug')('artillery:util'); const chalk = require('chalk'); const _ = require('lodash'); const _A = require('async'); const createS3Client = require('./create-s3-client'); const supportedRegions = [ 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', 'us-gov-east-1', 'us-gov-west-1', 'ca-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-central-1', 'eu-north-1', 'ap-south-1', 'ap-east-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'me-south-1', 'il-central-1', 'sa-east-1', 'cn-north-1', 'cn-northwest-1' ]; const getAccountId = require('../../aws/aws-get-account-id'); const { S3_BUCKET_NAME_PREFIX } = require('./constants'); const { paginateListObjectsV2 } = require('@aws-sdk/client-s3'); function atob(data) { return Buffer.from(data, 'base64').toString('ascii'); } function btoa(data) { return Buffer.from(data).toString('base64'); } async function getBucketName() { if (process.env.ARTILLERY_S3_BUCKET) { return process.env.ARTILLERY_S3_BUCKET; } const accountId = await getAccountId(); const bucketName = `${S3_BUCKET_NAME_PREFIX}-${accountId}`; // const bucketArn = `arn:aws:s3:::${bucketName}`; return bucketName; } function formatError(err) { return ( `${chalk.red('Error')}: ${err.message}${err.code ? ` (${err.code})` : ''}` ); } async function listAllObjectsWithPrefix(bucketName, prefix) { const s3Client = createS3Client(); const allObjects = []; const paginator = paginateListObjectsV2( { client: s3Client }, { Bucket: bucketName, Prefix: prefix, MaxKeys: 1000 } ); for await (const page of paginator) { if (page.Contents) { allObjects.push(...page.Contents); } } return allObjects; } module.exports = { supportedRegions, getAccountId, atob, btoa, formatError, listAllObjectsWithPrefix, getBucketName }; ================================================ FILE: packages/artillery/lib/platform/aws-ecs/worker/Dockerfile ================================================ # ******************************** # NOTE: Version we use here needs to be kept consistent with that in # artillery-engine-playwright. # ******************************** FROM mcr.microsoft.com/playwright:v1.58.1 ARG TARGETARCH ENV DEBIAN_FRONTEND=noninteractive # Install aws-lambda-ric build dependencies RUN apt-get update && apt-get install -y \ g++ \ make \ cmake \ unzip \ libcurl4-openssl-dev \ autoconf \ libtool \ python3-pip && pip3 install awscli --break-system-packages && pip3 install azure-cli==2.76.0 --break-system-packages RUN <> ~/.curlrc if [ "$TARGETARCH" = "arm64" ]; then # Temporal fix for SSL_ERROR_SYSCALL error on arm64 # see: https://github.com/curl/curl/issues/14154 echo 'insecure' >> ~/.curlrc fi EOT ARG WORKER_VERSION ENV WORKER_VERSION=$WORKER_VERSION # Additional dependencies for Fargate RUN apt-get install -y bash jq pwgen curl git zip tree # Define custom function directory ARG FUNCTION_DIR="/artillery" RUN mkdir -p ${FUNCTION_DIR} WORKDIR ${FUNCTION_DIR} COPY packages packages COPY packages/artillery/lib/platform/aws-lambda/lambda-handler/ . COPY package.json package.json ## Copy Fargate worker files COPY ./packages/artillery/lib/platform/aws-ecs/worker/loadgen-worker /artillery/loadgen-worker COPY ./packages/artillery/lib/platform/aws-ecs/worker/helpers.sh /artillery/helpers.sh # Install dependencies RUN npm install -w artillery --ignore-scripts --omit=dev RUN npm install aws-lambda-ric RUN npm cache clean --force \ && rm ./package.json \ && rm -rf /root/.cache \ && ln -s /artillery/node_modules/.bin/artillery /usr/local/bin/artillery \ && rm -rf /ms-playwright/firefox* \ && rm -rf /ms-playwright/webkit* \ && echo "ok" RUN chmod +x /artillery/loadgen-worker ENTRYPOINT ["/artillery/packages/artillery/bin/run"] ================================================ FILE: packages/artillery/lib/platform/aws-ecs/worker/helpers.sh ================================================ #!/usr/bin/env bash ARTIFACTORY_AUTH="${ARTIFACTORY_AUTH:-"null"}" ARTIFACTORY_EMAIL="${ARTIFACTORY_EMAIL:-"null"}" NPM_REGISTRY="${NPM_REGISTRY:-"null"}" NPM_TOKEN="${NPM_TOKEN:-"null"}" NPM_SCOPE="${NPM_SCOPE:-"null"}" NPMRC="${NPMRC:-"null"}" generate_npmrc () { if [[ "$ARTIFACTORY_AUTH" != "null" ]] && [[ "$ARTIFACTORY_EMAIL" != "null" ]] ; then echo "_auth=$ARTIFACTORY_AUTH" echo "email=$ARTIFACTORY_EMAIL" echo "always-auth=true" else # If only one is set - print a diagnostic message; # otherwise neither is set so do nothing. if [[ "$ARTIFACTORY_AUTH" != "null" ]] || [[ "$ARTIFACTORY_EMAIL" != "null" ]] ; then echo "Both ARTIFACTORY_AUTH and ARTIFACTORY_EMAIL must be set for Artifactory auth to work" fi fi # NOTE: Default value for NPM_SCOPE and NPM_TOKEN is "null" if [[ "$NPM_REGISTRY" == "null" ]] ; then NPM_REGISTRY="https://registry.npmjs.org/" fi NPM_REGISTRY_BASE="${NPM_REGISTRY#http:}" NPM_REGISTRY_BASE="${NPM_REGISTRY#https:}" NPM_TOKEN="${NPM_TOKEN:-"null"}" NPM_SCOPE="${NPM_SCOPE:-"null"}" # Set registry URL: # npm config set registry "$NPM_REGISTRY" echo "registry=${NPM_REGISTRY}" if [[ ! "$NPM_TOKEN" = "null" ]] ; then echo "${NPM_REGISTRY_BASE}:_authToken=${NPM_TOKEN}" fi if [[ "$NPM_SCOPE" != "null" ]] ; then if [[ "$NPM_SCOPE_REGISTRY" != "null" ]] ; then echo "${NPM_SCOPE}:registry=${NPM_SCOPE_REGISTRY}" else # npm config set "${NPM_SCOPE}:registry" "$NPM_REGISTRY" echo "${NPM_SCOPE}:registry=${NPM_REGISTRY}" fi fi # Any extra bits from the user: if [[ "$NPMRC" != "null" ]] ; then echo "$NPMRC" fi } base64d () { # Not expecting any tabs or newlines in the input so not read'ing in a loop. read encoded if [[ "$(uname)" == "Darwin" ]] ; then printf "%s" "$encoded" | base64 -D elif [[ "$(uname)" == "Linux" ]] ; then printf "%s" "$encoded" | base64 -d else printf "Unknown platform: $(uname)" # shellcheck disable=SC2034 EXIT_CODE=$ERR_UNKNOWN_PLATFORM exit fi } debug () { if [[ -n $DEBUG ]] ; then printf -- "%s\\n" "$@" fi } progress () { printf "******** [$worker_id] %s\\n" "$1" } ================================================ FILE: packages/artillery/lib/platform/aws-ecs/worker/loadgen-worker ================================================ #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' DEBUGX=${DEBUGX:-""} DEBUG=${DEBUG:-""} if [[ -n $DEBUGX ]] ; then set -x fi declare -r DIR=$(cd "$(dirname "$0")" && pwd) source "$DIR/helpers.sh" # shellcheck disable=2155 # declare -r DIR=$(cd "$(dirname "$0")" && pwd) declare -r ERR_ARGS=10 declare -r ERR_TEST_DIR_EMPTY=3 declare -r ERR_SIGNAL_SYNC=4 declare -r ERR_GO_TIMEOUT=5 declare -r ERR_CLI_ERROR=6 declare -r ERR_INTERRUPTED=7 declare -r ERR_UNKNOWN_PLATFORM=8 declare -r ERR_DEP=9 declare -r ERR_DEP_INSTALL=10 # npm install / yarn install failed declare -r ERR_NO_LICENSE=11 declare -r ERR_CLI_ERROR_EXPECT=21 declare -r ERR_HEARTBEAT_TIMEOUT=12 ERR_EXTRA_INFO="" # shellcheck disable=2155 declare -r TEST_DATA="$(pwd)/test_data" WAIT_TIMEOUT=${WAIT_TIMEOUT:-600} declare -t EXIT_CODE=0 CLI_RUNNING="no" CLI_STATUS= CLI_PID= CLEANING_UP="no" #mode="${MODE:-run}" # "run" or "bootstrap" is_azure= azure_storage_container_name= s3_test_data_path= cli_args=() cli_args_encoded= aws_region= sqs_queue_url= test_run_id= s3_run_data_base_path= s3_run_data_path= # This is set once we know if we're on Azure or AWS worker_id= is_leader=${IS_LEADER:-false} # true or false declare -r DEPENDENCIES=(jq aws az pwgen node npm yarn tree) send_message () { local body="$1" # body of the message, a string local type="$2" # type of the message: debug, leader, ensure if [[ "$is_azure" = "yes" ]] ; then send_message_aqs "$body" "$type" else send_message_sqs "$body" "$type" fi } send_message_aqs () { set +e set +o pipefail local body="$1" local type="$2" local aqs_message_payload="{\"msg\":\"$body\",\"type\":\"$type\"}" local aqs_message_attributes="{\"testId\": \"${test_run_id}\", \"workerId\": \"${worker_id}\"}" >/dev/null az storage message put \ --content "{ \"payload\": $aqs_message_payload, \"attributes\": $aqs_message_attributes }" \ --queue-name "$AQS_QUEUE_NAME" \ --account-name "$AZURE_STORAGE_ACCOUNT" || true set -e set -o pipefail } send_message_sqs () { set +e set +o pipefail local body="$1" local type="$2" local sqs_message_body="{\"msg\":\"$body\",\"type\":\"$type\"}" local sqs_message_attributes="{\"testId\": {\"StringValue\": \"${test_run_id}\", \"DataType\": \"String\"}, \"workerId\": {\"StringValue\": \"${worker_id}\", \"DataType\": \"String\"}}" >/dev/null aws sqs send-message \ --queue-url "${sqs_queue_url}" \ --message-body "$sqs_message_body" \ --message-attributes "$sqs_message_attributes" \ --message-group-id "${test_run_id}" \ --message-deduplication-id "$(pwgen -A 32 1)" \ --region "$aws_region" || true set -e set -o pipefail } send_event () { set +e set +o pipefail local payload="$1" if [[ "$is_azure" = "yes" ]] ; then send_event_aqs "$payload" else send_event_sqs "$payload" fi set -e set -o pipefail } send_event_sqs () { local payload="$1" local sqs_message_attributes="{\"testId\": {\"StringValue\": \"${test_run_id}\", \"DataType\": \"String\"}, \"workerId\": {\"StringValue\": \"${worker_id}\", \"DataType\": \"String\"}}" >/dev/null aws sqs send-message \ --queue-url "${sqs_queue_url}" \ --message-body "$payload" \ --message-attributes "$sqs_message_attributes" \ --message-group-id "${test_run_id}" \ --message-deduplication-id "$(pwgen -A 32 1)" \ --region "$aws_region" } send_event_aqs () { local payload="$1" local aqs_message_attributes="{\"testId\": \"${test_run_id}\", \"workerId\": \"${worker_id}\"}" >/dev/null az storage message put \ --content "{ \"payload\": $payload, \"attributes\": $aqs_message_attributes }" \ --queue-name "$AQS_QUEUE_NAME" \ --account-name "$AZURE_STORAGE_ACCOUNT" } install_npm_dependencies () { if [[ $(jq -r .modules "$METADATA_FILE") != "null" ]] ; then echo "Installing required npm dependencies" for dep in $(jq -r '.modules[]' "$METADATA_FILE") ; do echo "installing $dep" npm install --quiet "$dep" done else echo "No extra npm modules to install" fi if [[ -f "$TEST_DATA/package.json" ]] ; then echo "Installing dependencies in package.json" if [[ -f "$TEST_DATA/yarn.lock" ]] ; then # TODO: Test yarn's exit code yarn install else set +e npm install --loglevel=silent if [[ -f "npm-debug.log" ]] ; then cat npm-debug.log EXIT_CODE="$ERR_DEP_INSTALL" exit else echo "npm install completed" fi set -e fi else npm init -y --quiet fi } check_dependencies () { for dep in "${DEPENDENCIES[@]}" ; do set +e if ! command -v "$dep" > /dev/null ; then echo "Error: could not find $dep in \$PATH. Please install $dep." exit $ERR_DEP fi set -e done } sync_test_data () { mkdir "$TEST_DATA" || true pushd "$TEST_DATA" >/dev/null echo "is_azure: $is_azure" if [[ "$is_azure" = "yes" ]] ; then sync_test_data_azure else sync_test_data_s3 fi debug "$(pwd)" debug "$(ls -a)" } sync_test_data_azure () { # TODO: Exclude node_modules_stream.zip # This recreates the directory structure in the container, i.e. we'll have tests/$test_run_id here with all files under it # So we need to move them all up two levels to the current directory az storage blob download-batch -d . --account-name "$AZURE_STORAGE_ACCOUNT" -s "$azure_storage_container_name" --pattern "tests/$test_run_id/*" local tmpdir="$(mktemp -d)" set +e mv tests/$test_run_id/{.,}* $tmpdir rm -rf tests/$test_run_id mv $tmpdir/{.,}* . set -e } sync_test_data_s3 () { aws s3 sync --exclude node_modules_stream.zip "$s3_test_data_path" . >/dev/null } check_test_data () { file_count=$(find . -maxdepth 1 -name "*" | grep -v '^.$' -c) if [[ ! $file_count -gt 0 ]]; then echo "$TEST_DATA seems to be empty" EXIT_CODE=$ERR_TEST_DIR_EMPTY exit fi } install_dependencies () { pushd "$TEST_DATA" >/dev/null local METADATA_FILE="metadata.json" debug "$(cat $METADATA_FILE || true)" # Needed to install all packages to the dir of the test files. export NODE_PATH="$TEST_DATA:${NODE_PATH:-""}" generate_npmrc >> ~/.npmrc # Leader: pre-install modules for everyone else if [[ "$is_leader" = "true" ]] ; then send_message "leader npm pack start `date +%s`" "debug" install_npm_dependencies if [[ ! -d "node_modules" ]] ; then mkdir node_modules touch node_modules/.artillery fi zip -r -q node_modules.zip node_modules # | aws s3 cp - "$s3_test_data_path/node_modules_stream.zip" echo "Modules pre-packaged" # aws s3 mv "$s3_test_data_path/node_modules_stream.zip" "$s3_test_data_path/node_modules.zip" if [[ "$is_azure" = "yes" ]] ; then az storage blob upload --overwrite --account-name "$AZURE_STORAGE_ACCOUNT" --container-name "$azure_storage_container_name" --file node_modules.zip --name "tests/$test_run_id/node_modules.zip" else aws s3 cp node_modules.zip "$s3_test_data_path/node_modules.zip" fi send_message "leader npm prepack end `date +%s`" "debug" send_message "prepack_end" "leader" else # wait until node_modules.zip is available and unzip, or timeout # TODO: use aws s3api wait object-exists with a custom timeout send_message "follower npm prepack wait start `date +%s`" "debug" if [[ "$is_azure" = "yes" ]] ; then wait_for_go "tests/$test_run_id/node_modules.zip" else wait_for_go "$s3_test_data_path/node_modules.zip" fi unzip -o -q node_modules.zip send_message "follower npm prepack wait end `date +%s`" "debug" fi tree -I node_modules } signal_ready () { local synced_filename="synced_${worker_id}.json" echo "{ \"worker_id\": \"${worker_id}\" }" >> "$synced_filename" local synced_dest= local cp_status= if [[ "$is_azure" = "yes" ]] ; then send_event "{\"event\": \"workerReady\"}" synced_dest="${azure_storage_container_name}/$synced_filename" az storage blob upload --overwrite --account-name "$AZURE_STORAGE_ACCOUNT" --container-name "$azure_storage_container_name" --file "$synced_filename" --name "tests/$test_run_id/$synced_filename" cp_status=$? else synced_dest="${s3_run_data_path}/${synced_filename}" aws s3 cp "$synced_filename" "$synced_dest" 1>/dev/null 2>/dev/null cp_status=$? fi if [[ $cp_status -ne 0 ]]; then echo "could not send synced signal (to: $synced_dest)" EXIT_CODE=$ERR_SIGNAL_SYNC exit else echo "Worker $worker_id synced up & ready" fi } wait_for_go () { local SLEEP=2 local slept=0 local objpath= if [[ "$is_azure" = "yes" ]] ; then objpath="${1:-tests/$test_run_id/go.json}" else objpath="${1:-$s3_run_data_path/go.json}" fi echo "Waiting... ($objpath)" while true ; do set +e if [[ "$is_azure" = "yes" ]] ; then az storage blob download --account-name "$AZURE_STORAGE_ACCOUNT" --container-name "$azure_storage_container_name" --name "$objpath" --file "$(basename $objpath)" 1>/dev/null 2>/dev/null else aws s3 cp "$objpath" . 1>/dev/null 2>/dev/null fi local cp_exit_code=$? set -e if [[ $cp_exit_code -eq 0 ]]; then break else if [[ $slept -ge $WAIT_TIMEOUT ]]; then echo "Timed out waiting for go signal" EXIT_CODE=$ERR_GO_TIMEOUT exit else echo -n "." sleep $SLEEP (( slept = slept + SLEEP )) fi fi done } send_no_license_message () { set +e az storage message put \ --content "{\"payload\":{\"event\":\"workerError\",\"reason\":\"License not found - https://docs.art/az/license\", \"exitCode\":$ERR_NO_LICENSE},\"attributes\":{\"testId\": \"${test_run_id}\", \"workerId\": \"${worker_id}\"}}" \ --queue-name "$AQS_QUEUE_NAME" \ --account-name "$AZURE_STORAGE_ACCOUNT" || true set -e } decode_cli_args () { debug "encoded args $cli_args_encoded" local decoded_args= decoded_args=$(echo "$cli_args_encoded" | base64d) debug "decoded: $decoded_args" for an_arg in $(echo "$cli_args_encoded" | base64d | jq -r '.[] | @base64') ; do local decoded_arg= decoded_arg="$(printf -- "%s" "$an_arg" | base64d)" debug "decoded CLI arg: %s" "$decoded_arg" cli_args+=("$decoded_arg") done } check_heartbeat () { if [[ "$is_azure" = "yes" ]] ; then return # TODO: implement Azure blob storage equivalent fi local heartbeat_key="${s3_run_data_path}/heartbeat.json" local threshold=180 local check_interval=60 local grace_period=180 local started_at=$SECONDS debug "Heartbeat monitor started (grace=${grace_period}s, threshold=${threshold}s)" while true ; do sleep $check_interval local elapsed=$(( SECONDS - started_at )) if [[ $elapsed -lt $grace_period ]] ; then debug "Heartbeat: grace period (${elapsed}s < ${grace_period}s)" continue fi set +e aws s3 cp "$heartbeat_key" /tmp/heartbeat.json \ --region "$aws_region" 2>/dev/null local cp_exit=$? set -e if [[ $cp_exit -ne 0 ]] ; then printf "WARNING: Heartbeat S3 fetch failed (exit=%d), retrying next cycle\n" "$cp_exit" continue fi local latest_ts latest_ts=$(cat /tmp/heartbeat.json 2>/dev/null) if [[ -z "$latest_ts" ]] ; then printf "ERROR: No CLI heartbeat detected. Terminating worker.\n" echo "$ERR_HEARTBEAT_TIMEOUT" > /tmp/heartbeat_timeout if [[ "$CLI_RUNNING" = "yes" ]] && [[ -n "$CLI_PID" ]] ; then kill -TERM $CLI_PID 2>/dev/null || true sleep 15 if kill -0 $CLI_PID 2>/dev/null ; then kill -KILL $CLI_PID 2>/dev/null || true fi fi exit $ERR_HEARTBEAT_TIMEOUT fi # %3N (nanoseconds truncated to ms) requires GNU coreutils date. # Available on Amazon Linux / Fargate; falls back to second precision elsewhere. local now_ms=$(date +%s%3N 2>/dev/null || echo $(( $(date +%s) * 1000 ))) local age_ms=$(( now_ms - latest_ts )) local age_s=$(( age_ms / 1000 )) debug "Heartbeat: latest=${latest_ts}, age=${age_s}s" if [[ $age_s -gt $threshold ]] ; then printf "ERROR: CLI heartbeat expired (%ds old). Terminating worker.\n" "$age_s" echo "$ERR_HEARTBEAT_TIMEOUT" > /tmp/heartbeat_timeout if [[ "$CLI_RUNNING" = "yes" ]] && [[ -n "$CLI_PID" ]] ; then kill -TERM $CLI_PID 2>/dev/null || true sleep 15 if kill -0 $CLI_PID 2>/dev/null ; then kill -KILL $CLI_PID 2>/dev/null || true fi fi exit $ERR_HEARTBEAT_TIMEOUT fi done } run_a9 () { # NOTE: node_modules is required for plugins to be loaded export NODE_PATH="$TEST_DATA/node_modules:${NODE_PATH:-""}" export DEBUG=${DEBUG:-"debug:mode:off"} # can set via --launch-config if needed export ARTILLERY_PLUGIN_PATH=${ARTILLERY_PLUGIN_PATH:-""}:/artillery/packages/artillery/lib/platform/aws-ecs/legacy/plugins export ARTILLERY_PLUGINS="{\"sqs-reporter\":{\"region\": \"${aws_region}\"},\"inspect-script\":{}}" export SQS_TAGS="[{\"key\": \"testId\", \"value\": \"${test_run_id}\"},{\"key\":\"workerId\", \"value\":\"${worker_id}\"}]" if [[ "$is_azure" = "yes" ]] ; then export AZURE_STORAGE_QUEUE_URL=$sqs_queue_url else export SQS_QUEUE_URL=$sqs_queue_url export SQS_REGION=$aws_region fi export ARTILLERY_DISABLE_ENSURE=true debug "CLI args:" debug "${cli_args[@]}" # set max header size to 32KB -- solves the HPE_HEADER_OVERFLOW error # set max old space size to 12GB - max allocatable on Fargate MAX_OLD_SPACE_SIZE=${MAX_OLD_SPACE_SIZE:-12288} export NODE_OPTIONS="--max-http-header-size=32768 --max-old-space-size=$MAX_OLD_SPACE_SIZE ${NODE_OPTIONS:-""}" (set +eu ; ${ARTILLERY_BINARY:-"artillery"} "${cli_args[@]}" ; echo $? > exitCode ; set -eu) | tee output.txt & debug "node processes:" debug "$(pgrep -lfa node)" sleep 5 CLI_PID=$(pgrep -lfa node | grep artillery | awk '{print $1}') CLI_RUNNING="yes" check_heartbeat & HEARTBEAT_PID=$! debug "CLI pid:" debug "$CLI_PID" while kill -0 $CLI_PID 2> /dev/null ; do if [[ -f /tmp/heartbeat_timeout ]] ; then kill -KILL $CLI_PID 2>/dev/null || true break fi sleep 5 # signal handler will fire after we wake up done CLI_RUNNING="no" kill $HEARTBEAT_PID 2>/dev/null || true wait $HEARTBEAT_PID 2>/dev/null || true CLI_STATUS=$(cat exitCode) printf "Finished with code %s\n" "$CLI_STATUS" case `grep "inspect-script.config.ensure" "output.txt" >/dev/null; echo $?` in 0) # ensure spec found echo "got ensure spec" local ensure_spec=$(grep 'inspect-script.config.ensure' "output.txt" |awk -F 'ensure=' '{print $2}'|head -n 1) send_message "$ensure_spec" "ensure" ;; 1) # no ensure spec echo "no ensure spec" >&2 ;; *) # error - ignore echo "error while looking for ensure spec, ignoring" >&2 ;; esac # TODO: Upload to Storage Blob if on Azure if [[ "$is_azure" != "yes" ]] ; then aws s3 cp output.txt "${s3_run_data_path}/worker-log-${worker_id}.txt" echo "log: ${s3_run_data_path}/worker-log-${worker_id}.txt" fi if [[ -f /tmp/heartbeat_timeout ]] ; then EXIT_CODE=$(cat /tmp/heartbeat_timeout) rm -f /tmp/heartbeat_timeout elif [[ $CLI_STATUS -eq 0 ]] ; then EXIT_CODE=0 elif [[ $CLI_STATUS -eq $ERR_CLI_ERROR_EXPECT ]] ; then EXIT_CODE=$ERR_CLI_ERROR_EXPECT else EXIT_CODE=$ERR_CLI_ERROR fi exit $EXIT_CODE } main () { debug "$@" decode_cli_args s3_run_data_path="${s3_run_data_base_path}/${test_run_id}" progress "Test run ID = $test_run_id" progress "Syncing test data" sync_test_data check_test_data progress "Installing dependencies" install_dependencies progress "Ready to run" signal_ready progress "Waiting for green signal" wait_for_go progress "Off we go!" run_a9 } usage () { cat << EOF usage: $0 - run worker EOF } while getopts "z:p:a:r:q:i:d:t:h?" OPTION do case $OPTION in h) usage exit 0 ;; z) is_azure="$OPTARG" ;; p) s3_test_data_path="$OPTARG" ;; a) cli_args_encoded="$OPTARG" ;; r) aws_region="$OPTARG" ;; q) # Can also be AQS queue URL sqs_queue_url="$OPTARG" ;; i) test_run_id="$OPTARG" ;; d) s3_run_data_base_path="$OPTARG" ;; t) WAIT_TIMEOUT="$OPTARG" ;; \?) usage exit $ERR_ARGS ;; :) echo "Unknown option: -$OPTARG" >&2; exit 1;; * ) echo "Unimplemented option: -$OPTARG" >&2; exit 1;; esac done # shellcheck disable=2004 shift $(($OPTIND - 1)) # remove all args processed by getopts if [[ ! $# -eq 0 ]] ; then usage EXIT_CODE=$ERR_ARGS exit fi if [[ -z $s3_test_data_path || -z $cli_args_encoded || -z $test_run_id ]] ; then echo "Some required argument(s) not provided, aborting" >&2 EXIT_CODE=$ERR_ARGS exit fi if [[ "$is_azure" = "yes" ]] ; then # Remap for convenience azure_storage_container_name="$s3_test_data_path" az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID fi if [[ "$is_azure" != "yes" ]] ; then taskArn=$(curl -s "$ECS_CONTAINER_METADATA_URI_V4/task" \ | jq -r ".TaskARN" \ | cut -d "/" -f 3) fi worker_id=${WORKER_ID_OVERRIDE:-$(pwgen -A 12 1)} worker_id=${taskArn:-$worker_id} # make available to Artillery custom scripts/environment export WORKER_ID="$worker_id" progress "============================" progress "Worker starting up, ID = $worker_id, version = ${WORKER_VERSION:-unknown}, leader = $is_leader" progress "============================" cleanup () { local sig="$1" debug "cleanup called, signal:" debug "$sig" if [[ $CLEANING_UP = "no" ]] ; then CLEANING_UP="yes" if [[ "$is_azure" = "yes" ]] ; then if [[ "$is_leader" = "true" ]] ; then if [[ -z "${AZURE_RETAIN_BLOBS:-""}" ]] ; then # This exits with 0 regardless of whether the pattern matches any # blobs or not so it's OK to run this multiple times az storage blob delete-batch \ --account-name "$AZURE_STORAGE_ACCOUNT" \ -s "$azure_storage_container_name" \ --pattern "tests/$test_run_id/*" fi fi fi # Abnormal exit: if [[ $CLI_RUNNING = "yes" ]] ; then printf "Interrupted with %s, stopping\n" "$sig" EXIT_CODE=$ERR_INTERRUPTED kill -TERM $CLI_PID set +e timeout 20 tail --pid $CLI_PID -f /dev/null if [[ $? -eq 124 ]] ; then # timeout exits with 124 if the process it's waiting on is still running # i.e. if tail is still running it means the Artillery CLI did not exit: kill -KILL $CLI_PID CLI_STATUS=143 # SIGTERM (128 + 15) else # Preserve the exit code of the CLI CLI_STATUS=$(cat exitCode) fi set -e CLI_RUNNING="no" fi local sqs_message_body= if [[ $EXIT_CODE -eq 0 ]] ; then sqs_message_body='{"event": "workerDone"}' else # If 137 then something SIGKILL'ed Artillery local extra_info=$(printf "%s" "$$ERR_EXTRA_INFO" | jq -sR) sqs_message_body="{\"event\": \"workerError\", \"exitCode\": \"$EXIT_CODE\" }" fi send_event "$sqs_message_body" debug "Message body: $sqs_message_body" exit $EXIT_CODE else if [[ ! $sig = "EXIT" ]] ; then # EXIT will always fire after a TERM/INT, so if # that's the case we don't need to print this message. printf "Received %s but cleaning up already\n" "$sig" fi fi } set_trap_with_arg () { func="$1" ; shift for sig ; do # shellcheck disable=2064 trap "$func $sig" "$sig" done } set_trap_with_arg cleanup INT TERM EXIT main "$@" ================================================ FILE: packages/artillery/lib/platform/aws-lambda/dependencies.js ================================================ const fs = require('fs-extra'); const { PutObjectCommand } = require('@aws-sdk/client-s3'); const debug = require('debug')('platform:aws-lambda'); const Table = require('cli-table3'); const { promisify } = require('node:util'); const { createBOM } = require('../aws-ecs/legacy/bom'); const createS3Client = require('../aws-ecs/legacy/create-s3-client'); const _createLambdaBom = async ( absoluteScriptPath, absoluteConfigPath, flags ) => { const createBomOpts = {}; let entryPoint = absoluteScriptPath; const extraFiles = []; createBomOpts.scenarioPath = absoluteScriptPath; if (absoluteConfigPath) { entryPoint = absoluteConfigPath; extraFiles.push(absoluteScriptPath); createBomOpts.entryPointIsConfig = true; } // TODO: custom package.json path here if (flags) { createBomOpts.flags = flags; } const bom = await promisify(createBOM)(entryPoint, extraFiles, createBomOpts); return bom; }; async function _uploadFileToS3(item, testRunId, bucketName) { const s3 = createS3Client(); const prefix = `tests/${testRunId}`; let body; try { body = fs.readFileSync(item.orig); } catch (fsErr) { console.log(fsErr); } if (!body) { return; } const key = `${prefix}/${item.noPrefixPosix}`; await s3.send( new PutObjectCommand({ Bucket: bucketName, Key: key, // TODO: stream, not readFileSync Body: body }) ); debug(`Uploaded ${key}`); return; } async function _syncS3(bomManifest, testRunId, bucketName) { const metadata = { createdOn: Date.now(), name: testRunId, modules: bomManifest.modules }; //TODO: parallelise this let fileCount = 0; for (const file of bomManifest.files) { await _uploadFileToS3(file, testRunId, bucketName); fileCount++; } metadata.fileCount = fileCount; const plainS3 = createS3Client(); const prefix = `tests/${testRunId}`; const key = `${prefix}/metadata.json`; await plainS3.send( new PutObjectCommand({ Bucket: bucketName, Key: key, // TODO: stream, not readFileSync Body: JSON.stringify(metadata) }) ); debug(`Uploaded ${key}`); return `s3://${bucketName}/${key}`; } const createAndUploadTestDependencies = async ( bucketName, testRunId, absoluteScriptPath, absoluteConfigPath, flags ) => { const bom = await _createLambdaBom( absoluteScriptPath, absoluteConfigPath, flags ); artillery.log('Test bundle contents:'); const t = new Table({ head: ['Name', 'Type', 'Notes'] }); for (const f of bom.files) { t.push([f.noPrefix, 'file']); } for (const m of bom.modules) { t.push([ m, 'package', bom.pkgDeps.indexOf(m) === -1 ? 'not in package.json' : '' ]); } //TODO: add dotenv file if specified artillery.log(t.toString()); artillery.log(); const s3Path = await _syncS3(bom, testRunId, bucketName); return { bom, s3Path }; }; module.exports = { createAndUploadTestDependencies }; ================================================ FILE: packages/artillery/lib/platform/aws-lambda/index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const EventEmitter = require('node:events'); const debug = require('debug')('platform:aws-lambda'); const { randomUUID } = require('node:crypto'); const sleep = require('../../util/sleep'); const path = require('node:path'); const { LambdaClient, GetFunctionConfigurationCommand, InvokeCommand, CreateFunctionCommand, DeleteFunctionCommand, ResourceConflictException, ResourceNotFoundException } = require('@aws-sdk/client-lambda'); const { PutObjectCommand } = require('@aws-sdk/client-s3'); const { SQSClient, DeleteQueueCommand } = require('@aws-sdk/client-sqs'); const { IAMClient, GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand, CreatePolicyCommand } = require('@aws-sdk/client-iam'); const createS3Client = require('../aws-ecs/legacy/create-s3-client'); const { getBucketRegion } = require('../aws/aws-get-bucket-region'); const _https = require('node:https'); const { QueueConsumer } = require('../../queue-consumer'); const telemetry = require('../../telemetry'); const crypto = require('node:crypto'); const prices = require('./prices'); const _ = require('lodash'); const { SQS_QUEUES_NAME_PREFIX } = require('../aws/constants'); const ensureS3BucketExists = require('../aws/aws-ensure-s3-bucket-exists'); const getAccountId = require('../aws/aws-get-account-id'); const createSQSQueue = require('../aws/aws-create-sqs-queue'); const { createAndUploadTestDependencies } = require('./dependencies'); const awsGetDefaultRegion = require('../aws/aws-get-default-region'); const pkgVersion = require('../../../package.json').version; // https://stackoverflow.com/a/66523153 function memoryToVCPU(memMB) { if (memMB < 832) { return 0.5; } if (memMB < 3009) { return 2; } if (memMB < 5308) { return 3; } if (memMB < 7077) { return 4; } if (memMB < 8846) { return 5; } return 6; } class PlatformLambda { constructor(script, payload, opts, platformOpts) { this.workers = {}; this.count = 0; this.waitingReadyCount = 0; this.script = script; this.payload = payload; this.opts = opts; this.events = new EventEmitter(); const platformConfig = platformOpts.platformConfig; this.currentVersion = process.env.LAMBDA_IMAGE_VERSION || pkgVersion; this.ecrImageUrl = process.env.WORKER_IMAGE_URL; this.architecture = platformConfig.architecture || 'arm64'; this.region = platformConfig.region || 'us-east-1'; this.arnPrefix = this.region.startsWith('cn-') ? 'arn:aws-cn' : 'arn:aws'; this.securityGroupIds = platformConfig['security-group-ids']?.split(',') || []; this.subnetIds = platformConfig['subnet-ids']?.split(',') || []; this.useVPC = this.securityGroupIds.length > 0 && this.subnetIds.length > 0; this.memorySize = platformConfig['memory-size'] || 4096; this.testRunId = platformOpts.testRunId; this.lambdaRoleArn = platformConfig['lambda-role-arn'] || platformConfig.lambdaRoleArn; this.platformOpts = platformOpts; this.cloudKey = this.platformOpts.cliArgs.key || process.env.ARTILLERY_CLOUD_API_KEY; this.s3LifecycleConfigurationRules = [ { Expiration: { Days: 2 }, Filter: { Prefix: '/lambda' }, ID: 'RemoveAdHocTestData', Status: 'Enabled' }, { Expiration: { Days: 7 }, Filter: { Prefix: '/' }, ID: 'RemoveTestRunMetadata', Status: 'Enabled' } ]; this.artilleryArgs = []; } async init() { global.artillery.awsRegion = (await awsGetDefaultRegion()) || this.region; artillery.log('λ Preparing AWS Lambda function...'); this.accountId = await getAccountId(); const metadata = { region: this.region, platformConfig: { memory: this.memorySize, cpu: memoryToVCPU(this.memorySize) } }; global.artillery.globalEvents.emit('metadata', metadata); //make sure the bucket exists to send the zip file or the dependencies to const bucketName = await ensureS3BucketExists( this.region, this.s3LifecycleConfigurationRules, true ); this.bucketName = bucketName; global.artillery.s3BucketRegion = await getBucketRegion(bucketName); const { bom, s3Path } = await createAndUploadTestDependencies( this.bucketName, this.testRunId, this.opts.absoluteScriptPath, this.opts.absoluteConfigPath, this.platformOpts.cliArgs ); this.artilleryArgs.push('run'); if (this.platformOpts.cliArgs.environment) { this.artilleryArgs.push('-e'); this.artilleryArgs.push(this.platformOpts.cliArgs.environment); } if (this.platformOpts.cliArgs.solo) { this.artilleryArgs.push('--solo'); } if (this.platformOpts.cliArgs.target) { this.artilleryArgs.push('--target'); this.artilleryArgs.push(this.platformOpts.cliArgs.target); } if (this.platformOpts.cliArgs.variables) { this.artilleryArgs.push('-v'); this.artilleryArgs.push(this.platformOpts.cliArgs.variables); } if (this.platformOpts.cliArgs.overrides) { this.artilleryArgs.push('--overrides'); this.artilleryArgs.push(this.platformOpts.cliArgs.overrides); } if (this.platformOpts.cliArgs.dotenv) { this.artilleryArgs.push('--dotenv'); this.artilleryArgs.push(path.basename(this.platformOpts.cliArgs.dotenv)); } if (this.platformOpts.cliArgs['scenario-name']) { this.artilleryArgs.push('--scenario-name'); this.artilleryArgs.push(this.platformOpts.cliArgs['scenario-name']); } if (this.platformOpts.cliArgs.config) { this.artilleryArgs.push('--config'); const p = bom.files.filter( (x) => x.orig === this.opts.absoluteConfigPath )[0]; this.artilleryArgs.push(p.noPrefixPosix); } // This needs to be the last argument for now: const p = bom.files.filter( (x) => x.orig === this.opts.absoluteScriptPath )[0]; this.artilleryArgs.push(p.noPrefixPosix); // 36 is length of a UUUI v4 string const queueName = `${SQS_QUEUES_NAME_PREFIX}_${this.testRunId.slice( 0, 36 )}.fifo`; const sqsQueueUrl = await createSQSQueue(this.region, queueName); this.sqsQueueUrl = sqsQueueUrl; if (typeof this.lambdaRoleArn === 'undefined') { const lambdaRoleArn = await this.createLambdaRole(); this.lambdaRoleArn = lambdaRoleArn; } else { artillery.log(` - Lambda role ARN: ${this.lambdaRoleArn}`); } this.functionName = this.createFunctionNameWithHash(); await this.createOrUpdateLambdaFunctionIfNeeded(); artillery.log(` - Lambda function: ${this.functionName}`); artillery.log(` - Region: ${this.region}`); artillery.log(` - AWS account: ${this.accountId}`); debug({ bucketName, s3Path, sqsQueueUrl }); const consumer = new QueueConsumer(); consumer.create( { poolSize: Math.min(this.platformOpts.count, 100) }, { queueUrl: process.env.SQS_QUEUE_URL || this.sqsQueueUrl, region: this.region, waitTimeSeconds: 10, messageAttributeNames: ['testId', 'workerId'], visibilityTimeout: 60, batchSize: 10, handleMessage: async (message) => { let body = null; try { body = JSON.parse(message.Body); } catch (err) { console.error(err); console.log(message.Body); } // // Ignore any messages that are invalid or not tagged properly. // if (process.env.LOG_SQS_MESSAGES) { console.log(message); } if (!body) { throw new Error('SQS message with empty body'); } const attrs = message.MessageAttributes; if (!attrs || !attrs.testId || !attrs.workerId) { throw new Error('SQS message with no testId or workerId'); } if (this.testRunId !== attrs.testId.StringValue) { throw new Error('SQS message for an unknown testId'); } const workerId = attrs.workerId.StringValue; if (body.event === 'workerStats') { this.events.emit('stats', workerId, body); // event consumer accesses body.stats } else if (body.event === 'artillery.log') { console.log(body.log); } else if (body.event === 'done') { // 'done' handler in Launcher exects the message argument to have an "id" and "report" fields body.id = workerId; body.report = body.stats; // Launcher expects "report", SQS reporter sends "stats" this.events.emit('done', workerId, body); } else if ( body.event === 'phaseStarted' || body.event === 'phaseCompleted' ) { body.id = workerId; this.events.emit(body.event, workerId, { phase: body.phase }); } else if (body.event === 'workerError') { global.artillery.suggestedExitCode = body.exitCode || 1; if (body.exitCode !== 21) { this.events.emit(body.event, workerId, { id: workerId, error: new Error( `A Lambda function has exited with an error. Reason: ${body.reason}` ), level: 'error', aggregatable: false, logs: body.logs }); } } else if (body.event === 'workerReady') { this.events.emit(body.event, workerId); this.waitingReadyCount++; // TODO: Do this only for batches of workers with "wait" option set if (this.waitingReadyCount === this.count) { // TODO: Retry const s3 = createS3Client(); await s3.send( new PutObjectCommand({ Body: Buffer.from(''), Bucket: this.bucketName, Key: `/${this.testRunId}/green` }) ); } } else { debug(body); } } } ); let queueEmpty = 0; consumer.on('error', (err) => { artillery.log(err); }); consumer.on('empty', (_err) => { debug('queueEmpty:', queueEmpty); queueEmpty++; }); consumer.start(); this.sqsConsumer = consumer; // TODO: Start the timer when the first worker is created const startedAt = Date.now(); global.artillery.ext({ ext: 'beforeExit', method: async (event) => { try { await telemetry.init().capture({ event: 'ping', awsAccountId: crypto .createHash('sha1') .update(this.accountId) .digest('base64') }); process.nextTick(() => { telemetry.shutdown(); }); } catch (_err) {} function round(number, decimals) { const m = 10 ** decimals; return Math.round(number * m) / m; } if (event.flags && event.flags.platform === 'aws:lambda') { let price = 0; if (!prices[this.region]) { price = prices.base[this.architecture]; } else { price = prices[this.region][this.architecture]; } const duration = Math.ceil((Date.now() - startedAt) / 1000); const total = ((price * this.memorySize) / 1024) * this.platformOpts.count * duration; const cost = round(total / 10e10, 4); console.log(`\nEstimated AWS Lambda cost for this test: $${cost}\n`); } } }); } getDesiredWorkerCount() { return this.platformOpts.count; } async startJob() { await this.init(); for (let i = 0; i < this.platformOpts.count; i++) { const { workerId } = await this.createWorker(); this.workers[workerId] = { id: workerId }; await this.runWorker(workerId); } } async createWorker() { const workerId = randomUUID(); return { workerId }; } async runWorker(workerId) { const lambda = new LambdaClient({ apiVersion: '2015-03-31', region: this.region }); const event = { SQS_QUEUE_URL: this.sqsQueueUrl, SQS_REGION: this.region, WORKER_ID: workerId, ARTILLERY_ARGS: this.artilleryArgs, TEST_RUN_ID: this.testRunId, BUCKET: this.bucketName, WAIT_FOR_GREEN: true, ARTILLERY_CLOUD_API_KEY: this.cloudKey }; if (process.env.ARTILLERY_CLOUD_ENDPOINT) { event.ARTILLERY_CLOUD_ENDPOINT = process.env.ARTILLERY_CLOUD_ENDPOINT; } debug('Lambda event payload:'); debug({ event }); const payload = JSON.stringify(event); // Wait for the function to be invocable: const timeout = this.useVPC ? 240e3 : 120e3; let waited = 0; let ok = false; let state; while (waited < timeout) { try { state = ( await lambda.send( new GetFunctionConfigurationCommand({ FunctionName: this.functionName }) ) ).State; if (state === 'Active') { debug('Lambda function ready:', this.functionName); ok = true; break; } else { await sleep(10 * 1000); waited += 10 * 1000; } } catch (err) { debug('Error getting lambda state:', err); await sleep(10 * 1000); waited += 10 * 1000; } } if (!ok) { debug( 'Time out waiting for lamda function to be ready:', this.functionName ); throw new Error( 'Timeout waiting for lambda function to be ready for invocation' ); } await lambda.send( new InvokeCommand({ FunctionName: this.functionName, Payload: payload, InvocationType: 'Event' }) ); this.count++; } async stopWorker(_workerId) { // TODO: Send message to that worker and have it exit early } async shutdown() { if (this.sqsConsumer) { this.sqsConsumer.stop(); } const sqs = new SQSClient({ region: this.region }); const lambda = new LambdaClient({ apiVersion: '2015-03-31', region: this.region }); try { await sqs.send( new DeleteQueueCommand({ QueueUrl: this.sqsQueueUrl }) ); if (process.env.RETAIN_LAMBDA === 'false') { await lambda.send( new DeleteFunctionCommand({ FunctionName: this.functionName }) ); } } catch (err) { console.error(err); } } async createLambdaRole() { const ROLE_NAME = 'artilleryio-default-lambda-role-20230116'; const POLICY_NAME = 'artilleryio-lambda-policy-20230116'; const iam = new IAMClient({ region: global.artillery.awsRegion }); try { const res = await iam.send(new GetRoleCommand({ RoleName: ROLE_NAME })); return res.Role.Arn; } catch (err) { debug(err); } const principalService = this.region.startsWith('cn-') ? 'lambda.amazonaws.com.cn' : 'lambda.amazonaws.com'; const res = await iam.send( new CreateRoleCommand({ AssumeRolePolicyDocument: `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "${principalService}" }, "Action": "sts:AssumeRole" } ] }`, Path: '/', RoleName: ROLE_NAME }) ); const lambdaRoleArn = res.Role.Arn; await iam.send( new AttachRolePolicyCommand({ PolicyArn: `${this.arnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`, RoleName: ROLE_NAME }) ); await iam.send( new AttachRolePolicyCommand({ PolicyArn: `${this.arnPrefix}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole`, RoleName: ROLE_NAME }) ); const iamRes = await iam.send( new CreatePolicyCommand({ PolicyDocument: `{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["sqs:*"], "Resource": "${this.arnPrefix}:sqs:*:${this.accountId}:artilleryio*" }, { "Effect": "Allow", "Action": ["s3:HeadObject", "s3:PutObject", "s3:ListBucket", "s3:GetObject", "s3:GetObjectAttributes"], "Resource": [ "${this.arnPrefix}:s3:::artilleryio-test-data*", "${this.arnPrefix}:s3:::artilleryio-test-data*/*" ] } ] } `, PolicyName: POLICY_NAME, Path: '/' }) ); await iam.send( new AttachRolePolicyCommand({ PolicyArn: iamRes.Policy.Arn, RoleName: ROLE_NAME }) ); // See https://stackoverflow.com/a/37438525 for why we need this await sleep(10 * 1000); return lambdaRoleArn; } async createOrUpdateLambdaFunctionIfNeeded() { const existingLambdaConfig = await this.getLambdaFunctionConfiguration(); if (existingLambdaConfig) { debug( 'Lambda function with this configuration already exists. Using existing function.' ); return; } try { await this.createLambda({ bucketName: this.bucketName, functionName: this.functionName }); return; } catch (err) { if (err instanceof ResourceConflictException) { debug( 'Lambda function with this configuration already exists. Using existing function.' ); return; } throw new Error(`Failed to create Lambda Function: \n${err}`); } } async getLambdaFunctionConfiguration() { const lambda = new LambdaClient({ apiVersion: '2015-03-31', region: this.region }); try { const res = await lambda.send( new GetFunctionConfigurationCommand({ FunctionName: this.functionName }) ); return res; } catch (err) { if (err instanceof ResourceNotFoundException) { return null; } throw new Error(`Failed to get Lambda Function: \n${err}`); } } createFunctionNameWithHash(_lambdaConfig) { const changeableConfig = { MemorySize: this.memorySize, VpcConfig: { SecurityGroupIds: this.securityGroupIds, SubnetIds: this.subnetIds } }; const configHash = crypto .createHash('md5') .update(JSON.stringify(changeableConfig)) .digest('hex'); let name = `artilleryio-v${this.currentVersion.replace(/\./g, '-')}-${ this.architecture }-${configHash}`; if (name.length > 64) { name = name.slice(0, 64); } return name; } async createLambda(opts) { const { functionName } = opts; const lambda = new LambdaClient({ apiVersion: '2015-03-31', region: this.region }); const lambdaConfig = { PackageType: 'Image', Code: { ImageUri: this.ecrImageUrl || `248481025674.dkr.ecr.${this.region}.amazonaws.com/artillery-worker:${this.currentVersion}-${this.architecture}` }, ImageConfig: { Command: ['a9-handler-index.handler'], EntryPoint: ['/usr/bin/npx', 'aws-lambda-ric'] }, FunctionName: functionName, Description: 'Artillery.io test', MemorySize: parseInt(this.memorySize, 10), Timeout: 900, Role: this.lambdaRoleArn, //TODO: architecture influences the entrypoint. We should review which architecture to use in the end (may impact Playwright viability) Architectures: [this.architecture], Environment: { Variables: { S3_BUCKET_PATH: this.bucketName, NPM_CONFIG_CACHE: '/tmp/.npm', //TODO: move this to Dockerfile AWS_LAMBDA_LOG_FORMAT: 'JSON', //TODO: review this. we need to find a ways for logs to look better in Cloudwatch ARTILLERY_WORKER_PLATFORM: 'aws:lambda' } } }; if (this.useVPC) { lambdaConfig.VpcConfig = { SecurityGroupIds: this.securityGroupIds, SubnetIds: this.subnetIds }; } await lambda.send(new CreateFunctionCommand(lambdaConfig)); } } module.exports = PlatformLambda; ================================================ FILE: packages/artillery/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js ================================================ const fs = require('node:fs'); const path = require('node:path'); const { runProcess } = require('./a9-handler-helpers'); const syncTestData = async (bucketName, testRunId) => { console.log('Syncing test data'); const LOCAL_TEST_DATA_PATH = `/tmp/test_data/${testRunId}`; const REMOTE_TEST_DATA_PATH = `s3://${bucketName}/tests/${testRunId}`; if (!fs.existsSync(LOCAL_TEST_DATA_PATH)) { fs.mkdirSync(LOCAL_TEST_DATA_PATH, { recursive: true }); } const result = await runProcess( 'aws', ['s3', 'sync', REMOTE_TEST_DATA_PATH, LOCAL_TEST_DATA_PATH], { log: true } ); if (result.code !== 0 || result.stderr) { throw new Error(`Failed to sync test data:\n ${result.stderr}`); } console.log('Test data synced'); }; const installNpmDependencies = async (testDataLocation) => { //TODO: handle npmrc (i.e. artifactory, etc) console.log(`Changing directory to ${testDataLocation}`); process.chdir(testDataLocation); const metadataJson = fs.readFileSync( path.join(testDataLocation, 'metadata.json') ); //first, install custom dependencies for (const dep of JSON.parse(metadataJson).modules) { console.log(`Installing ${dep}`); await runProcess('npm', ['install', dep, '--prefix', testDataLocation], { log: true, env: { HOME: testDataLocation } }); } if (!fs.existsSync(path.join(testDataLocation, 'package.json'))) { await runProcess('npm', ['init', '-y', '--quiet'], { log: true, env: { HOME: testDataLocation } }); } const installResult = await runProcess( 'npm', ['install', '--prefix', testDataLocation], { log: true, env: { HOME: testDataLocation } } ); console.log(installResult); console.log('Finished installing test data'); }; module.exports = { syncTestData, installNpmDependencies }; ================================================ FILE: packages/artillery/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js ================================================ const { spawn } = require('node:child_process'); const sleep = async (n) => new Promise((resolve, _reject) => { setTimeout(() => { resolve(); }, n); }); async function runProcess(name, args, { env, log }) { return new Promise((resolve, _reject) => { const proc = spawn(name, args, { env }); let stdout = ''; let stderr = ''; proc.stdout.on('data', (data) => { if (log) { console.log(data.toString()); } stdout += data.toString(); }); proc.stderr.on('data', (data) => { if (log) { console.error(data.toString()); } stderr += data.toString(); }); proc.once('close', (code) => { resolve({ stdout, stderr, code }); }); proc.on('error', (err) => { resolve({ stdout, stderr, err }); }); }); } module.exports = { runProcess, sleep }; ================================================ FILE: packages/artillery/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs'); const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { randomUUID } = require('node:crypto'); const { runProcess, sleep } = require('./a9-handler-helpers'); const { syncTestData, installNpmDependencies } = require('./a9-handler-dependencies'); const path = require('node:path'); const TIMEOUT_THRESHOLD_MSEC = 20 * 1000; class MQ { constructor({ region, queueUrl, attrs }) { this.sqs = new SQSClient({ region }); this.queueUrl = queueUrl; this.attrs = attrs; } async send(body) { const messageAttributes = Object.keys(this.attrs).reduce((acc, key) => { acc[key] = { DataType: 'String', StringValue: this.attrs[key] }; return acc; }, {}); return this.sqs.send( new SendMessageCommand({ QueueUrl: this.queueUrl, MessageBody: JSON.stringify(body), MessageAttributes: messageAttributes, MessageDeduplicationId: randomUUID(), MessageGroupId: this.attrs.testId }) ); } } async function handler(event, context) { const { SQS_QUEUE_URL, SQS_REGION, TEST_RUN_ID, WORKER_ID, ARTILLERY_ARGS, BUCKET, ENV, WAIT_FOR_GREEN, ARTILLERY_CLOUD_API_KEY, ARTILLERY_CLOUD_ENDPOINT } = event; console.log('TEST_RUN_ID: ', TEST_RUN_ID); const mq = new MQ({ region: SQS_REGION, queueUrl: SQS_QUEUE_URL, attrs: { testId: TEST_RUN_ID, workerId: WORKER_ID } }); const TEST_DATA_LOCATION = `/tmp/test_data/${TEST_RUN_ID}`; try { await syncTestData(BUCKET, TEST_RUN_ID); } catch (err) { await mq.send({ event: 'workerError', reason: 'TestDataSyncFailure', logs: { err: { message: err.message, stack: err.stack } } }); } try { await installNpmDependencies(TEST_DATA_LOCATION); } catch (err) { await mq.send({ event: 'workerError', reason: 'InstallDependenciesFailure', logs: { err: { message: err.message, stack: err.stack } } }); } const interval = setInterval(async () => { const timeRemaining = context.getRemainingTimeInMillis(); if (timeRemaining > TIMEOUT_THRESHOLD_MSEC) { return; } await mq.send({ event: 'workerError', reason: 'AWSLambdaTimeout' }); clearInterval(interval); }, 5000).unref(); // TODO: Stop Artillery process - relying on Lambda runtime to shut everything down now const s3 = new S3Client(); await mq.send({ event: 'workerReady' }); let waitingFor = 0; const MAX_WAIT_MSEC = 3 * 60 * 1000; const SLEEP_MSEC = 2500; if (WAIT_FOR_GREEN) { while (waitingFor < MAX_WAIT_MSEC) { try { const params = { Bucket: BUCKET, Key: `/${TEST_RUN_ID}/green` }; await s3.send(new HeadObjectCommand(params)); break; } catch (_err) { await sleep(SLEEP_MSEC); waitingFor += SLEEP_MSEC; } } } try { const { err, code, stdout, stderr } = await execArtillery({ SQS_QUEUE_URL, SQS_REGION, TEST_RUN_ID, WORKER_ID, ARTILLERY_ARGS, TEST_DATA_LOCATION, ENV, ARTILLERY_CLOUD_API_KEY, ARTILLERY_CLOUD_ENDPOINT }); if (err || code !== 0) { console.log(err); await mq.send({ event: 'workerError', reason: 'ArtilleryError', exitCode: code, logs: { stdout, stderr } }); } } catch (err) { console.error(err); await mq.send({ event: 'workerError', reason: 'StartupFailure', logs: { err: { message: err.message, stack: err.stack } } }); } } async function execArtillery(options) { const { SQS_QUEUE_URL, SQS_REGION, TEST_RUN_ID, WORKER_ID, ARTILLERY_ARGS, ENV, NODE_BINARY_PATH, ARTILLERY_BINARY_PATH, TEST_DATA_LOCATION, ARTILLERY_CLOUD_API_KEY, ARTILLERY_CLOUD_ENDPOINT } = options; const env = Object.assign( {}, process.env, { ARTILLERY_PLUGINS: `{"sqs-reporter":{"region": "${SQS_REGION}"}}`, SQS_TAGS: `[{"key":"testId","value":"${TEST_RUN_ID}"},{"key":"workerId","value":"${WORKER_ID}"}]`, SQS_QUEUE_URL: SQS_QUEUE_URL, SQS_REGION: SQS_REGION, ARTILLERY_DISABLE_ENSURE: 'true', WORKER_ID: WORKER_ID, // Set test run ID for this Artillery process explicitly. This makes sure that $testId // template variable is set to the same value for all Lambda workers as the one user // sees on their terminal ARTILLERY_TEST_RUN_ID: TEST_RUN_ID // SHIP_LOGS: 'true', }, ARTILLERY_CLOUD_API_KEY ? { ARTILLERY_CLOUD_API_KEY } : {}, ARTILLERY_CLOUD_ENDPOINT ? { ARTILLERY_CLOUD_ENDPOINT } : {}, ENV ); const TEST_DATA_NODE_MODULES = `${TEST_DATA_LOCATION}/node_modules`; const ARTILLERY_NODE_MODULES = '/artillery/node_modules'; const ARTILLERY_PATH = ARTILLERY_BINARY_PATH || `${ARTILLERY_NODE_MODULES}/artillery/bin/run`; // Set the plugin path to the legacy SQS plugin as well as to user's test data for third party plugins env.ARTILLERY_PLUGIN_PATH = `${TEST_DATA_NODE_MODULES}:${ARTILLERY_NODE_MODULES}/artillery/lib/platform/aws-ecs/legacy/plugins`; env.HOME = '/tmp'; env.NODE_PATH = ['/artillery/node_modules', TEST_DATA_NODE_MODULES].join( path.delimiter ); return runProcess( NODE_BINARY_PATH || 'node', [ARTILLERY_PATH].concat(ARTILLERY_ARGS), { env, log: true } ); } module.exports = { handler, runProcess, execArtillery }; ================================================ FILE: packages/artillery/lib/platform/aws-lambda/lambda-handler/package.json ================================================ { "name": "lambda-handler", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "artillery-plugin-sqs-reporter": "^1.2.0" } } ================================================ FILE: packages/artillery/lib/platform/aws-lambda/prices.js ================================================ // Compute pricing for AWS Lambda // GB/second divided by 10e10 // https://aws.amazon.com/lambda/pricing/ module.exports = { base: { x86_64: 1666670, arm64: 1333340 }, 'af-south-1': { x86_64: 2210000, arm64: 1768000 }, 'ap-east-1': { x86_64: 2292000, arm64: 1830000 }, 'eu-south-1': { x86_64: 1951720, arm64: 1561380 }, 'me-south-1': { x86_64: 2066670, arm64: 1653340 }, 'me-central-1': { x86_64: 2066670, arm64: 1653340 } }; ================================================ FILE: packages/artillery/lib/platform/az/aci.js ================================================ // Copyright (c) Artillery Software Inc. // SPDX-License-Identifier: BUSL-1.1 // // Non-evaluation use of Artillery on Azure requires a commercial license const { QueueConsumer } = require('./aqs-queue-consumer'); const { SQS_QUEUES_NAME_PREFIX } = require('../aws/constants'); const { DefaultAzureCredential } = require('@azure/identity'); const { QueueClient } = require('@azure/storage-queue'); const { ContainerInstanceManagementClient } = require('@azure/arm-containerinstance'); const { BlobServiceClient } = require('@azure/storage-blob'); const { createTest } = require('../aws-ecs/legacy/create-test'); const util = require('../aws-ecs/legacy/util'); const generateId = require('../../util/generate-id'); const EventEmitter = require('eventemitter3'); const debug = require('debug')('platform:azure-aci'); const { IMAGE_VERSION, WAIT_TIMEOUT } = require('../aws-ecs/legacy/constants'); const { regionNames } = require('./regions'); const path = require('node:path'); const { Timeout, sleep } = require('../aws-ecs/legacy/time'); const dotenv = require('dotenv'); const fs = require('node:fs'); // got is loaded lazily via await import('got') // Helper to convert readable stream to string async function streamToString(readableStream) { return new Promise((resolve, reject) => { const chunks = []; readableStream.on('data', (data) => { chunks.push(data.toString()); }); readableStream.on('end', () => { resolve(chunks.join('')); }); readableStream.on('error', reject); }); } class PlatformAzureACI { constructor(script, variablePayload, opts, platformOpts) { this.script = script; this.variablePayload = variablePayload; this.opts = opts; this.platformOpts = platformOpts; this.cloudKey = this.platformOpts.cliArgs.key || process.env.ARTILLERY_CLOUD_API_KEY; this.events = new EventEmitter(); this.testRunId = platformOpts.testRunId; this.workers = {}; this.count = 0; this.waitingReadyCount = 0; this.artilleryArgs = []; this.azureTenantId = process.env.AZURE_TENANT_ID || platformOpts.platformConfig['tenant-id']; this.azureSubscriptionId = process.env.AZURE_SUBSCRIPTION_ID || platformOpts.platformConfig['subscription-id']; this.azureClientId = process.env.AZURE_CLIENT_ID; this.azureClientSecret = process.env.AZURE_CLIENT_SECRET; this.storageAccount = process.env.AZURE_STORAGE_ACCOUNT || platformOpts.platformConfig['storage-account']; this.blobContainerName = process.env.AZURE_STORAGE_BLOB_CONTAINER || platformOpts.platformConfig['blob-container']; this.resourceGroupName = process.env.AZURE_RESOURCE_GROUP || platformOpts.platformConfig['resource-group']; this.cpu = parseInt(platformOpts.platformConfig.cpu, 10) || 4; this.memory = parseInt(platformOpts.platformConfig.memory, 10) || 8; this.region = platformOpts.platformConfig.region || 'eastus'; this.extraEnvVars = {}; if (!regionNames.includes(this.region)) { const err = new Error(`Invalid region: ${this.region}`); err.code = 'INVALID_REGION'; err.url = 'https://docs.art/az/regions'; throw err; } if ( !this.azureTenantId || !this.azureSubscriptionId || !this.azureClientId || !this.azureClientSecret ) { const err = new Error('Azure credentials not found'); err.code = 'AZURE_CREDENTIALS_NOT_FOUND'; err.url = 'https://docs.art/az/credentials'; throw err; } if ( !this.storageAccount || !this.blobContainerName || !this.resourceGroupName ) { const err = new Error('Azure configuration not found'); err.code = 'AZURE_CONFIG_NOT_FOUND'; err.url = 'https://docs.art/az/configuration'; throw err; } this.containerInstances = []; } async init() { const credential = new DefaultAzureCredential(); artillery.log('Tenant ID:', this.azureTenantId); artillery.log('Subscription ID:', this.azureSubscriptionId); artillery.log('Storage account:', this.storageAccount); artillery.log('Blob container:', this.blobContainerName); artillery.log('Resource group:', this.resourceGroupName); if (this.platformOpts.count > 5) { const ok = await this.checkLicense(); if (!ok) { console.log(); console.log(` +--------------------------------------------------+ | License for Azure integration not found | | | | Load tests on Azure are limited to a maximum of | | 5 workers without a valid license. | | See https://docs.art/az/license for more details | +--------------------------------------------------+ `); throw new Error('ERR_LICENSE_REQUIRED'); } } // // Upload test bundle // this.blobServiceClient = new BlobServiceClient( `https://${this.storageAccount}.blob.core.windows.net`, credential ); this.blobContainerClient = this.blobServiceClient.getContainerClient( this.blobContainerName ); const customSyncClient = { send: async (command) => { // command is always an instance of PutObjectCommand() from @aws-sdk/client-s3 const { Key, Body } = command.input; const blockBlobClient = this.blobContainerClient.getBlockBlobClient(Key); await blockBlobClient.upload(Body, Body.length); } }; const { manifest } = await createTest(this.opts.absoluteScriptPath, { name: this.testRunId, config: this.platformOpts.cliArgs.config, flags: this.platformOpts.cliArgs, customSyncClient }); // // Create the queue // this.queueName = `${SQS_QUEUES_NAME_PREFIX}_${this.testRunId}.` .replaceAll('_', '-') .slice(0, 63); this.queueUrl = process.env.AZURE_STORAGE_QUEUE_URL || `https://${this.storageAccount}.queue.core.windows.net/${this.queueName}`; const queueClient = new QueueClient(this.queueUrl, credential); await queueClient.create(); this.aqsClient = queueClient; // Construct CLI args for the container this.artilleryArgs = []; this.artilleryArgs.push('run'); if (this.platformOpts.cliArgs.environment) { this.artilleryArgs.push('-e'); this.artilleryArgs.push(this.platformOpts.cliArgs.environment); } if (this.platformOpts.cliArgs.solo) { this.artilleryArgs.push('--solo'); } if (this.platformOpts.cliArgs.target) { this.artilleryArgs.push('--target'); this.artilleryArgs.push(this.platformOpts.cliArgs.target); } if (this.platformOpts.cliArgs.variables) { this.artilleryArgs.push('-v'); this.artilleryArgs.push(this.platformOpts.cliArgs.variables); } if (this.platformOpts.cliArgs.overrides) { this.artilleryArgs.push('--overrides'); this.artilleryArgs.push(this.platformOpts.cliArgs.overrides); } if (this.platformOpts.cliArgs.dotenv) { const dotEnvPath = path.resolve( process.cwd(), this.platformOpts.cliArgs.dotenv ); const contents = fs.readFileSync(dotEnvPath); const envVars = dotenv.parse(contents); this.extraEnvVars = Object.assign({}, this.extraEnvVars, envVars); } if (this.platformOpts.cliArgs['scenario-name']) { this.artilleryArgs.push('--scenario-name'); this.artilleryArgs.push(this.platformOpts.cliArgs['scenario-name']); } if (this.platformOpts.cliArgs.config) { this.artilleryArgs.push('--config'); const p = manifest.files.filter( (x) => x.orig === this.opts.absoluteConfigPath )[0]; this.artilleryArgs.push(p.noPrefixPosix); } if (this.platformOpts.cliArgs.quiet) { this.artilleryArgs.push('--quiet'); } // This needs to be the last argument for now: const p = manifest.files.filter( (x) => x.orig === this.opts.absoluteScriptPath )[0]; this.artilleryArgs.push(p.noPrefixPosix); const poolSize = typeof process.env.CONSUMER_POOL_SIZE !== 'undefined' ? parseInt(process.env.CONSUMER_POOL_SIZE, 10) : Math.max(Math.ceil(this.count / 25), 5); const consumer = new QueueConsumer( { poolSize }, { queueUrl: process.env.AZURE_STORAGE_QUEUE_URL || this.queueUrl, handleMessage: async (message) => { let payload = null; let attributes = null; try { const result = JSON.parse(message.Body); payload = result.payload; attributes = result.attributes; } catch (parseErr) { console.error(parseErr); console.error(message.Body); } if (process.env.LOG_QUEUE_MESSAGES) { console.log(message); } if (!payload) { throw new Error('AQS message with an empty body'); } // Handle overflow messages stored in blob storage if (payload._overflowRef) { try { const blobClient = this.blobContainerClient.getBlockBlobClient( payload._overflowRef ); const downloadResponse = await blobClient.download(0); const downloaded = await streamToString( downloadResponse.readableStreamBody ); const fullMessage = JSON.parse(downloaded); payload = fullMessage.payload; attributes = fullMessage.attributes; } catch (blobErr) { console.error('Failed to fetch worker message:', blobErr); throw new Error( `Failed to fetch worker message: ${payload._overflowRef}` ); } } if (!attributes || !attributes.testId || !attributes.workerId) { throw new Error('AQS message with no testId or workerId'); } if (this.testRunId !== attributes.testId) { throw new Error('AQS message for an unknown testId'); } const workerId = attributes.workerId; if (payload.event === 'workerStats') { this.events.emit('stats', workerId, payload); } else if (payload.event === 'artillery.log') { console.log(payload.log); } else if (payload.event === 'done') { // 'done' handler in Launcher exects the message argument to have an "id" and "report" fields payload.id = workerId; payload.report = payload.stats; this.events.emit('done', workerId, payload); } else if ( payload.event === 'phaseStarted' || payload.event === 'phaseCompleted' ) { payload.id = workerId; this.events.emit(payload.event, workerId, { phase: payload.phase }); } else if (payload.event === 'workerError') { global.artillery.suggestedExitCode = payload.exitCode || 1; if (payload.exitCode !== 21) { this.events.emit(payload.event, workerId, { id: workerId, error: new Error( `A worker has exited with an error. Reason: ${payload.reason}` ), level: 'error', aggregatable: false, logs: payload.logs }); } } else if (payload.event === 'workerReady') { this.events.emit(payload.event, workerId); this.waitingReadyCount++; // TODO: Do this only for batches of workers with "wait" option set if (this.waitingReadyCount === this.count) { await this.sendGoSignal(); } } else { debug(payload); } } } ); consumer.on('error', (err) => { console.error(err); }); this.queueConsumer = consumer; const metadata = { region: this.region, platformConfig: { memory: this.memory, cpu: this.cpu } }; global.artillery.globalEvents.emit('metadata', metadata); } getDesiredWorkerCount() { return this.platformOpts.count; } async startJob() { await this.init(); console.log('Creating container instances...'); // Create & run the leader: const { workerId } = await this.createWorker(); this.workers[workerId] = { workerId }; await this.runWorker(workerId, { isLeader: true }); // Run the rest of the containers we need: for (let i = 0; i < this.platformOpts.count - 1; i++) { const { workerId } = await this.createWorker(); this.workers[workerId] = { workerId }; await this.runWorker(workerId); if (i > 0 && i % 10 === 0) { const delayMs = Math.floor( Math.random() * parseInt(process.env.AZURE_LAUNCH_STAGGER_SEC || '5', 10) ) * 1000; await sleep(delayMs); } } let instancesCreated = false; console.log('Waiting for Azure ACI to create container instances...'); const containerInstanceClient = new ContainerInstanceManagementClient( new DefaultAzureCredential(), this.azureSubscriptionId ); const provisioningWaitTimeout = new Timeout(WAIT_TIMEOUT * 1000).start(); let containerGroupsInTestRun = []; while (true) { const containerGroupListResult = containerInstanceClient.containerGroups.listByResourceGroup( this.resourceGroupName ); containerGroupsInTestRun = []; for await (const containerGroup of containerGroupListResult) { if (containerGroup.name.indexOf(this.testRunId) > 0) { containerGroupsInTestRun.push(containerGroup); } } const byStatus = containerGroupsInTestRun.reduce((acc, cg) => { if (!acc[cg.provisioningState]) { acc[cg.provisioningState] = 0; } acc[cg.provisioningState]++; return acc; }, {}); if ( (byStatus.Succeeded || 0) + (byStatus.Running || 0) === this.count ) { instancesCreated = true; break; } if (provisioningWaitTimeout.timedout()) { break; } await sleep(10000); } if (instancesCreated) { console.log( 'Container instances have been created. Waiting for workers to start...' ); await this.queueConsumer.start(); } else { console.log('Some containers instances failed to provision'); console.log('Please see the Azure console for details'); console.log( 'https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/Microsoft.ContainerInstance%2FcontainerGroups' ); await global.artillery.shutdown(); } } async shutdown() { this.queueConsumer.stop(); try { await this.aqsClient.delete(); } catch (_err) {} const credential = new DefaultAzureCredential(); if (process.env.RETAIN_CONTAINER_INSTANCES !== 'true') { const containerInstanceClient = new ContainerInstanceManagementClient( credential, this.azureSubscriptionId ); const containerGroupListResult = containerInstanceClient.containerGroups.listByResourceGroup( this.resourceGroupName ); for await (const containerGroup of containerGroupListResult) { if (containerGroup.name.indexOf(this.testRunId) > 0) { try { await containerInstanceClient.containerGroups.beginDeleteAndWait( this.resourceGroupName, containerGroup.name ); } catch (err) { console.log(err); } } } } } async sendGoSignal() { const Key = `tests/${this.testRunId}/go.json`; const blockBlobClient = this.blobContainerClient.getBlockBlobClient(Key); const _res = await blockBlobClient.upload('', 0); } async createWorker() { const workerId = generateId('worker'); return { workerId }; } async runWorker(workerId, opts = { isLeader: false }) { const credential = new DefaultAzureCredential(); const imageVersion = process.env.ARTILLERY_WORKER_IMAGE_VERSION || IMAGE_VERSION; const defaultArchitecture = 'x86_64'; const containerImageURL = process.env.WORKER_IMAGE_URL || `public.ecr.aws/d8a4z9o5/artillery-worker:${imageVersion}-${defaultArchitecture}`; const client = new ContainerInstanceManagementClient( credential, this.azureSubscriptionId ); const environmentVariables = [ { name: 'WORKER_ID_OVERRIDE', value: workerId }, { name: 'ARTILLERY_TEST_RUN_ID', value: this.testRunId }, // { // name: 'DEBUGX', // value: 'true', // }, { name: 'DEBUG', value: 'cloud' }, { name: 'IS_LEADER', value: String(opts.isLeader) }, { name: 'AQS_QUEUE_NAME', value: this.queueName }, { name: 'AZURE_STORAGE_ACCOUNT', value: this.storageAccount }, { name: 'AZURE_STORAGE_BLOB_CONTAINER', value: this.blobContainerName }, { name: 'AZURE_SUBSCRIPTION_ID', secureValue: this.azureSubscriptionId }, { name: 'AZURE_TENANT_ID', secureValue: this.azureTenantId }, { name: 'AZURE_CLIENT_ID', secureValue: this.azureClientId }, { name: 'AZURE_CLIENT_SECRET', secureValue: this.azureClientSecret }, { name: 'AZURE_STORAGE_AUTH_MODE', value: 'login' } ]; if (this.cloudKey) { environmentVariables.push({ name: 'ARTILLERY_CLOUD_API_KEY', secureValue: this.cloudKey }); } const cloudEndpoint = process.env.ARTILLERY_CLOUD_ENDPOINT; if (cloudEndpoint) { environmentVariables.push({ name: 'ARTILLERY_CLOUD_ENDPOINT', secureValue: cloudEndpoint }); } for (const [name, value] of Object.entries(this.extraEnvVars)) { environmentVariables.push({ name, value }); } const containerGroup = { location: this.region, containers: [ { name: 'artillery-worker', image: containerImageURL, resources: { requests: { cpu: this.cpu, memoryInGB: this.memory } }, command: [ '/artillery/loadgen-worker', '-z', 'yes', // yes for Azure '-q', this.queueUrl, '-p', this.blobContainerName, '-a', util.btoa(JSON.stringify(this.artilleryArgs)), '-i', this.testRunId, '-t', String(WAIT_TIMEOUT), '-d', 'NOT_USED_ON_AZURE', '-r', 'NOT_USED_ON_AZURE' ], environmentVariables } ], osType: 'Linux', restartPolicy: 'Never' }; if (!this.ts) { this.ts = Date.now(); } const containerGroupName = `artillery-test-${this.ts}-${this.testRunId}-${this.count}`; try { const containerInstance = await client.containerGroups.beginCreateOrUpdate( this.resourceGroupName, containerGroupName, containerGroup ); this.containerInstances.push(containerInstance); this.count++; } catch (err) { // TODO: Make this better console.log(err.code); console.log(err.details?.error?.message); throw err; } } async stopWorker(_workerId) {} async checkLicense() { const request = (await import('got')).default; const baseUrl = process.env.ARTILLERY_CLOUD_ENDPOINT || 'https://app.artillery.io'; const res = await request.get(`${baseUrl}/api/user/whoami`, { headers: { 'x-auth-token': this.cloudKey }, throwHttpErrors: false, retry: { limit: 3 } }); try { const body = JSON.parse(res.body); const activeOrg = body.activeOrg; if (!activeOrg) { return false; } if (!Array.isArray(body.memberships)) { return false; } const activeMembership = body.memberships.find( (membership) => membership.id === activeOrg ); if (!activeMembership) { return false; } const plan = activeMembership.plan; return plan !== 'developer'; } catch (_err) { return false; } } } module.exports = PlatformAzureACI; ================================================ FILE: packages/artillery/lib/platform/az/aqs-queue-consumer.js ================================================ // Copyright (c) Artillery Software Inc. // SPDX-License-Identifier: BUSL-1.1 // // Non-evaluation use of Artillery on Azure requires a commercial license // const EventEmitter = require('eventemitter3'); const { QueueClient } = require('@azure/storage-queue'); const { DefaultAzureCredential } = require('@azure/identity'); const debug = require('debug')('platform:azure-aci'); class AzureQueueConsumer extends EventEmitter { constructor( opts = { poolSize: 30 }, { queueUrl, pollIntervalMsec = 5000, visibilityTimeout = 60, batchSize = 32, handleMessage } ) { super(); this.queueUrl = queueUrl; this.batchSize = batchSize; this.visibilityTimeout = visibilityTimeout; this.handleMessage = handleMessage; this.pollIntervalMsec = pollIntervalMsec; this.poolSize = opts.poolSize; this.consumers = []; } async start() { const credential = new DefaultAzureCredential(); for (let i = 0; i < this.poolSize; i++) { debug('Creating consumer in pool', i); const queueClient = new QueueClient(this.queueUrl, credential); const pollInterval = setInterval(async () => { const messages = await queueClient.receiveMessages({ numberOfMessages: this.batchSize, visibilityTimeout: this.visibilityTimeout }); // TODO: Handle errors - no auth, no queue, network etc for (const messageItem of messages.receivedMessageItems) { const message = { Body: messageItem.messageText }; let processed = false; try { await this.handleMessage(message); processed = true; } catch (err) { console.log(err); } if (processed) { try { await queueClient.deleteMessage( messageItem.messageId, messageItem.popReceipt ); } catch (_err) {} } } }, this.pollIntervalMsec); this.consumers.push(pollInterval); } } async stop() { for (const interval of this.consumers) { clearInterval(interval); } } // TODO: events: error, empty } module.exports = { QueueConsumer: AzureQueueConsumer }; ================================================ FILE: packages/artillery/lib/platform/az/regions.js ================================================ const regionNames = [ 'australiacentral', 'australiacentral2', 'australiaeast', 'australiasoutheast', 'brazilsouth', 'canadacentral', 'canadaeast', 'centralindia', 'centralus', 'eastasia', 'eastus', 'eastus2', 'francecentral', 'francesouth', 'germanynorth', 'germanywestcentral', 'israelcentral', 'italynorth', 'japaneast', 'japanwest', 'jioindiawest', 'koreacentral', 'koreasouth', 'mexicocentral', 'northcentralus', 'northeurope', 'norwayeast', 'norwaywest', 'polandcentral', 'qatarcentral', 'southafricanorth', 'southafricawest', 'southcentralus', 'southeastasia', 'southindia', 'spaincentral', 'swedencentral', 'switzerlandnorth', 'switzerlandwest', 'uaecentral', 'uaenorth', 'uksouth', 'ukwest', 'westcentralus', 'westeurope', 'westindia', 'westus', 'westus2' ]; module.exports = { regionNames }; ================================================ FILE: packages/artillery/lib/platform/cloud/api.js ================================================ const { getCloudHttpClient } = require('./http-client'); class Client { constructor({ apiKey, baseUrl }) { this.apiKey = apiKey || process.env.ARTILLERY_CLOUD_API_KEY; if (!apiKey) { const err = new Error(); err.name = 'CloudAPIKeyMissing'; throw err; } this.baseUrl = baseUrl || process.env.ARTILLERY_CLOUD_ENDPOINT || 'https://app.artillery.io'; this.whoamiEndpoint = `${this.baseUrl}/api/user/whoami`; this.stashDetailsEndpoint = `${this.baseUrl}/api/stash`; this.defaultHeaders = { 'x-auth-token': this.apiKey }; } async whoami() { const request = await getCloudHttpClient(); const res = await request.get(this.whoamiEndpoint, { headers: this.defaultHeaders }); const body = JSON.parse(res.body); this.orgId = body.activeOrg; return body; } async getStashDetails({ orgId }) { const request = await getCloudHttpClient(); const currentOrgId = orgId || this.orgId; const res = await request.get( `${this.baseUrl}/api/org/${currentOrgId}/stash`, { headers: this.defaultHeaders } ); if (res.statusCode === 200) { let body = {}; try { body = JSON.parse(res.body); } catch (err) { console.error(err); return null; } if (body.url && body.token) { return { url: body.url, token: body.token }; } else { return null; } } else { return null; } } } function createClient(opts) { return new Client(opts); } module.exports = { createClient }; ================================================ FILE: packages/artillery/lib/platform/cloud/cloud.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const debug = require('debug')('cloud'); const { getCloudHttpClient } = require('./http-client'); const awaitOnEE = require('../../util/await-on-ee'); const sleep = require('../../util/sleep'); const util = require('node:util'); const chokidar = require('chokidar'); const fs = require('node:fs'); const path = require('node:path'); const { isCI, name: ciName, GITHUB_ACTIONS } = require('ci-info'); class ArtilleryCloudPlugin { constructor(_script, _events, { flags }) { this.enabled = false; const isInteractiveUse = typeof flags.record !== 'undefined'; const enabledInCloudWorker = typeof process.env.WORKER_ID !== 'undefined' && typeof process.env.ARTILLERY_CLOUD_API_KEY !== 'undefined'; if (!isInteractiveUse && !enabledInCloudWorker) { return; } this.enabled = true; this.apiKey = flags.key || process.env.ARTILLERY_CLOUD_API_KEY; this.baseUrl = process.env.ARTILLERY_CLOUD_ENDPOINT || 'https://app.artillery.io'; this.eventsEndpoint = `${this.baseUrl}/api/events`; this.whoamiEndpoint = `${this.baseUrl}/api/user/whoami`; this.getAssetUploadUrls = `${this.baseUrl}/api/asset-upload-urls`; this.pingEndpoint = `${this.baseUrl}/api/ping`; this.defaultHeaders = { 'x-auth-token': this.apiKey }; this.unprocessedLogsCounter = 0; this.cancellationRequestedBy = ''; let testEndInfo = {}; // This value is available in cloud workers only. With interactive use, it'll be set // in the test:init event handler. this.testRunId = process.env.ARTILLERY_TEST_RUN_ID; if (isInteractiveUse) { global.artillery.globalEvents.on('test:init', async (testInfo) => { debug('test:init', testInfo); this.testRunId = testInfo.testRunId; const testRunUrl = `${this.baseUrl}/${this.orgId}/load-tests/${global.artillery.testRunId}`; testEndInfo.testRunUrl = testRunUrl; this.getLoadTestEndpoint = `${this.baseUrl}/api/load-tests/${this.testRunId}/status`; let ciURL = null; if (isCI && GITHUB_ACTIONS) { const { GITHUB_SERVER_URL, GITHUB_REPOSITORY, GITHUB_RUN_ID } = process.env; if (GITHUB_SERVER_URL && GITHUB_REPOSITORY && GITHUB_RUN_ID) { ciURL = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`; } } const metadata = Object.assign({}, testInfo.metadata, { isCI, ciName, ciURL }); await this._event('testrun:init', { metadata: metadata }); this.setGetLoadTestInterval = this.setGetStatusInterval(); if (typeof testInfo.flags.note !== 'undefined') { await this._event('testrun:addnote', { text: testInfo.flags.note }); } this.uploading = 0; }); global.artillery.globalEvents.on('phaseStarted', async (phase) => { await this._event('testrun:event', { eventName: 'phaseStarted', eventAttributes: phase }); }); global.artillery.globalEvents.on('phaseCompleted', async (phase) => { await this._event('testrun:event', { eventName: 'phaseCompleted', eventAttributes: phase }); }); global.artillery.globalEvents.on('stats', async (report) => { debug('stats', new Date()); const ts = Number(report.period); await this._event('testrun:metrics', { report, ts }); }); global.artillery.globalEvents.on('done', async (report) => { debug('done'); debug( 'testrun:aggregatereport: payload size:', JSON.stringify(report).length ); await this._event('testrun:aggregatereport', { aggregate: report }); }); global.artillery.globalEvents.on('checks', async (checks) => { debug('checks'); await this._event('testrun:checks', { checks }); }); global.artillery.globalEvents.on('logLines', async (lines, ts) => { debug('logLines event', ts); this.unprocessedLogsCounter += 1; let text = ''; try { JSON.stringify(lines); } catch (stringifyErr) { console.log('Could not serialize console log'); console.log(stringifyErr); } for (const args of lines) { text += util.format(...Object.keys(args).map((k) => args[k])) + '\n'; } try { await this._event('testrun:textlog', { lines: text, ts }); } catch (err) { debug(err); } finally { this.unprocessedLogsCounter -= 1; } debug('last 100 characters:'); debug(text.slice(text.length - 100, text.length)); }); global.artillery.globalEvents.on('metadata', async (metadata) => { await this._event('testrun:addmetadata', { metadata }); }); } // isInteractiveUse global.artillery.ext({ ext: 'beforeExit', method: async ({ testInfo, report }) => { debug('beforeExit'); testEndInfo = { ...testEndInfo, ...testInfo, report }; } }); // Send test end events just before the CLI shuts down. This ensures that all console // output has been captured and sent to the dashboard. global.artillery.ext({ ext: 'onShutdown', method: async (opts) => { if (!this.enabled || this.off) { return; } if (isInteractiveUse) { clearInterval(this.setGetLoadTestInterval); // Wait for the last logLines events to be processed, as they can sometimes finish processing after shutdown has finished await awaitOnEE( global.artillery.globalEvents, 'logLines', 200, 1 * 1000 //wait at most 1 second for a final log lines event emitter to be fired ); } await this.waitOnUnprocessedLogs(5 * 60 * 1000); //just waiting for ee is not enough, as the api call takes time if (isInteractiveUse) { await this._event('testrun:end', { ts: testEndInfo.endTime, exitCode: global.artillery.suggestedExitCode || opts.exitCode, isEarlyStop: !!opts.earlyStop, report: testEndInfo.report }); console.log('\n'); if (this.cancellationRequestedBy) { console.log(`Test run stopped by ${this.cancellationRequestedBy}.`); } console.log(`Run URL: ${testEndInfo.testRunUrl}`); } } }); } async init() { this.request = await getCloudHttpClient(); if (!this.apiKey) { const err = new Error(); err.name = 'CloudAPIKeyMissing'; this.off = true; throw err; } let res; let body; try { res = await this.request.get(this.whoamiEndpoint, { headers: this.defaultHeaders, retry: { limit: 0 } }); body = JSON.parse(res.body); debug(res.body); this.orgId = body.activeOrg; } catch (err) { this.off = true; throw err; } if (res.statusCode === 401) { const err = new Error(); err.name = 'APIKeyUnauthorized'; this.off = true; throw err; } let postSucceeded = false; try { res = await this.request.post(this.pingEndpoint, { headers: this.defaultHeaders }); if (res.statusCode === 200) { postSucceeded = true; } } catch (_err) { this.off = true; } if (!postSucceeded) { const err = new Error(); err.name = 'PingFailed'; this.off = true; throw err; } console.log('Artillery Cloud reporting is configured for this test run'); console.log( `Run URL: ${this.baseUrl}/${this.orgId}/load-tests/${global.artillery.testRunId}` ); this.user = { id: body.id, email: body.email }; const outputDir = process.env.PLAYWRIGHT_TRACING_OUTPUT_DIR || `/tmp/${global.artillery.testRunId}/`; try { fs.mkdirSync(outputDir, { recursive: true }); } catch (_err) {} const watcher = chokidar.watch(outputDir, { ignored: /(^|[/\\])\../, // ignore dotfiles persistent: true, ignorePermissionErrors: true, ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 500 } }); watcher.on('add', (fp) => { if (path.basename(fp).startsWith('trace-') && fp.endsWith('.zip')) { this.uploading++; this._uploadAsset(fp); } }); } async _uploadAsset(localFilename) { const payload = { testRunId: this.testRunId, filenames: [path.basename(localFilename)] }; debug(payload); let url; try { // TODO: This could get rejected if a limit is exceeded so need to handle that case const res = await this.request.post(this.getAssetUploadUrls, { headers: this.defaultHeaders, json: payload }); const body = JSON.parse(res.body); debug(body); url = body.urls[path.basename(localFilename)]; } catch (err) { debug(err); } if (!url) { return; } const fileStream = fs.createReadStream(localFilename); try { const _response = await this.request.put(url, { body: fileStream }); } catch (error) { console.error('Failed to upload Playwright trace recording:', error); console.log(error.code, error.name, error.message, error.stack); } finally { this.uploading--; artillery.globalEvents.emit('counter', 'browser.traces.uploaded', 1); try { fs.unlinkSync(localFilename); } catch (err) { debug(err); } } } async waitOnUnprocessedLogs(maxWaitTime) { let waitedTime = 0; while ( (this.unprocessedLogsCounter > 0 || this.uploading > 0) && waitedTime < maxWaitTime ) { debug('waiting on unprocessed logs'); await sleep(500); waitedTime += 500; } return true; } setGetStatusInterval() { const interval = setInterval(async () => { if (this.cancellationRequestedBy) { return; } const res = await this._getLoadTestStatus(); if (!res) { debug('No response from Artillery Cloud get status'); return; } if (res.status !== 'CANCELLATION_REQUESTED') { return; } console.log( `WARNING: Artillery Cloud user ${res.cancelledBy} requested to stop the test. Stopping test run - this may take a few seconds.` ); this.cancellationRequestedBy = res.cancelledBy; global.artillery.suggestedExitCode = 8; await global.artillery.shutdown({ earlyStop: true }); }, 5000); return interval; } async _getLoadTestStatus() { debug('☁️', 'Getting load test status'); try { const res = await this.request.get(this.getLoadTestEndpoint, { headers: this.defaultHeaders }); return JSON.parse(res.body); } catch (error) { debug(error); } } async _event(eventName, eventPayload) { debug('☁️', eventName, eventPayload); try { const res = await this.request.post(this.eventsEndpoint, { headers: this.defaultHeaders, json: { eventType: eventName, eventData: Object.assign({}, eventPayload, { testRunId: this.testRunId }) }, retry: { limit: 2 } }); if (res.statusCode !== 200) { if (res.statusCode === 401) { console.log( 'Error: API key is invalid. Could not send test data to Artillery Cloud.' ); } else { console.log('Error: error sending test data to Artillery Cloud'); console.log('Test report may be incomplete'); } let body; try { body = JSON.parse(res.body); } catch (_err) {} if (body?.requestId) { console.log('Request ID:', body.requestId); } } debug('☁️', eventName, 'sent'); } catch (err) { debug(err); } } cleanup(done) { debug('cleaning up'); done(null); } } module.exports.Plugin = ArtilleryCloudPlugin; ================================================ FILE: packages/artillery/lib/platform/cloud/http-client.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const DEFAULT_TIMEOUT_MS = 20 * 10000; const DEFAULT_RETRY_LIMIT = 3; let _client; async function getCloudHttpClient() { if (!_client) { const got = (await import('got')).default; _client = got.extend({ timeout: { response: DEFAULT_TIMEOUT_MS }, retry: { limit: DEFAULT_RETRY_LIMIT, methods: ['GET', 'POST', 'PUT'] }, throwHttpErrors: false }); } return _client; } module.exports = { getCloudHttpClient }; ================================================ FILE: packages/artillery/lib/platform/local/artillery-worker-local.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const EventEmitter = require('eventemitter3'); const { Worker } = require('node:worker_threads'); const path = require('node:path'); const STATES = require('../worker-states'); const awaitOnEE = require('../../util/await-on-ee'); const returnWorkerEnv = (needsSourcemap) => { const env = { ...process.env }; if (needsSourcemap) { env.NODE_OPTIONS = process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} --enable-source-maps` : '--enable-source-maps'; } return env; }; class ArtilleryWorker { constructor(opts) { this.opts = opts; this.events = new EventEmitter(); // events for consumers of this object this.workerEvents = new EventEmitter(); // turn events delivered via 'message' events into their own messages } async init(_opts) { this.state = STATES.initializing; const workerEnv = returnWorkerEnv(global.artillery.hasTypescriptProcessor); this.worker = new Worker(path.join(__dirname, 'worker.js'), { env: workerEnv }); this.workerId = this.worker.threadId; this.worker.on('error', this.onError.bind(this)); // TODO: this.worker.on('exit', (exitCode) => { this.events.emit('exit', exitCode); }); this.worker.on('messageerror', (_err) => {}); // TODO: Expose performance metrics via getHeapSnapshot() and performance object. await awaitOnEE(this.worker, 'online', 10); // Relay messages onto the real event emitter: this.worker.on('message', (message) => { switch (message.event) { case 'log': this.events.emit('log', message); this.workerEvents.emit('log', message); break; case 'workerError': this.events.emit('workerError', message); this.workerEvents.emit('workerError', message); break; case 'phaseStarted': this.events.emit('phaseStarted', message); this.workerEvents.emit('phaseStarted', message); break; case 'phaseCompleted': this.events.emit('phaseCompleted', message); this.workerEvents.emit('phaseCompleted', message); break; case 'stats': this.events.emit('stats', message); this.workerEvents.emit('stats', message); break; case 'done': this.events.emit('done', message); this.workerEvents.emit('done', message); break; case 'running': this.events.emit('running', message); this.workerEvents.emit('running', message); break; case 'readyWaiting': this.events.emit('readyWaiting', message); this.workerEvents.emit('readyWaiting', message); break; case 'setSuggestedExitCode': this.events.emit('setSuggestedExitCode', message); break; default: global.artillery.log( `Unknown message from worker ${message}`, 'error' ); } }); this.state = STATES.online; } async prepare(opts) { this.state = STATES.preparing; const { script, payload, options } = opts; let scriptForWorker = script; if (script.__transpiledTypeScriptPath && script.__originalScriptPath) { scriptForWorker = { __transpiledTypeScriptPath: script.__transpiledTypeScriptPath, __originalScriptPath: script.__originalScriptPath, __phases: script.config?.phases }; } this.worker.postMessage({ command: 'prepare', opts: { script: scriptForWorker, payload, options, testRunId: global.artillery.testRunId } }); await awaitOnEE(this.workerEvents, 'readyWaiting', 50); this.state = STATES.readyWaiting; } async run(opts) { this.worker.postMessage({ command: 'run', opts: JSON.parse(opts) }); await awaitOnEE(this.workerEvents, 'running', 50); this.state = STATES.running; } async stop() { this.worker.postMessage({ command: 'stop' }); } onError(err) { // TODO: set state, clean up this.events.emit('error', err); console.log('worker error, id:', this.workerId, err); } } module.exports = { ArtilleryWorker, STATES }; ================================================ FILE: packages/artillery/lib/platform/local/index.js ================================================ const { ArtilleryWorker } = require('./artillery-worker-local'); const core = require('../../dispatcher'); const { handleScriptHook, prepareScript, loadProcessor } = core.runner.runnerFuncs; const debug = require('debug')('platform:local'); const EventEmitter = require('node:events'); const _ = require('lodash'); const divideWork = require('../../dist'); const STATES = require('../worker-states'); const os = require('node:os'); class PlatformLocal { constructor(script, payload, opts, platformOpts) { // We need these to run before/after hooks: this.script = script; this.payload = payload; this.opts = opts; this.events = new EventEmitter(); // send worker events such as workerError, etc this.platformOpts = platformOpts; this.workers = {}; this.workerScripts = {}; this.count = Infinity; } getDesiredWorkerCount() { return this.count; } async startJob() { await this.init(); if (this.platformOpts.mode === 'distribute') { // Disable worker threads for Playwright-based load tests const count = this.script.config.engines?.playwright ? 1 : Math.max(1, os.cpus().length - 1); this.workerScripts = divideWork(this.script, count); this.count = this.workerScripts.length; } else { // --count may only be used when mode is "multiply" this.count = this.platformOpts.count; this.workerScripts = new Array(this.count).fill().map((_) => this.script); } for (const script of this.workerScripts) { const w1 = await this.createWorker(); this.workers[w1.workerId] = { id: w1.workerId, script, state: STATES.initializing, proc: w1 }; debug(`worker init ok: ${w1.workerId}`); } for (const [workerId, w] of Object.entries(this.workers)) { this.opts.cliArgs = this.platformOpts.cliArgs; await this.prepareWorker(workerId, { script: w.script, payload: this.payload, options: this.opts }); this.workers[workerId].state = STATES.preparing; } debug('workers prepared'); // the initial context is stringified and copied to the workers const contextVarsString = JSON.stringify(this.contextVars); for (const [workerId, _w] of Object.entries(this.workers)) { await this.runWorker(workerId, contextVarsString); this.workers[workerId].state = STATES.initializing; } } async init() { // 'before' hook is executed in the main thread, // its context is then passed to the workers const contextVars = await this.runHook('before'); this.contextVars = contextVars; // TODO: Rename to something more descriptive } async createWorker() { const worker = new ArtilleryWorker(); await worker.init(); const workerId = worker.workerId; worker.events.on('workerError', (message) => { this.events.emit('workerError', workerId, message); }); worker.events.on('log', (message) => { this.events.emit('log', workerId, message); }); worker.events.on('phaseStarted', (message) => { this.events.emit('phaseStarted', workerId, message); }); worker.events.on('phaseCompleted', (message) => { this.events.emit('phaseCompleted', workerId, message); }); worker.events.on('stats', (message) => { this.events.emit('stats', workerId, message); }); worker.events.on('done', (message) => { this.events.emit('done', workerId, message); }); worker.events.on('readyWaiting', (message) => { this.events.emit('readyWaiting', workerId, message); }); worker.events.on('setSuggestedExitCode', (message) => { this.events.emit('setSuggestedExitCode', workerId, message); }); worker.events.on('exit', (message) => { this.events.emit('exit', workerId, message); }); worker.events.on('error', (_err) => { // TODO: Only exit if ALL workers fail, otherwise log and carry on process.nextTick(() => process.exit(11)); }); return worker; } async prepareWorker(workerId, opts) { return this.workers[workerId].proc.prepare(opts); } async runWorker(workerId, contextVarsString) { // TODO: this will become opts debug('runWorker', workerId); return this.workers[workerId].proc.run(contextVarsString); } async stopWorker(workerId) { return this.workers[workerId].proc.stop(); } async shutdown() { // 'after' hook is executed in the main thread, after all workers // are done await this.runHook('after', this.contextVars); for (const [workerId, _w] of Object.entries(this.workers)) { await this.stopWorker(workerId); } } // ******** async runHook(hook, initialContextVars) { if (!this.script[hook]) { return {}; } const runnableScript = await loadProcessor( prepareScript(this.script, _.cloneDeep(this.payload)), this.opts ); const contextVars = await handleScriptHook( hook, runnableScript, this.events, initialContextVars ); debug(`hook ${hook} context vars`, contextVars); return contextVars; } } module.exports = PlatformLocal; ================================================ FILE: packages/artillery/lib/platform/local/worker.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // // Artillery Core worker process // const { parentPort, threadId } = require('node:worker_threads'); const { getStash } = require('../../../lib/stash'); const { createGlobalObject } = require('../../artillery-global'); const core = require('@artilleryio/int-core'); const createRunner = core.runner.runner; const debug = require('debug')('artillery:worker'); const _path = require('node:path'); const { SSMS } = require('@artilleryio/int-core').ssms; const { loadPlugins, loadPluginsConfig } = require('../../load-plugins'); const EventEmitter = require('eventemitter3'); const p = require('node:util').promisify; const { loadProcessor } = core.runner.runnerFuncs; const prepareTestExecutionPlan = require('../../util/prepare-test-execution-plan'); process.env.LOCAL_WORKER_ID = threadId; parentPort.on('message', onMessage); let shuttingDown = false; let runnerInstance = null; global.artillery._workerThreadSend = send; // // Supported messages: run, stop // async function onMessage(message) { if (message.command === 'prepare') { await prepare(message.opts); return; } if (message.command === 'run') { run(message.opts); return; } if (message.command === 'stop') { await cleanup(); // Unload plugins // TODO: v3 plugins for (const o of global.artillery.plugins) { if (o.plugin.cleanup) { try { await p(o.plugin.cleanup.bind(o.plugin))(); debug('plugin unloaded:', o.name); } catch (cleanupErr) { send({ event: 'workerError', error: cleanupErr, level: 'error', aggregatable: true }); } } } process.exit(0); } } async function cleanup() { return new Promise((resolve, _reject) => { if (shuttingDown) { resolve(); } shuttingDown = true; if (runnerInstance && typeof runnerInstance.stop === 'function') { runnerInstance.stop().then(() => { resolve(); }); } else { resolve(); } }); } async function createGlobalStashClient(cliArgs) { try { global.artillery.stash = await getStash({ apiKey: cliArgs?.key || process.env.ARTILLERY_CLOUD_API_KEY }); } catch (error) { if (error.name !== 'CloudAPIKeyMissing') { console.error(error); } global.artillery.stash = null; } } async function prepare(opts) { await createGlobalObject(); await createGlobalStashClient(opts.options.cliArgs); global.artillery.globalEvents.on('log', (...args) => { send({ event: 'log', args }); }); let _script; if ( opts.script.__transpiledTypeScriptPath && opts.script.__originalScriptPath ) { // Load and process pre-compiled TypeScript file _script = await prepareTestExecutionPlan( [opts.script.__originalScriptPath], opts.options.cliArgs, [] ); } else { _script = opts.script; } const { payload, options } = opts; const script = await loadProcessor(_script, options); if (opts.script.__phases) { script.config.phases = opts.script.__phases; } global.artillery.testRunId = opts.testRunId; // // load plugins // const plugins = await loadPlugins(script.config.plugins, script, options); // NOTE: We don't subscribe plugins to stats/done events from // individual runner instances here - those are handled in // launch-platform instead. (If we subscribe plugins to events here, // they will receive individual stats/done events from workers, // instead of objects that have been properly aggregated.) const stubEE = new EventEmitter(); for (const [name, result] of Object.entries(plugins)) { if (result.isLoaded) { global.artillery.plugins[name] = result.plugin; if (result.version === 3) { // TODO: v3 plugins } else { // const msg = `WARNING: Legacy plugin detected: ${name} // See https://artillery.io/docs/resources/core/v2.html for more details.`; // send({ // event: 'workerError', // error: new Error(msg), // level: 'warn', // aggregatable: true // }); script.config = { ...script.config, // Load additional plugins configuration from the environment plugins: loadPluginsConfig(script.config.plugins) }; if (result.version === 1) { result.plugin = new result.PluginExport(script.config, stubEE); global.artillery.plugins.push(result); } else if (result.version === 2) { result.plugin = new result.PluginExport.Plugin( script, stubEE, options ); global.artillery.plugins.push(result); } else { // TODO: } } } else { const msg = `WARNING: Could not load plugin: ${name}`; send({ event: 'workerError', error: new Error(msg), level: 'warn', aggregatable: true }); } } // TODO: use await createRunner(script, payload, options) .then((runner) => { runnerInstance = runner; runner.on('phaseStarted', onPhaseStarted); runner.on('phaseCompleted', onPhaseCompleted); runner.on('stats', onStats); runner.on('done', onDone); // TODO: Enum for all event types send({ event: 'readyWaiting' }); }) .catch((err) => { // TODO: Clean up and exit (error state) // TODO: Handle workerError in launcher when readyWaiting // is not received and worker exits. send({ event: 'workerError', error: err, level: 'error', aggregatable: true }); }); function onPhaseStarted(phase) { send({ event: 'phaseStarted', phase: phase }); } function onPhaseCompleted(phase) { send({ event: 'phaseCompleted', phase: phase }); } function onStats(stats) { send({ event: 'stats', stats: SSMS.serializeMetrics(stats) }); } async function onDone(report) { await runnerInstance.stop(); send({ event: 'done', report: SSMS.serializeMetrics(report) }); } } async function run(opts) { if (runnerInstance) { runnerInstance.run(opts); send({ event: 'running' }); } else { // TODO: Emit error / set state } } // TODO: id -> workerId, ts -> _ts function send(data) { const payload = Object.assign({ id: threadId, ts: Date.now() }, data); debug(payload); parentPort.postMessage(payload); } ================================================ FILE: packages/artillery/lib/platform/worker-states.js ================================================ module.exports = { initializing: 1, online: 2, preparing: 3, readyWaiting: 4, running: 5, unknown: 6, stoppedError: 7, completed: 8, stoppedEarly: 9, stoppedFailed: 10, timedout: 11 }; ================================================ FILE: packages/artillery/lib/queue-consumer/index.js ================================================ const { EventEmitter } = require('eventemitter3'); const debug = require('debug')('queue-consumer'); const { Consumer } = require('sqs-consumer'); class QueueConsumer extends EventEmitter { create(opts = { poolSize: 30 }, queueConsumerOpts) { this.events = new EventEmitter(); this.consumers = []; for (let i = 0; i < opts.poolSize; i++) { const sqsConsumer = Consumer.create(queueConsumerOpts); sqsConsumer.on('error', (err) => { // TODO: Ignore "SQSError: SQS delete message failed:" errors if (err.message?.match(/ReceiptHandle.+expired/i)) { debug(err.name, err.message); } else { sqsConsumer.stop(); this.emit('error', err); } }); let empty = 0; sqsConsumer.on('empty', () => { empty++; if (empty > 10) { this.emit('messageReceiveTimeout'); // TODO: } }); this.consumers.push(sqsConsumer); } return this; } constructor(_opts) { super(); } start() { for (const consumer of this.consumers) { consumer.start(); } } stop() { for (const consumer of this.consumers) { consumer.stop(); } } } module.exports = { QueueConsumer }; ================================================ FILE: packages/artillery/lib/stash.js ================================================ const { Redis } = require('@upstash/redis'); const { createClient } = require('./platform/cloud/api'); async function init(details) { if (details) { return new Redis({ url: details.url, token: details.token }); } else { return null; } } /** * Get an Artillery Stash client instance * * * @param {Object} options - Configuration options * @param {string} options.apiKey - Artillery Cloud API key (optional, can use ARTILLERY_CLOUD_API_KEY env var) * @returns {Promise} - Redis client instance or null if not available */ async function getStash(options = {}) { const cloud = createClient({ apiKey: options.apiKey || process.env.ARTILLERY_CLOUD_API_KEY }); const whoami = await cloud.whoami(); if (!whoami.activeOrg) { return null; } const stashDetails = await cloud.getStashDetails({ orgId: whoami.activeOrg }); if (!stashDetails) { return null; } return init(stashDetails); } module.exports = { initStash: init, getStash }; ================================================ FILE: packages/artillery/lib/telemetry.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const { version: artilleryVersion } = require('../package.json'); const { isCI, name: ciName } = require('ci-info'); const debug = require('debug')('telemetry'); const POSTHOG_TOKEN = '_uzX-_WJoVmE_tsLvu0OFD2tpd0HGz72D5sU1zM2hbs'; const notice = () => { console.log( 'Anonymized telemetry is on. Learn more: https://artillery.io/docs/resources/core/telemetry.html' ); }; const isEnabled = () => { return typeof process.env.ARTILLERY_DISABLE_TELEMETRY === 'undefined'; }; async function capture(eventName, data) { if (!isEnabled()) { return; } const debugEnabled = typeof process.env.ARTILLERY_TELEMETRY_DEBUG !== 'undefined'; const url = 'https://us.i.posthog.com/i/v0/e/'; const headers = { 'Content-Type': 'application/json' }; let telemetryDefaults = {}; try { telemetryDefaults = JSON.parse(process.env.ARTILLERY_TELEMETRY_DEFAULTS); } catch (_err) { /* empty */ } const properties = Object.assign( { ...data, $process_person_profile: false, version: artilleryVersion, os: process.platform, isCi: isCI, ciName: isCI ? ciName : undefined, $ip: 'not-collected' }, telemetryDefaults ); const payload = { api_key: POSTHOG_TOKEN, event: eventName, distinct_id: data.distinctId || 'artillery-core', properties }; if (debugEnabled) { console.log(`Telemetry data: ${JSON.stringify(payload.properties)}`); } try { await fetch(url, { method: 'POST', headers: headers, body: JSON.stringify(payload) }); } catch (err) { debug(err); } } module.exports = { notice, capture, isEnabled }; ================================================ FILE: packages/artillery/lib/util/await-on-ee.js ================================================ const sleep = require('./sleep'); async function awaitOnEE(ee, message, pollMs = 1000, maxWaitMs = Infinity) { let messageFired = false; let args = null; let waitedMs = 0; ee.once(message, (...eventArgs) => { messageFired = true; args = eventArgs; }); while (true && waitedMs < maxWaitMs) { if (messageFired) { break; } await sleep(pollMs); waitedMs += pollMs; } return args; } module.exports = awaitOnEE; ================================================ FILE: packages/artillery/lib/util/generate-id.js ================================================ const { customAlphabet } = require('nanoid'); function generateId(prefix = '') { const idf = customAlphabet('3456789abcdefghjkmnpqrtwxyz'); const testRunId = `${prefix}${idf(4)}_${idf(29)}_${idf(4)}`; return testRunId; } module.exports = generateId; ================================================ FILE: packages/artillery/lib/util/parse-tag-string.js ================================================ module.exports = function parseTagString(input) { const result = { tags: [], errors: [] }; if (!input) { return result; } const tagList = input.split(',').map((x) => x.trim()); for (const t of tagList) { const cs = t.split(':'); if (cs.length !== 2) { result.errors.push(t); } else { result.tags.push({ name: cs[0].trim(), value: cs[1].trim() }); } } return result; }; ================================================ FILE: packages/artillery/lib/util/prepare-test-execution-plan.js ================================================ const csv = require('csv-parse'); const fs = require('node:fs'); const path = require('node:path'); const p = require('node:util').promisify; const debug = require('debug')('artillery'); const { readScript, parseScript, addOverrides, addVariables, addDefaultPlugins, resolveConfigTemplates, checkConfig, resolveConfigPath } = require('../../util'); const validateScript = require('./validate-script'); const _ = require('lodash'); async function prepareTestExecutionPlan(inputFiles, flags, _args) { const scriptPath = inputFiles[0]; let script1 = {}; for (const fn of inputFiles) { const fn2 = fn.toLowerCase(); const absoluteFn = path.resolve(process.cwd(), fn); if ( fn2.endsWith('.yml') || fn2.endsWith('.yaml') || fn2.endsWith('.json') ) { const data = await readScript(absoluteFn); const parsedData = await parseScript(data); script1 = _.merge(script1, parsedData); } else { if (fn2.endsWith('.js')) { const parsedData = require(absoluteFn); script1 = _.merge(script1, parsedData); } else if (fn2.endsWith('.ts')) { const outputPath = path.join( path.dirname(absoluteFn), `dist/${path.basename(fn)}.js` ); const entryPoint = path.resolve(process.cwd(), fn); // TODO: external packages will have to be specified externally to the script transpileTypeScript(entryPoint, outputPath, []); debug('transpiled TypeScript file into JS. Bundled file:', outputPath); const parsedData = require(outputPath); script1 = _.merge(script1, parsedData); // These magic properties are used by the worker to load the transpiled file script1.__transpiledTypeScriptPath = outputPath; script1.__originalScriptPath = entryPoint; } else { console.log('Unknown file type', fn); console.log( 'Only JSON (.json), YAML (.yml/.yaml) and TypeScript (.ts) files are supported' ); console.log('https://docs.art/e/file-types'); throw new Error('Unknown file type'); } } } // We run the check here because subsequent steps can overwrite the target to undefined in // cases where the value of config.target is set to a value from the environment which // is not available at this point in time. Example: target is set to an environment variable // the value of which is only available at runtime in AWS Fargate const hasOriginalTarget = typeof script1.config.target !== 'undefined' || typeof script1.config.environments?.[flags.environment]?.target !== 'undefined'; script1 = await checkConfig(script1, scriptPath, flags); const script2 = await resolveConfigPath(script1, flags, scriptPath); const script3 = await addOverrides(script2, flags); const script4 = await addVariables(script3, flags); // The resolveConfigTemplates function expects the config and script path to be passed explicitly because it is used in Fargate as well where the two arguments will not be available on the script const script5 = await resolveConfigTemplates( script4, flags, script4._configPath, script4._scriptPath ); if (!script5.config.target && !hasOriginalTarget) { throw new Error('No target specified and no environment chosen'); } const validationError = validateScript(script5); if (validationError) { console.log(`Scenario validation error: ${validationError}`); process.exit(1); } const script6 = await readPayload(script5); if (typeof script6.config.phases === 'undefined' || flags.solo) { script6.config.phases = [ { duration: 1, arrivalCount: 1 } ]; } script6.config.statsInterval = script6.config.statsInterval || 30; const script7 = addDefaultPlugins(script5); const script8 = replaceProcessorIfTypescript(script7, scriptPath); return script8; } async function readPayload(script) { if (!script.config.payload) { return script; } for (const payloadSpec of script.config.payload) { const data = fs.readFileSync(payloadSpec.path, 'utf-8'); const csvOpts = Object.assign( { skip_empty_lines: typeof payloadSpec.skipEmptyLines === 'undefined' ? true : payloadSpec.skipEmptyLines, cast: typeof payloadSpec.cast === 'undefined' ? true : payloadSpec.cast, from_line: payloadSpec.skipHeader === true ? 2 : 1, delimiter: payloadSpec.delimiter || ',' }, payloadSpec.options ); const parsedData = await p(csv)(data, csvOpts); payloadSpec.data = parsedData; } return script; } function transpileTypeScript(entryPoint, outputPath, userExternalPackages) { const esbuild = require('esbuild-wasm'); esbuild.buildSync({ entryPoints: [entryPoint], outfile: outputPath, bundle: true, platform: 'node', format: 'cjs', sourcemap: 'inline', external: ['@playwright/test', ...userExternalPackages] }); return outputPath; } function replaceProcessorIfTypescript(script, scriptPath) { const relativeProcessorPath = script.config.processor; const userExternalPackages = script.config.bundling?.external || []; if (!relativeProcessorPath) { return script; } const extensionType = path.extname(relativeProcessorPath); if (extensionType !== '.ts') { return script; } const actualProcessorPath = path.resolve( path.dirname(scriptPath), relativeProcessorPath ); const processorFileName = path.basename(actualProcessorPath, extensionType); const processorDir = path.dirname(actualProcessorPath); const newProcessorPath = path.join( processorDir, `dist/${processorFileName}.js` ); //TODO: move require to top of file when Lambda bundle size issue is solved //must be conditionally required for now as this package is removed in Lambda for now to avoid bigger package sizes const esbuild = require('esbuild-wasm'); try { esbuild.buildSync({ entryPoints: [actualProcessorPath], outfile: newProcessorPath, bundle: true, platform: 'node', format: 'cjs', sourcemap: 'inline', external: ['@playwright/test', ...userExternalPackages] }); } catch (error) { throw new Error(`Failed to compile Typescript processor\n${error.message}`); } global.artillery.hasTypescriptProcessor = newProcessorPath; console.log( `Bundled Typescript file into JS. New processor path: ${newProcessorPath}` ); script.config.processor = newProcessorPath; return script; } module.exports = prepareTestExecutionPlan; ================================================ FILE: packages/artillery/lib/util/sleep.js ================================================ async function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } module.exports = sleep; ================================================ FILE: packages/artillery/lib/util/validate-script.js ================================================ const Joi = require('joi').defaults((schema) => schema.options({ allowUnknown: true, abortEarly: true }) ); const config = Joi.object({ target: Joi.string().when('environments', { not: Joi.exist(), then: Joi.required() }), http: Joi.object({ extendedMetrics: Joi.boolean(), maxSockets: Joi.number(), timeout: Joi.alternatives(Joi.number(), Joi.string()) }), environments: Joi.object(), processor: Joi.string(), phases: Joi.array(), engines: Joi.object() // payload: Joi.alternatives(Joi.object(), Joi.array()) }); const capture = Joi.object({ as: Joi.string().required() }); const httpMethodProps = { url: Joi.string().required(), headers: Joi.object(), cookie: Joi.object(), followRedirect: Joi.boolean(), qs: Joi.object(), gzip: Joi.boolean(), auth: Joi.object({ user: Joi.string(), pass: Joi.string() }), beforeRequest: Joi.array().items(Joi.string()).single(), afterResponse: Joi.array().items(Joi.string()).single(), capture: Joi.array().items(capture).single() }; const httpItems = { get: Joi.object(httpMethodProps), post: Joi.object(httpMethodProps), put: Joi.object(httpMethodProps), patch: Joi.object(httpMethodProps), delete: Joi.object(httpMethodProps) }; const socketioItems = { emit: Joi.any().when(Joi.ref('....engine'), { is: 'socketio', then: Joi.alternatives( Joi.object({ channel: Joi.string(), concat: Joi.boolean(), data: Joi.any() }), Joi.array().items(Joi.string()) ), otherwise: Joi.any() }) }; const wsItems = { connect: Joi.any().when(Joi.ref('....engine'), { is: 'ws', then: Joi.alternatives(Joi.object(), Joi.string()), otherwise: Joi.any() }), send: Joi.any() }; const flowItemSchema = Joi.object({ function: Joi.string(), log: Joi.string(), think: Joi.alternatives(Joi.number(), Joi.string()), loop: Joi.array(), ...httpItems, ...wsItems, ...socketioItems }).when('.loop', { is: Joi.exist(), then: Joi.object({ count: Joi.alternatives(Joi.number(), Joi.string()), over: Joi.alternatives(Joi.array(), Joi.string()) }), otherwise: Joi.when('...engine', { is: Joi.exist().valid('socketio'), then: Joi.object().max(4), otherwise: Joi.object().length(1) }) }); const scenarioItem = Joi.object({ name: Joi.string(), engine: Joi.string(), beforeScenario: Joi.array().items(Joi.string()).single(), afterScenario: Joi.array().items(Joi.string()).single(), flow: Joi.any().when('engine', { is: Joi.valid('socketio', 'ws', 'http'), then: Joi.array().items(flowItemSchema).required(), otherwise: Joi.array().items(Joi.any()) }) }); const beforeAfterSchema = Joi.object({ flow: Joi.when('engine', { is: Joi.exist(), then: Joi.when('engine', { is: Joi.valid('socketio', 'ws', 'http'), then: Joi.array().items(flowItemSchema).required(), otherwise: Joi.array().items(Joi.any()) }), otherwise: Joi.array().items(flowItemSchema).required() }) }); const schema = Joi.object({ config: config, scenarios: Joi.array().items(scenarioItem).required(), before: beforeAfterSchema, after: beforeAfterSchema }); module.exports = (script) => { const { error } = schema.validate(script); if (error?.details.length) { return error.details[0].message; } }; ================================================ FILE: packages/artillery/lib/util.js ================================================ const fs = require('node:fs'); const path = require('node:path'); const YAML = require('js-yaml'); const debug = require('debug')('util'); const moment = require('moment'); const _ = require('lodash'); const chalk = require('chalk'); const engineUtil = require('@artilleryio/int-commons').engine_util; const renderVariables = engineUtil._renderVariables; const template = engineUtil.template; const { contextFuncs } = require('@artilleryio/int-core').runner; const p = require('node:util').promisify; module.exports = { readScript, parseScript, addOverrides, addVariables, addDefaultPlugins, resolveConfigPath, resolveConfigTemplates, checkConfig, renderVariables, template, formatDuration, padded, rainbow }; async function readScript(scriptPath) { const data = p(fs.readFile)(scriptPath, 'utf-8'); return data; } async function parseScript(data) { return YAML.safeLoad(data); } async function addOverrides(script, flags) { if (!flags.overrides) { return script; } const o = JSON.parse(flags.overrides); const result = _.mergeWith( script, o, function customizer(_objVal, srcVal, _k, _obj, _src, _stack) { if (_.isArray(srcVal)) { return srcVal; } else { return undefined; } } ); return result; } async function addVariables(script, flags) { if (!flags.variables) { return script; } const variables = JSON.parse(flags.variables); script.config.variables = script.config.variables || {}; for (const [k, v] of Object.entries(variables)) { script.config.variables[k] = v; } return script; } function addDefaultPlugins(script) { const finalScript = _.cloneDeep(script); if (!script.config.plugins) { finalScript.config.plugins = {}; } const additionalPluginsAndOptions = { 'metrics-by-endpoint': { suppressOutput: true, stripQueryString: true } }; for (const [pluginName, pluginOptions] of Object.entries( additionalPluginsAndOptions )) { if (!finalScript.config.plugins[pluginName]) { finalScript.config.plugins[pluginName] = pluginOptions; } } return finalScript; } async function resolveConfigTemplates(script, flags, configPath, scriptPath) { const cliVariables = flags.variables ? JSON.parse(flags.variables) : {}; script.config = engineUtil.template(script.config, { vars: { $scenarioFile: scriptPath, $dirname: path.dirname(configPath), $testId: global.artillery.testRunId, $processEnvironment: process.env, $env: process.env, $environment: flags.environment, ...cliVariables }, funcs: contextFuncs }); return script; } async function checkConfig(script, scriptPath, flags) { script._environment = flags.environment; script.config = script.config || {}; if (flags.environment) { debug('environment specified: %s', flags.environment); if ( script.config.environments?.[flags.environment] ) { _.merge(script.config, script.config.environments[flags.environment]); } else { // TODO: Emit an event instead console.log( `WARNING: environment ${flags.environment} is set but is not defined in the script` ); } } if (flags.target && script.config) { script.config.target = flags.target; } // // Override/set config.tls if needed: // if (flags.insecure) { if (script.config.tls) { if (script.config.tls.rejectUnauthorized) { console.log( 'WARNING: TLS certificate validation enabled in the ' + 'test script, but explicitly disabled with ' + '-k/--insecure.' ); } script.config.tls.rejectUnauthorized = false; } else { script.config.tls = { rejectUnauthorized: false }; } } // // Turn config.payload into an array: // if (_.get(script, 'config.payload')) { // Is it an object or an array? if (_.isArray(script.config.payload)) { // an array - nothing to do } else if (_.isObject(script.config.payload)) { if (flags.payload && !_.get(script.config.payload, 'path')) { script.config.payload.path = path.resolve(process.cwd(), flags.payload); } else if (!flags.payload && !_.get(script.config.payload, 'path')) { console.log( 'WARNING: config.payload.path not set and payload file not specified with -p' ); } else if (flags.payload && _.get(script.config.payload, 'path')) { console.log( 'WARNING - both -p and config.payload.path are set, config.payload.path will be ignored.' ); script.config.payload.path = flags.payload; } else { // no -p but config.payload.path is set - nothing to do } // Make it an array script.config.payload = [script.config.payload]; } else { console.log('Ignoring config.payload, not an object or an array.'); } } // // Resolve all payload paths to absolute paths now: // const absoluteScriptPath = path.resolve(process.cwd(), scriptPath); _.forEach(script.config.payload, (payloadSpec) => { const resolvedPathToPayload = path.resolve( path.dirname(absoluteScriptPath), payloadSpec.path ); payloadSpec.path = resolvedPathToPayload; }); script._scriptPath = absoluteScriptPath; return script; } async function resolveConfigPath(script, flags, scriptPath) { if (!flags.config) { script._configPath = scriptPath; return script; } const absoluteConfigPath = path.resolve(process.cwd(), flags.config); script._configPath = absoluteConfigPath; if (!script.config.processor) { return script; } const processorPath = path.resolve( path.dirname(absoluteConfigPath), script.config.processor ); const stats = fs.statSync(processorPath, { throwIfNoEntry: false }); if (typeof stats === 'undefined') { // No file at that path - backwards compatibility mode: console.log( 'WARNING - config.processor is now resolved relative to the config file' ); console.log('Expected to find file at:', processorPath); } else { script.config.processor = processorPath; } return script; } function formatDuration(durationInMs) { const duration = moment.duration(durationInMs); const days = duration.days(); const hours = duration.hours(); const minutes = duration.minutes(); const seconds = duration.seconds(); const timeComponents = []; if (days) { timeComponents.push(`${days} ${maybePluralize(days, 'day')}`); } if (hours || days) { timeComponents.push(`${hours} ${maybePluralize(hours, 'hour')}`); } if (minutes || hours || days) { timeComponents.push(`${minutes} ${maybePluralize(minutes, 'minute')}`); } timeComponents.push(`${seconds} ${maybePluralize(seconds, 'second')}`); return timeComponents.join(', '); } function maybePluralize(amount, singular, plural = `${singular}s`) { return amount === 1 ? singular : plural; } function padded(str1, str2, length = 79, formatPadding = chalk.gray) { const truncated = maybeTruncate(str1, length); return ( truncated + ' ' + formatPadding('.'.repeat(length - truncated.length)) + ' ' + str2 ); } function maybeTruncate(str, length) { return str.length > length ? `${str.slice(0, length - 3)}...` : str; } function rainbow(str) { const letters = str.split(''); const colors = ['red', 'yellow', 'green', 'cyan', 'blue', 'magenta']; const colorsCount = colors.length; return letters .map((l, i) => { const color = colors[i % colorsCount]; return chalk[color](l); }) .join(''); } ================================================ FILE: packages/artillery/lib/utils-config.js ================================================ const fs = require('node:fs'); const os = require('node:os'); const configFilePath = `${os.homedir()}/.artilleryrc`; function readArtilleryConfig() { try { const config = fs.readFileSync(configFilePath, 'utf-8'); return JSON.parse(config); } catch (_err) { return {}; } } function updateArtilleryConfig(data) { try { const updatedConf = { ...readArtilleryConfig(), ...data }; fs.writeFileSync(configFilePath, JSON.stringify(updatedConf)); return updatedConf; } catch (err) { console.error(err); } } module.exports = { readArtilleryConfig, updateArtilleryConfig }; ================================================ FILE: packages/artillery/man/artillery.1 ================================================ .\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . .TH "ARTILLERY" "8" "December 2018" "" "" . .SH "NAME" \fBartillery\fR \- backend & API testing toolkit . .SH "DESCRIPTION" Artillery is a toolkit for load testing and functional testing of backend services & APIs\. It supports HTTP, WebSocket, and Socket\.io out of the box, and can be extended with plugins\. . .SH "SYNOPSIS" The artillery CLI has several commands\. Run artillery \-\-help to see all of the available commands: . .IP "" 4 . .nf artillery \-\-help Usage: artillery [options] [command] Commands: run [options]