Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stats in json to stdout (new command line option --json) #2269

Merged
merged 8 commits into from Jan 3, 2023
Merged
6 changes: 6 additions & 0 deletions locust/argument_parser.py
Expand Up @@ -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(
Expand Down
14 changes: 11 additions & 3 deletions locust/main.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions locust/stats.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
86 changes: 85 additions & 1 deletion locust/test/test_main.py
@@ -1,3 +1,4 @@
import json
import os
import platform
import pty
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down