diff --git a/locust/argument_parser.py b/locust/argument_parser.py index b7c3357c9d..e05129d5eb 100644 --- a/locust/argument_parser.py +++ b/locust/argument_parser.py @@ -488,6 +488,12 @@ def setup_parser_arguments(parser): help="Store HTML report to file path specified", env_var="LOCUST_HTML", ) + stats_group.add_argument( + "--json", + default=False, + action="store_true", + help="Prints the final stats in JSON format to stdout. Useful for parsing the results in other programs/scripts. Use together with --headless and --skip-log for an output only with the json data.", + ) log_group = parser.add_argument_group("Logging options") log_group.add_argument( diff --git a/locust/main.py b/locust/main.py index 02f36b3618..9b8396ebf8 100644 --- a/locust/main.py +++ b/locust/main.py @@ -13,7 +13,14 @@ from .env import Environment from .log import setup_logging, greenlet_exception_logger from . import stats -from .stats import print_error_report, print_percentile_stats, print_stats, stats_printer, stats_history +from .stats import ( + print_error_report, + print_percentile_stats, + print_stats, + print_stats_json, + stats_printer, + stats_history, +) from .stats import StatsCSV, StatsCSVFileWriter from .user.inspectuser import print_task_ratio, print_task_ratio_json from .util.timespan import parse_timespan @@ -428,8 +435,9 @@ def shutdown(): logger.debug("Cleaning up runner...") if runner is not None: runner.quit() - - if not isinstance(runner, locust.runners.WorkerRunner): + if options.json: + print_stats_json(runner.stats) + elif not isinstance(runner, locust.runners.WorkerRunner): print_stats(runner.stats, current=False) print_percentile_stats(runner.stats) print_error_report(runner.stats) diff --git a/locust/stats.py b/locust/stats.py index b52d9a33bc..490e132522 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -2,6 +2,7 @@ from abc import abstractmethod import datetime import hashlib +import json from tempfile import NamedTemporaryFile import time from collections import namedtuple, OrderedDict @@ -787,6 +788,10 @@ def print_stats(stats: RequestStats, current=True) -> None: console_logger.info("") +def print_stats_json(stats: RequestStats) -> None: + print(json.dumps(stats.serialize_stats(), indent=4)) + + def get_stats_summary(stats: RequestStats, current=True) -> List[str]: """ stats summary will be returned as list of string diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 0b4bcca820..1ef19bfe6d 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -1,3 +1,4 @@ +import json import os import platform import pty @@ -6,7 +7,7 @@ import textwrap from tempfile import TemporaryDirectory from unittest import TestCase -from subprocess import PIPE, STDOUT +from subprocess import PIPE, STDOUT, DEVNULL import gevent import requests @@ -1400,6 +1401,89 @@ def t(self): self.assertEqual(0, proc.returncode) self.assertEqual(0, proc_worker.returncode) + def test_json_can_be_parsed(self): + LOCUSTFILE_CONTENT = textwrap.dedent( + """ + from locust import User, task, constant + + class User1(User): + wait_time = constant(1) + + @task + def t(self): + pass + """ + ) + with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked: + proc = subprocess.Popen( + ["locust", "-f", mocked.file_path, "--headless", "-t", "5s", "--json"], + stderr=DEVNULL, + stdout=PIPE, + text=True, + ) + stdout, stderr = proc.communicate() + + try: + json.loads(stdout) + except json.JSONDecodeError: + self.fail(f"Trying to parse {stdout} as json failed") + self.assertEqual(0, proc.returncode) + + def test_json_schema(self): + LOCUSTFILE_CONTENT = textwrap.dedent( + """ + from locust import HttpUser, task, constant + + class QuickstartUser(HttpUser): + wait_time = constant(1) + + @task + def hello_world(self): + self.client.get("/") + + """ + ) + with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked: + proc = subprocess.Popen( + [ + "locust", + "-f", + mocked.file_path, + "--host", + "http://google.com", + "--headless", + "-u", + "1", + "-t", + "2s", + "--json", + ], + stderr=DEVNULL, + stdout=PIPE, + text=True, + ) + stdout, stderr = proc.communicate() + + try: + data = json.loads(stdout) + except json.JSONDecodeError: + self.fail(f"Trying to parse {stdout} as json failed") + + self.assertEqual(0, proc.returncode) + + result = data[0] + self.assertEqual(float, type(result["last_request_timestamp"])) + self.assertEqual(float, type(result["start_time"])) + self.assertEqual(int, type(result["num_requests"])) + self.assertEqual(int, type(result["num_none_requests"])) + self.assertEqual(float, type(result["total_response_time"])) + self.assertEqual(float, type(result["max_response_time"])) + self.assertEqual(float, type(result["min_response_time"])) + self.assertEqual(int, type(result["total_content_length"])) + self.assertEqual(dict, type(result["response_times"])) + self.assertEqual(dict, type(result["num_reqs_per_sec"])) + self.assertEqual(dict, type(result["num_fail_per_sec"])) + def test_worker_indexes(self): content = """ from locust import HttpUser, task, between