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

[load] baseline trends | sas download #6

Merged
merged 9 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/load/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Release History
* Add CLI parameter `--report` to 'az load test-run download-files' to download the dashboard reports.
* Enable debug level logging using `--debug-mode` argument in 'az load test-run create' command .
* Return the SAS URL to copy artifacts to storage accounts using command 'az load test-run get-artifacts-url'.
* Add config for high-scale load tests and appropriate messaging in the 'az load test-run download-files' command for such tests.
* Add config for high-scale load tests and extend 'az load test-run download-files' to support download of logs and results from artifacts container for such tests.
* Add command 'az load test convert-to-jmx' to convert URL type tests to JMX tests.
* Add commands 'az load test set-baseline' to set the baseline for a test and 'az load test compare-to-baseline' to compare recent test runs to the baseline test run.


1.3.1
Expand Down
3 changes: 3 additions & 0 deletions src/load/azext_load/data_plane/load_test/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from azext_load.data_plane.utils import validators
from azext_load.data_plane.utils.constants import LoadCommandsConstants
from azure.cli.core.commands import CliCommandType
from .custom import trends_output_transformer

admin_custom_sdk = CliCommandType(
operations_tmpl="azext_load.data_plane.load_test.custom#{}"
Expand All @@ -31,6 +32,8 @@ def load_test_commands(self, _):
"convert_to_jmx",
confirmation=LoadCommandsConstants.CONVERT_TO_JMX_CONFIRM_PROMPT
)
g.custom_command("set-baseline", "set_baseline")
g.custom_command("compare-to-baseline", "compare_to_baseline", table_transformer=trends_output_transformer)

with self.command_group(
"load test app-component",
Expand Down
94 changes: 93 additions & 1 deletion src/load/azext_load/data_plane/load_test/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,27 @@

import os

from azext_load.data_plane.utils.constants import LoadTestTrendsKeys
from azext_load.data_plane.utils.utils import (
convert_yaml_to_test,
create_autostop_criteria_from_args,
create_or_update_test_with_config,
create_or_update_test_without_config,
download_file,
generate_trends_row,
get_admin_data_plane_client,
get_testrun_data_plane_client,
load_yaml,
upload_file_to_test,
upload_files_helper,
create_autostop_criteria_from_args,
)
from azext_load.data_plane.utils.models import (
AllowedTestTypes,
AllowedTrendsResponseTimeAggregations,
)
from azure.cli.core.azclierror import InvalidArgumentValueError, FileOperationError
from azure.core.exceptions import ResourceNotFoundError
from collections import OrderedDict
from knack.log import get_logger

logger = get_logger(__name__)
Expand Down Expand Up @@ -302,6 +307,93 @@ def convert_to_jmx(
return response.as_dict()


def set_baseline(
cmd,
load_test_resource,
test_id,
test_run_id,
resource_group_name=None,
):
logger.info("Setting baseline for test with test ID: %s", test_id)
test_client = get_admin_data_plane_client(cmd, load_test_resource, resource_group_name)
test_run_client = get_testrun_data_plane_client(cmd, load_test_resource, resource_group_name)
existing_test = test_client.get_test(test_id)
existing_test_run = test_run_client.get_test_run(test_run_id)
if existing_test_run.get("testId") != test_id:
raise InvalidArgumentValueError(
f"Test run with ID: {test_run_id} is not associated with test ID: {test_id}"
)
if existing_test_run.get("status") not in ["CANCELLED", "DONE"]:
raise InvalidArgumentValueError(
f"Test run with ID: {test_run_id} does not have a valid "
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
f"test run status {existing_test_run.get('status')}. "
"Valid test run status are: CANCELLED, DONE"
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
)
if existing_test_run.get("testRunStatistics", {}).get("Total") is None:
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
raise InvalidArgumentValueError(
f"Sampler statistics are not yet available for test run ID {test_run_id}. "
"Please try again later."
)
body = create_or_update_test_without_config(
test_id=test_id,
body=existing_test,
baseline_test_run_id=test_run_id,
)
response = test_client.create_or_update_test(test_id=test_id, body=body)
logger.debug("Set test run %s as baseline for test: %s", test_run_id, test_id)
return response.as_dict()


def compare_to_baseline(
cmd,
load_test_resource,
test_id,
resource_group_name=None,
response_time_aggregate=AllowedTrendsResponseTimeAggregations.MEAN.value,
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
):
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Showing test trends for test with test ID: %s", test_id)
test_client = get_admin_data_plane_client(cmd, load_test_resource, resource_group_name)
test_run_client = get_testrun_data_plane_client(cmd, load_test_resource, resource_group_name)
test = test_client.get_test(test_id)
if test.get("baselineTestRunId") is None:
raise InvalidArgumentValueError(
f"Test with ID: {test_id} does not have a baseline test run associated with it."
)
baseline_test_run_id = test.get("baselineTestRunId")
baseline_test_run = test_run_client.get_test_run(baseline_test_run_id)
all_test_runs = test_run_client.list_test_runs(test_id=test_id)
all_test_runs = [run.as_dict() for run in all_test_runs]
logger.debug("Total number of test runs: %s", len(all_test_runs))
count = 0
recent_test_runs = []
for run in all_test_runs:
if (
run.get("testRunId") != baseline_test_run_id
and count < 10 # Show only 10 most recent test runs
and run.get("status") in ["CANCELLED", "DONE"]
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
):
recent_test_runs.append(run)
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
count += 1

logger.debug("Number of recent test runs: %s", len(recent_test_runs))
rows = [
generate_trends_row(baseline_test_run, response_time_aggregate=response_time_aggregate)
]
for run in recent_test_runs:
rows.append(
generate_trends_row(run, response_time_aggregate=response_time_aggregate)
)
logger.debug("Retrieved test trends: %s", rows)
return rows


def trends_output_transformer(result):
table = []
for row in result:
table.append(OrderedDict([(k, row.get(k)) for k in LoadTestTrendsKeys.ORDERED_HEADERS]))
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
return table


def add_test_app_component(
cmd,
load_test_resource,
Expand Down
25 changes: 25 additions & 0 deletions src/load/azext_load/data_plane/load_test/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,31 @@
az load test convert-to-jmx --load-test-resource sample-alt-resource --resource-group sample-rg --test-id sample-existing-test-id
"""

helps[
"load test set-baseline"
] = """
type: command
short-summary: Set a test run as the baseline for comparison with other runs in the test.
examples:
- name: Set baseline test run.
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
text: |
az load test set-baseline --load-test-resource sample-alt-resource --resource-group sample-rg --test-id sample-existing-test-id --test-run-id sample-associated-test-run-id
"""

helps[
"load test compare-to-baseline"
] = """
type: command
short-summary: Compare the sampler statistics from recent test runs with those of the baseline test run.
examples:
- name: Compare recent test runs to baseline.
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
text: |
az load test compare-to-baseline --load-test-resource sample-alt-resource --resource-group sample-rg --test-id sample-existing-test-id -o table
- name: Compare recent test runs to baseline with specific aggregation.
text: |
az load test compare-to-baseline --load-test-resource sample-alt-resource --resource-group sample-rg --test-id sample-existing-test-id --aggregation P95 -o table
"""

helps[
"load test download-files"
] = """
Expand Down
6 changes: 6 additions & 0 deletions src/load/azext_load/data_plane/load_test/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def load_arguments(self, _):
c.argument("autostop_error_rate_time_window", argtypes.autostop_error_rate_time_window)
c.argument("regionwise_engines", argtypes.regionwise_engines)

with self.argument_context("load test set-baseline") as c:
c.argument("test_run_id", argtypes.test_run_id)

with self.argument_context("load test compare-to-baseline") as c:
c.argument("response_time_aggregate", argtypes.response_time_aggregate)

with self.argument_context("load test download-files") as c:
c.argument("path", argtypes.dir_path)
c.argument("force", argtypes.force)
Expand Down
49 changes: 40 additions & 9 deletions src/load/azext_load/data_plane/load_test_run/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

from azext_load.data_plane.utils.utils import (
create_or_update_test_run_body,
download_from_storage_container,
get_file_info_and_download,
get_testrun_data_plane_client,
)
from azext_load.data_plane.utils.constants import HighScaleThreshold
from azure.cli.core.azclierror import InvalidArgumentValueError
from azure.core.exceptions import ResourceNotFoundError
from urllib.parse import urlparse

from knack.log import get_logger

logger = get_logger(__name__)
Expand Down Expand Up @@ -226,6 +229,36 @@ def _is_high_scale_test_run(test_run_data):
return False


def _download_from_artifacts_container(artifacts_container, path, logs=False, results=False):
logger.warning(
"Downloading %s from artifacts container for high scale test run",
{"logs" if logs else "results" if results else "files"}
)
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
if artifacts_container is not None and artifacts_container.get("url") is not None:
artifacts_container_url = artifacts_container.get("url")
artifacts_container_url = _update_artifacts_container_path(artifacts_container_url, logs, results)
download_from_storage_container(artifacts_container_url, path)
logger.warning(
"%s from artifacts container downloaded to %s",
{"Logs" if logs else "Results" if results else "Files"},
path
)
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
else:
logger.warning("No artifacts container found")


def _update_artifacts_container_path(artifacts_container_url, logs, results):
artifacts_container_path = urlparse(artifacts_container_url).path
artifacts_container_path_updated = (
artifacts_container_path
+ f"{'' if artifacts_container_path.endswith('/') else '/'}"
+ f"{'logs' if logs else 'results' if results else ''}"
)
return artifacts_container_url.replace(
artifacts_container_path, artifacts_container_path_updated,
)


def download_test_run_files(
cmd,
load_test_resource,
Expand All @@ -250,28 +283,26 @@ def download_test_run_files(
_download_input_file(test_run_input_artifacts, test_run_id, path)

is_high_scale_test_run = _is_high_scale_test_run(test_run_data)
high_scale_test_run_message = ""
artifacts_container = (
test_run_output_artifacts.get("artifactsContainerInfo")
if test_run_output_artifacts
else None
)
if test_run_log:
if is_high_scale_test_run:
high_scale_test_run_message += f"Logs file for high-scale test {test_run_id} "\
"is not available for download. "
_download_from_artifacts_container(artifacts_container, path, logs=True)
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
else:
_download_logs_file(test_run_output_artifacts, test_run_id, path)

if test_run_results:
if is_high_scale_test_run:
high_scale_test_run_message += f"Results file for high-scale test {test_run_id} "\
"is not available for download. "
_download_from_artifacts_container(artifacts_container, path, results=True)
else:
_download_results_file(test_run_output_artifacts, test_run_id, path)

if test_run_report:
_download_reports_file(test_run_output_artifacts, test_run_id, path)

if high_scale_test_run_message:
high_scale_test_run_message += "Use the 'get-artifacts-url' command to fetch the SAS URL and access the file."
return high_scale_test_run_message


# app components
def add_test_run_app_component(
Expand Down
7 changes: 7 additions & 0 deletions src/load/azext_load/data_plane/utils/argtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,10 @@
nargs="+",
help="Specify the engine count for each region in the format: region1=engineCount1 region2=engineCount2 .... Use region names in the format accepted by Azure Resource Manager (ARM). Ensure the regions are supported by Azure Load Testing. Multi-region load tests can only target public endpoints.",
)

response_time_aggregate = CLIArgumentType(
options_list=["--aggregation"],
type=str,
choices=utils.get_enum_values(models.AllowedTrendsResponseTimeAggregations),
mbhardwaj-msft marked this conversation as resolved.
Show resolved Hide resolved
help="Specify the aggregation method for statistics.",
)
29 changes: 29 additions & 0 deletions src/load/azext_load/data_plane/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,32 @@ class HighScaleThreshold:
class LoadCommandsConstants:
CONVERT_TO_JMX_CONFIRM_PROMPT = "Once the test is converted, the process cannot be reversed.\n" \
"Do you want to continue?"


@dataclass
class LoadTestTrendsKeys:
NAME = "Name"
DURATION = "Duration (in minutes)"
VUSERS = "Virtual Users"
TOTAL_REQUESTS = "Total Requests"
MEAN_RES_TIME = "Mean Response Time"
MEDIAN_RES_TIME = "Median Response Time"
MAX_RES_TIME = "Max Response Time"
MIN_RES_TIME = "Min Response Time"
P75_RES_TIME = "75th Percentile Response Time"
P90_RES_TIME = "90th Percentile Response Time"
P95_RES_TIME = "95th Percentile Response Time"
P96_RES_TIME = "96th Percentile Response Time"
P98_RES_TIME = "98th Percentile Response Time"
P99_RES_TIME = "99th Percentile Response Time"
P999_RES_TIME = "99.9th Percentile Response Time"
P9999_RES_TIME = "99.99th Percentile Response Time"
ERROR_PCT = "Error Percentage"
THROUGHPUT = "Throughput"
STATUS = "Status"

ORDERED_HEADERS = [NAME, DURATION, VUSERS, TOTAL_REQUESTS,
MEAN_RES_TIME, MEDIAN_RES_TIME, MAX_RES_TIME, MIN_RES_TIME,
P75_RES_TIME, P90_RES_TIME, P95_RES_TIME, P96_RES_TIME,
P98_RES_TIME, P99_RES_TIME, P999_RES_TIME,
P9999_RES_TIME, ERROR_PCT, THROUGHPUT, STATUS]
15 changes: 15 additions & 0 deletions src/load/azext_load/data_plane/utils/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,18 @@ class AllowedTestTypes(str, Enum):
class AllowedTestPlanFileExtensions(str, Enum):
JMX = ".jmx"
URL = ".json"


class AllowedTrendsResponseTimeAggregations(str, Enum):
MEAN = "MEAN"
MEDIAN = "MEDIAN"
MAX = "MAX"
MIN = "MIN"
P75 = "P75"
P90 = "P90"
P95 = "P95"
P96 = "P96"
P98 = "P98"
P99 = "P99"
P999 = "P999"
P9999 = "P9999"
Loading