示例#1
0
 def on_exit(**kwargs):
     if ssh_identity_file is not None:
         os.remove(ssh_identity_file)
     locustfile_source.cleanup()
     print_formatted_stats_on_primary_node(runners.locust_runner.stats)
     report_to_cloudwatch_metrics(runners.locust_runner.stats)
     console_logger.info('exiting')
示例#2
0
    def print_formatted_stats_on_primary_node(stats):
        if options.master_host is not None:  # Slave mode. Do not print stats on slaves
            return
        for key in sorted(six.iterkeys(stats.entries)):
            item = stats.entries[key]
            console_logger.info(json.dumps({**{
                "locust_stat_type": "standard",
                "rps": item.total_rps
            }, **item.serialize()}))

        percentile_stats = [{**{
            "locust_stat_type": "percentile",
            "name": stats.entries[key].name,
            "method": stats.entries[key].method,
            "num_request": stats.entries[key].num_requests
        }, **get_percentiles(stats.entries[key])} for key in sorted(six.iterkeys(stats.entries))]
        for item in percentile_stats:
            console_logger.info(json.dumps(item))
示例#3
0
    def list_top_10_currencies_by_volume_24h(self):
        try:
            resp_json = _resp.json()
            # console_logger.info(resp_json)

            global _resp_number

            # Receive 8 responses
            if _resp_number < 8:
                _resp_number += 1
                console_logger.info("Response #{}".format(_resp_number))
                console_logger.info(
                    "Top 10 currencies in past 24 hours by volume:")

                # Display top 10 currencies
                for number_of_currency in range(10):
                    console_logger.info(
                        resp_json['data'][number_of_currency]['name'])
                    # console_logger.info(round(resp_json['data'][number_of_currency]['quote']['USD']['volume_24h'], 1))

            else:
                runners.locust_runner.quit()

        except JSONDecodeError:
            console_logger.error("ERROR: Failed to decode response into JSON")
示例#4
0
def on_slave_report(client_id, data):
    if 'instanceId' in data:
        instanceId = data['instanceId'].strip()
        #console_logger.info( 'instanceId %s' % (instanceId) )
        if client_id not in locust.runners.locust_runner.clients:
            if client_id not in reportedZombies:
                reportedZombies.add(client_id)
                console_logger.info('zombie instance %s' % (instanceId))
    else:
        instanceId = None

    ipAddr = None
    if 'ipAddr' in data:
        ipAddr = data['ipAddr'].strip()

    if client_id not in locust.runners.locust_runner.clients:
        return  # NO FURTHER PROCESSING of reports from zombies

    if g_stopping:
        if len(data['stats']):
            console_logger.info(
                'on_slave_report DROPPING because g_stopping len(stats): %d',
                len(data['stats']))
        return
    if len(data['stats']):
        #if len(data['stats']) != 1:
        #    console_logger.info( '%d stats objects' % len(data['stats']) )
        nUsers = data['user_count']
        stats = data['stats_total']
        rpss = data['stats_total']['num_reqs_per_sec']
        #rpss = data['stats'][0]['num_reqs_per_sec']
        if len(rpss) > 0:
            rps = sorted(rpss.values())[int(len(rpss) /
                                            2)]  # approximate median value
            maxRps = max(rpss.values())
            minRps = min(rpss.values())
        else:
            rps = maxRps = minRps = 0
        numReqs = data['stats_total']['num_requests']
        rtimes = data['stats_total'][
            'response_times']  # these are quantized when large
        if numReqs:
            meanRTime = data['stats_total']['total_response_time'] / numReqs
        else:
            meanRTime = 0
        #meanRTime = sum(rtimes.keys()) / len(rtimes.keys())  # old way, imprecise
        maxRTime = data['stats_total'][
            'max_response_time']  # was max( rtimes.keys() )
        minRTime = data['stats_total']['min_response_time']
        if minRTime == None:
            minRTime = 0
        #minRTime = min( rtimes.keys() )  # old way, imprecise

        medRTime = median_from_dict(numReqs, rtimes)
        if medRTime == None:
            medRTime = 0

        # get begin and end time stamps (they are request times, not response-received times)
        startTimeStamp = data['stats_total']['start_time']
        # endTimeStamp = data['stats_total']['last_request_timestamp']  # not useful, it is truncated
        startDateTime = datetime.datetime.fromtimestamp(
            startTimeStamp
        )  # could be bad if closks out of sync (or wrong timezone)
        masterDateTime = datetime.datetime.now()
        timeDiscrep = (masterDateTime - startDateTime).total_seconds() - 3
        if abs(timeDiscrep) > 6.0:
            console_logger.info('timeDiscrep: %.3f for instance %s',
                                timeDiscrep, instanceId)
            if abs(timeDiscrep) > 15:
                console_logger.info('DROPPING due to time discrepancy %s',
                                    instanceId)
                return  # DROPPING data

        endDateTime = datetime.datetime.now()

        if instanceId and ipAddr:
            workerName = '%s_%s' % (ipAddr, instanceId)
        elif instanceId:
            workerName = '_%s' % (instanceId)
        else:
            workerName = client_id.rsplit('_', 1)[0]
            if workerName == 'localhost':
                if instanceId:
                    workerName = instanceId
                else:
                    workerName = client_id

        if g_jlogFile:
            json.dump([workerName, data], g_jlogFile)
            print("", file=g_jlogFile)
            g_jlogFile.flush()

        numFails = stats['num_failures']

        #console_logger.info( '%s worker: %s; reqs: %d RPS: %.1f (%.1f-%.1f); response time: %.1f (%.1f-%.1f)' % \
        #    (datetime.datetime.now().isoformat(), workerName, numReqs, rps, minRps, maxRps, meanRTime, minRTime, maxRTime) )
        #console_logger.info( 'rTimes: %s' % (data['stats_total']['response_times'].keys()) )
        #console_logger.info( 'stats_total: %s' % (data['stats_total']) )
        if data['stats_total']['max_response_time'] != maxRTime:
            console_logger.info('stats_total: %s' % (data['stats_total']))
        if numFails > 0:
            console_logger.info('num_failures: %d' % (numFails))

        if False:
            console_logger.info('disabled')
        else:
            try:
                outString = '%s,%s,%s,%d,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%d,%d' % \
                    (startDateTime.isoformat(), endDateTime.isoformat(),
                        workerName, numReqs, rps, maxRps, minRps,
                        meanRTime, maxRTime, medRTime, minRTime, numFails, nUsers
                        )
                if g_statsOutFile:
                    print(outString, file=g_statsOutFile)
                    g_statsOutFile.flush()
                else:
                    console_logger.info('NO g_statsOutFile (master)')
            except:
                print('outString error',
                      rps,
                      maxRps,
                      minRps,
                      meanRTime,
                      maxRTime,
                      medRTime,
                      minRTime,
                      file=sys.stderr)
示例#5
0
def on_master_stop_hatching():
    '''event hook function to be called by Locust '''
    global g_stopping
    console_logger.info('on_master_stop_hatching called')
    g_stopping = True
示例#6
0
def on_hatch_complete(user_count=0):
    '''event hook function to be called by Locust '''
    console_logger.info('on_hatch_complete called with count %d', user_count)
示例#7
0
def main():
    parser, options = parse_options()

    # setup logging
    if not options.skip_log_setup:
        setup_logging(options.loglevel, options.logfile)

    logger = logging.getLogger(__name__)

    locust_path = get_locust_path()

    if options.web:
        web_port = options.web_port
        logger.info('Running easy-locust web: 0.0.0.0:{}'.format(web_port))
        init_app(port=web_port)
        sys.exit(0)

    if options.demo:
        if not locust_path:
            logger.error(
                '''Cannot locate Python path, make sure it is in right place. If windows add it to sys PATH,
            if linux make sure python is installed in /usr/local/lib/''')
            sys.exit(1)
        pt_demo_path = os.path.join(locust_path, 'demo',
                                    'demo_pressuretest.xls')
        pt_demo_path_json = os.path.join(locust_path, 'demo',
                                         'demo_locustfile.json')
        pt_new_demo = os.path.join(os.getcwd(), 'PtDemo.xls')
        pt_new_demo_json = os.path.join(os.getcwd(), 'demo.json')
        shutil.copyfile(pt_demo_path, pt_new_demo)
        shutil.copyfile(pt_demo_path_json, pt_new_demo_json)
        sys.exit(0)

    if options.xlsfile:
        pt_file = options.xlsfile
        if not (pt_file.endswith('.xls') or pt_file.endswith('.json')):
            logger.error(
                "PressureTest file must be end with '.xls' or '.json' and see --help for available options."
            )
            sys.exit(1)
        if not os.path.isfile(pt_file):
            logger.error('PressureTest file is not exist, please check it.')
            sys.exit(1)
        _status = generate_locust_file(pt_file)
        if not _status:
            sys.exit(1)
        sys.exit(0)

    locustfile = find_locustfile(options.locustfile)

    if not locustfile:
        logger.error(
            "Could not find any locustfile! Ensure file ends in '.py' and see --help for available options."
        )
        sys.exit(1)

    if locustfile == "locust.py" or locustfile == "locust.xls" or locustfile == "locust.json":
        logger.error(
            "The locustfile must not be named `locust.py` or `locust.xls` or `locust.json`. "
            "Please rename the file and try again.")
        sys.exit(1)

    docstring, locusts = load_locustfile(locustfile)

    if options.list_commands:
        console_logger.info("Available Locusts:")
        for name in locusts:
            console_logger.info("    " + name)
        sys.exit(0)

    if not locusts:
        logger.error("No Locust class found!")
        sys.exit(1)

    # make sure specified Locust exists
    if options.locust_classes:
        missing = set(options.locust_classes) - set(locusts.keys())
        if missing:
            logger.error("Unknown Locust(s): %s\n" % (", ".join(missing)))
            sys.exit(1)
        else:
            names = set(options.locust_classes) & set(locusts.keys())
            locust_classes = [locusts[n] for n in names]
    else:
        # list() call is needed to consume the dict_view object in Python 3
        locust_classes = list(locusts.values())

    if options.show_task_ratio:
        console_logger.info("\n Task ratio per locust class")
        console_logger.info("-" * 80)
        print_task_ratio(locust_classes)
        console_logger.info("\n Total task ratio")
        console_logger.info("-" * 80)
        print_task_ratio(locust_classes, total=True)
        sys.exit(0)
    if options.show_task_ratio_json:
        from json import dumps
        task_data = {
            "per_class": get_task_ratio_dict(locust_classes),
            "total": get_task_ratio_dict(locust_classes, total=True)
        }
        console_logger.info(dumps(task_data))
        sys.exit(0)

    if options.run_time:
        if not options.no_web:
            logger.error(
                "The --run-time argument can only be used together with --no-web"
            )
            sys.exit(1)
        try:
            options.run_time = parse_timespan(options.run_time)
        except ValueError:
            logger.error(
                "Valid --run-time formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc."
            )
            sys.exit(1)

        def spawn_run_time_limit_greenlet():
            logger.info("Run time limit set to %s seconds" % options.run_time)

            def timelimit_stop():
                logger.info("Time limit reached. Stopping Locust.")
                runners.locust_runner.quit()

            gevent.spawn_later(options.run_time, timelimit_stop)

    if options.step_time:
        if not options.step_load:
            logger.error(
                "The --step-time argument can only be used together with --step-load"
            )
            sys.exit(1)
        try:
            options.step_time = parse_timespan(options.step_time)
        except ValueError:
            logger.error(
                "Valid --step-time formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc."
            )
            sys.exit(1)

    if options.master:
        # Add -d for automatically run slaves
        if options.distribute:
            ptpy = locustfile
            if options.locustfile.endswith('.xls'):
                _type = 'xls'
                pt_s = PtExcel(options.locustfile)
                master_ip, pt_slave_info = pt_s.pt_slave()
            else:
                _type = 'dict'
                with open(options.locustfile, 'r') as f:
                    _d = json.load(f, encoding='utf-8')
                master_ip = _d.get('master_ip')
                pt_slave_info = _d.get('slaves')
            if master_ip == '':
                logger.error(
                    'master IP cannot be None if you use --distribute')
                sys.exit(1)
            if options.boomer:
                locust_cli_slave = 'nohup ./client_v1 --web --master-host={masteIP} > /dev/null 2>&1 &'.format(
                    masteIP=master_ip)
                targets_dict, file_list = gen_boomer_client_json(
                    options.locustfile)
                boomer_client_file = os.path.join(locust_path, 'boomer_client',
                                                  'client_v1')
                file_list.append(boomer_client_file)
                thread_pool = []
                try:
                    for slave in pt_slave_info:
                        if _type == 'xls':
                            slave_ip, slave_username, slave_password = slave
                        else:
                            slave_ip, slave_username, slave_password = slave[
                                'ip'], slave['username'], slave['password']
                        _t = Thread(target=pt_slave_boomer,
                                    args=(slave_ip, slave_username,
                                          slave_password, file_list,
                                          locust_cli_slave, targets_dict))
                        logger.info('Prepare slave {}'.format(slave_ip))
                        thread_pool.append(_t)
                        _t.start()
                    for each_t in thread_pool:
                        each_t.join()
                    file_list.pop()
                    for each in file_list:
                        os.remove(each)
                except KeyboardInterrupt:
                    pass
                except Exception as e:
                    logger.error(
                        'Something happened, collect Exceptions here: {}'.
                        format(e))
            else:
                try:
                    locust_cli_slave = 'nohup locust -f /root/locust_client.py --slave --master-host={masteIP} > /dev/null 2>&1 &'\
                        .format(masteIP=master_ip)
                    thread_pool = []
                    for slave in pt_slave_info:
                        if _type == 'xls':
                            slave_ip, slave_username, slave_password = slave
                        else:
                            slave_ip, slave_username, slave_password = slave[
                                'ip'], slave['username'], slave['password']
                        _t = Thread(target=pt_slave,
                                    args=(slave_ip, slave_username,
                                          slave_password, ptpy,
                                          locust_cli_slave))
                        logger.info('Prepare slave {}'.format(slave_ip))
                        thread_pool.append(_t)
                        _t.start()
                    for each_t in thread_pool:
                        each_t.join()
                except KeyboardInterrupt:
                    pass
                except Exception as e:
                    logger.error(
                        'Something happened, collect Exceptions here: {}'.
                        format(e))

        runners.locust_runner = MasterLocustRunner(locust_classes, options)
    else:
        runners.locust_runner = LocalLocustRunner(locust_classes, options)
    # main_greenlet is pointing to runners.locust_runner.greenlet by default, it will point the web greenlet later if in web mode
    main_greenlet = runners.locust_runner.greenlet

    if options.no_web:
        if options.master:
            while len(runners.locust_runner.clients.ready
                      ) < options.expect_slaves:
                logging.info(
                    "Waiting for slaves to be ready, %s of %s connected",
                    len(runners.locust_runner.clients.ready),
                    options.expect_slaves)
                time.sleep(1)
        if options.step_time:
            runners.locust_runner.start_stepload(options.num_clients,
                                                 options.hatch_rate,
                                                 options.step_clients,
                                                 options.step_time)
        else:
            runners.locust_runner.start_hatching(options.num_clients,
                                                 options.hatch_rate)
            # make locusts are spawned
            time.sleep(1)
    else:
        # spawn web greenlet
        logger.info("Starting web monitor at http://%s:%s" %
                    (options.web_host or "*", options.port))
        main_greenlet = gevent.spawn(web.start, locust_classes, options)

    if options.run_time:
        spawn_run_time_limit_greenlet()

    stats_printer_greenlet = None
    if not options.only_summary and (options.print_stats or options.no_web):
        # spawn stats printing greenlet
        stats_printer_greenlet = gevent.spawn(stats_printer)

    if options.csvfilebase:
        gevent.spawn(stats_writer, options.csvfilebase,
                     options.stats_history_enabled)

    def shutdown(code=0):
        """
        Shut down locust by firing quitting event, printing/writing stats and exiting
        """
        logger.info("Shutting down (exit code %s), bye." % code)
        if stats_printer_greenlet is not None:
            stats_printer_greenlet.kill(block=False)
        logger.info("Cleaning up runner...")
        if runners.locust_runner is not None:
            runners.locust_runner.quit()
        logger.info("Running teardowns...")
        events.quitting.fire(reverse=True)
        print_stats(runners.locust_runner.stats, current=False)
        print_percentile_stats(runners.locust_runner.stats)
        if options.csvfilebase:
            write_stat_csvs(options.csvfilebase, options.stats_history_enabled)
        print_error_report()
        sys.exit(code)

    # install SIGTERM handler
    def sig_term_handler():
        logger.info("Got SIGTERM signal")
        shutdown(0)

    gevent.signal(signal.SIGTERM, sig_term_handler)

    try:
        logger.info("Starting Locust %s" % version)
        main_greenlet.join()
        code = 0
        if len(runners.locust_runner.errors) or len(
                runners.locust_runner.exceptions):
            code = options.exit_code_on_error
        shutdown(code=code)
    except KeyboardInterrupt:
        shutdown(0)
示例#8
0
def main(user_params=None, common_param=None):
    parser, options, arguments = parse_options()

    # setup logging
    setup_logging(options.loglevel, options.logfile)
    logger = logging.getLogger(__name__)

    if options.show_version:
        print("Locust %s" % (version, ))
        sys.exit(0)

    locustfile = find_locustfile(options.locustfile)

    if not locustfile:
        logger.error(
            "Could not find any locustfile! Ensure file ends in '.py' and see --help for available options."
        )
        sys.exit(1)

    if locustfile == "locust.py":
        logger.error(
            "The locustfile must not be named `locust.py`. Please rename the file and try again."
        )
        sys.exit(1)

    docstring, locusts = load_locustfile(locustfile)

    if options.list_commands:
        console_logger.info("Available Locusts:")
        for name in locusts:
            console_logger.info("    " + name)
        sys.exit(0)

    if not locusts:
        logger.error("No Locust class found!")
        sys.exit(1)

    # make sure specified Locust exists
    if arguments:
        missing = set(arguments) - set(locusts.keys())
        if missing:
            logger.error("Unknown Locust(s): %s\n" % (", ".join(missing)))
            sys.exit(1)
        else:
            names = set(arguments) & set(locusts.keys())
            locust_classes = [locusts[n] for n in names]
    else:
        # list() call is needed to consume the dict_view object in Python 3
        locust_classes = list(locusts.values())

    if options.show_task_ratio:
        console_logger.info("\n Task ratio per locust class")
        console_logger.info("-" * 80)
        print_task_ratio(locust_classes)
        console_logger.info("\n Total task ratio")
        console_logger.info("-" * 80)
        print_task_ratio(locust_classes, total=True)
        sys.exit(0)
    if options.show_task_ratio_json:
        from json import dumps
        task_data = {
            "per_class": get_task_ratio_dict(locust_classes),
            "total": get_task_ratio_dict(locust_classes, total=True)
        }
        console_logger.info(dumps(task_data))
        sys.exit(0)

    if not options.no_web and not options.slave:
        # spawn web greenlet
        logger.info("Starting web monitor at %s:%s" %
                    (options.web_host or "*", options.port))
        main_greenlet = gevent.spawn(web.start, locust_classes, options)

    if not options.master and not options.slave:
        if user_params:
            runners.locust_runner = ParameterizableLocustRunner(
                locust_classes, options, user_params, common_param)
        else:
            runners.locust_runner = LocalLocustRunner(locust_classes, options)
        # spawn client spawning/hatching greenlet
        if options.no_web:
            runners.locust_runner.start_hatching(wait=True)
            main_greenlet = runners.locust_runner.greenlet
    elif options.master:
        runners.locust_runner = MasterLocustRunner(locust_classes, options)
        if options.no_web:
            while len(runners.locust_runner.clients.ready
                      ) < options.expect_slaves:
                logging.info(
                    "Waiting for slaves to be ready, %s of %s connected",
                    len(runners.locust_runner.clients.ready),
                    options.expect_slaves)
                time.sleep(1)

            runners.locust_runner.start_hatching(options.num_clients,
                                                 options.hatch_rate)
            main_greenlet = runners.locust_runner.greenlet
    elif options.slave:
        try:
            runners.locust_runner = SlaveLocustRunner(locust_classes, options)
            main_greenlet = runners.locust_runner.greenlet
        except socket.error as e:
            logger.error("Failed to connect to the Locust master: %s", e)
            sys.exit(-1)

    if not options.only_summary and (options.print_stats or
                                     (options.no_web and not options.slave)):
        # spawn stats printing greenlet
        gevent.spawn(stats_printer)

    if options.csvfilebase:
        gevent.spawn(stats_writer, options.csvfilebase)

    def shutdown(code=0):
        """
        Shut down locust by firing quitting event, printing/writing stats and exiting
        """
        logger.info("Shutting down (exit code %s), bye." % code)

        events.quitting.fire()
        print_stats(runners.locust_runner.request_stats)
        print_percentile_stats(runners.locust_runner.request_stats)
        if options.csvfilebase:
            write_stat_csvs(options.csvfilebase)
        print_error_report()
        sys.exit(code)

    # install SIGTERM handler
    def sig_term_handler():
        logger.info("Got SIGTERM signal")
        shutdown(0)

    gevent.signal(gevent.signal.SIGTERM, sig_term_handler)

    try:
        logger.info("Starting Locust %s" % version)
        main_greenlet.join()
        code = 0
        if len(runners.locust_runner.errors):
            code = 1
        shutdown(code=code)
    except KeyboardInterrupt as e:
        shutdown(0)
示例#9
0
def main():
    options = parse_options()

    ssh_pvt_key_ssm_param_name = options.ssh_pvt_key_ssm_param_name

    def get_ssh_identity_file():
        if ssh_pvt_key_ssm_param_name is not None:
            ssm = boto3.client('ssm')
            ssh_pvt_key = ssm.get_parameter(Name=ssh_pvt_key_ssm_param_name, WithDecryption=True)['Parameter']['Value']
            fd, file_name = tempfile.mkstemp()
            with open(file_name, 'w') as file:
                file.write(ssh_pvt_key)
                os.close(fd)
                return file_name
        return None

    ssh_identity_file = get_ssh_identity_file()

    locustfile_selector = LocustFileSelectorPipeline(
        [GitLocustFileSelectorMiddleware(ssh_identity_file=ssh_identity_file)])

    locustfile_source = locustfile_selector.select(options.locustfile_source)

    locusfile = locustfile_source.fetch()

    def get_percentiles(stat_entry):
        return {str(e) + '%': stat_entry.get_response_time_percentile(e) for e in PERCENTILES_TO_REPORT}

    def print_formatted_stats_on_primary_node(stats):
        if options.master_host is not None:  # Slave mode. Do not print stats on slaves
            return
        for key in sorted(six.iterkeys(stats.entries)):
            item = stats.entries[key]
            console_logger.info(json.dumps({**{
                "locust_stat_type": "standard",
                "rps": item.total_rps
            }, **item.serialize()}))

        percentile_stats = [{**{
            "locust_stat_type": "percentile",
            "name": stats.entries[key].name,
            "method": stats.entries[key].method,
            "num_request": stats.entries[key].num_requests
        }, **get_percentiles(stats.entries[key])} for key in sorted(six.iterkeys(stats.entries))]
        for item in percentile_stats:
            console_logger.info(json.dumps(item))

    def create_standard_metric_data(stat_entry):
        return [
            {
                'MetricName': 'Req/s', 'Value': stat_entry.total_rps,
                'Unit': 'Count/Second'
            },
            {
                'MetricName': 'Min Response Time',
                'Value': stat_entry.min_response_time,
                'Unit': 'Milliseconds'
            },
            {
                'MetricName': 'Max Response Time',
                'Value': stat_entry.max_response_time,
                'Unit': 'Milliseconds'
            },
            {
                'MetricName': 'Avg Response Time',
                'Value': stat_entry.avg_response_time,
                'Unit': 'Milliseconds'
            },
            {
                'MetricName': 'Median Response Time',
                'Value': stat_entry.median_response_time,
                'Unit': 'Milliseconds'
            },
            {
                'MetricName': 'Total Requests',
                'Value': stat_entry.num_requests,
                'Unit': 'Count'
            },
            {
                'MetricName': 'Total Failed Requests',
                'Value': stat_entry.num_failures,
                'Unit': 'Count'
            },
        ]

    def create_percentile_metric_data(stat_entry):
        return [{
            'MetricName': str(p * 100) + '% Latency',
            'Value': stat_entry.get_response_time_percentile(p),
            'Unit': 'Percent'
        } for p in PERCENTILES_TO_REPORT]

    def create_metric_data(stat_entry):
        timestamp = datetime.utcnow()

        return list(map(lambda e: {**e, **{
            'Timestamp': timestamp,
            'Dimensions': [
                {
                    'Name': 'Method',
                    'Value': stat_entry.method
                },
                {
                    'Name': 'Name',
                    'Value': stat_entry.name
                },
                {
                    'Name': 'Host',
                    'Value': options.host
                }]}}, create_standard_metric_data(stat_entry) + create_percentile_metric_data(stat_entry)))

    cloudwatch = None

    def ensure_cloudwatch_client_created():
        nonlocal cloudwatch
        if cloudwatch is not None:
            return
        cloudwatch = boto3.client('cloudwatch')

    def report_to_cloudwatch_metrics(stats):
        namespace = options.cloudwatch_metric_ns
        if namespace is None:
            return

        if options.master_host is not None:  # represents that this is the master node
            #  Metrics will not be reported if this is not the master node
            return
        ensure_cloudwatch_client_created()
        for entry in six.itervalues(stats.entries):
            metric_data = create_metric_data(entry)
            cloudwatch.put_metric_data(Namespace=namespace, MetricData=metric_data)

    def on_exit(**kwargs):
        if ssh_identity_file is not None:
            os.remove(ssh_identity_file)
        locustfile_source.cleanup()
        print_formatted_stats_on_primary_node(runners.locust_runner.stats)
        report_to_cloudwatch_metrics(runners.locust_runner.stats)
        console_logger.info('exiting')

    argv = [sys.argv[0], '-f', locusfile, '--no-web', '-c', options.num_clients, '-r', options.hatch_rate,
            '-H', options.host]

    if options.expect_slaves is not None:
        argv += ['--master', '--expect-slaves', str(options.expect_slaves)]

    if options.master_host is not None:
        argv += ['--slave', '--master-host', str(options.master_host)]
        if options.step_time:
            argv += ['--step-time', options.step_time, '--step-clients', str(options.step_clients)]
    else:
        argv += ['--run-time', options.run_time]

    if options.step_load:
        argv += ['--step-load']

    sys.argv = argv

    atexit.register(on_exit)
    console_logger.info('starting')
    locust_main.main()