Beispiel #1
0
    def delete(self, surveyName=None):
        # pylint: disable=no-value-for-parameter

        # basic auth protected route if basic auth is enabled
        if current_app.config.get("BASIC_AUTH_ENABLED"):
            print("basic_auth_enabled")
            auth = BasicAuth()
            auth_valid = auth.authenticate()
            print(auth_valid)
            if auth_valid is None or not auth_valid:
                return auth.challenge()

        payload = request.get_json()

        if surveyName is None:
            if payload is None or not bool(payload):
                return error_response(
                    PAYLOAD_MISSING_OR_EMPTY,
                    PAYLOAD_MUST_BE_SENT.format(resource="Surveys")), 400
        elif surveyName is not None:
            payload["surveyName"] = surveyName

        surveyToDelete = payload.get("surveyName")

        if surveyToDelete is None:
            return error_response(
                API_PARAMETER_MISSING,
                API_PARAMETER_MUST_BE_PROVIDED.format(
                    resource="Surveys", parameter="surveyName")), 400

        try:
            SurveyCRUD().delete_survey(surveyName=surveyToDelete)
            return {"surveyName": surveyToDelete}, 200
        except InternalError as e:
            return error_response(INTERNAL_ERROR, str(e)), 500
Beispiel #2
0
class WebUI:
    """
    Sets up and runs a Flask web

     that can start and stop load tests using the
    :attr:`environment.runner <locust.env.Environment.runner>` as well as show the load test statistics
    in :attr:`environment.stats <locust.env.Environment.stats>`
    """

    app = None
    """
    Reference to the :class:`flask.Flask` app. Can be used to add additional web routes and customize
    the Flask app in other various ways. Example::

        from flask import request

        @web_ui.app.route("/my_custom_route")
        def my_custom_route():
            return "your IP is: %s" % request.remote_addr
    """

    greenlet = None
    """
    Greenlet of the running web server
    """

    server = None
    """Reference to the :class:`pyqsgi.WSGIServer` instance"""

    template_args: dict = None
    """Arguments used to render index.html for the web UI. Must be used with custom templates
    extending index.html."""

    def __init__(
        self,
        environment,
        host,
        port,
        auth_credentials=None,
        tls_cert=None,
        tls_key=None,
        stats_csv_writer=None,
        delayed_start=False,
    ):
        """
        Create WebUI instance and start running the web server in a separate greenlet (self.greenlet)

        Arguments:
        environment: Reference to the current Locust Environment
        host: Host/interface that the web server should accept connections to
        port: Port that the web server should listen to
        auth_credentials:  If provided, it will enable basic auth with all the routes protected by default.
                           Should be supplied in the format: "user:pass".
        tls_cert: A path to a TLS certificate
        tls_key: A path to a TLS private key
        delayed_start: Whether or not to delay starting web UI until `start()` is called. Delaying web UI start
                       allows for adding Flask routes or Blueprints before accepting requests, avoiding errors.
        """
        environment.web_ui = self
        self.stats_csv_writer = stats_csv_writer or StatsCSV(environment, stats_module.PERCENTILES_TO_REPORT)
        self.environment = environment
        self.host = host
        self.port = port
        self.tls_cert = tls_cert
        self.tls_key = tls_key
        app = Flask(__name__)
        self.app = app
        app.debug = True
        app.root_path = os.path.dirname(os.path.abspath(__file__))
        self.app.config["BASIC_AUTH_ENABLED"] = False
        self.auth = None
        self.greenlet = None

        if auth_credentials is not None:
            credentials = auth_credentials.split(":")
            if len(credentials) == 2:
                self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
                self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
                self.app.config["BASIC_AUTH_ENABLED"] = True
                self.auth = BasicAuth()
                self.auth.init_app(self.app)
            else:
                raise AuthCredentialsError(
                    "Invalid auth_credentials. It should be a string in the following format: 'user.pass'"
                )
        if environment.runner:
            self.update_template_args()
        if not delayed_start:
            self.start()

        @app.route("/")
        @self.auth_required_if_enabled
        def index():
            if not environment.runner:
                return make_response("Error: Locust Environment does not have any runner", 500)
            self.update_template_args()
            return render_template("index.html", **self.template_args)

        @app.route("/swarm", methods=["POST"])
        @self.auth_required_if_enabled
        def swarm():
            assert request.method == "POST"
            user_count = int(request.form["user_count"])
            spawn_rate = float(request.form["spawn_rate"])
            if request.form.get("host"):
                environment.host = str(request.form["host"])

            if environment.step_load:
                step_user_count = int(request.form["step_user_count"])
                step_duration = parse_timespan(str(request.form["step_duration"]))
                environment.runner.start_stepload(user_count, spawn_rate, step_user_count, step_duration)
                return jsonify(
                    {"success": True, "message": "Swarming started in Step Load Mode", "host": environment.host}
                )

            if environment.shape_class:
                environment.runner.start_shape()
                return jsonify(
                    {"success": True, "message": "Swarming started using shape class", "host": environment.host}
                )

            environment.runner.start(user_count, spawn_rate)
            return jsonify({"success": True, "message": "Swarming started", "host": environment.host})

        @app.route("/stop")
        @self.auth_required_if_enabled
        def stop():
            environment.runner.stop()
            return jsonify({"success": True, "message": "Test stopped"})

        @app.route("/stats/reset")
        @self.auth_required_if_enabled
        def reset_stats():
            environment.events.reset_stats.fire()
            environment.runner.stats.reset_all()
            environment.runner.exceptions = {}
            return "ok"

        @app.route("/stats/report")
        @self.auth_required_if_enabled
        def stats_report():
            stats = self.environment.runner.stats

            start_ts = stats.start_time
            start_time = datetime.datetime.fromtimestamp(start_ts)
            start_time = start_time.strftime("%Y-%m-%d %H:%M:%S")

            end_ts = stats.last_request_timestamp
            end_time = datetime.datetime.fromtimestamp(end_ts)
            end_time = end_time.strftime("%Y-%m-%d %H:%M:%S")

            host = None
            if environment.host:
                host = environment.host
            elif environment.runner.user_classes:
                all_hosts = set([l.host for l in environment.runner.user_classes])
                if len(all_hosts) == 1:
                    host = list(all_hosts)[0]

            requests_statistics = list(chain(sort_stats(stats.entries), [stats.total]))
            failures_statistics = sort_stats(stats.errors)
            exceptions_statistics = []
            for exc in environment.runner.exceptions.values():
                exc["nodes"] = ", ".join(exc["nodes"])
                exceptions_statistics.append(exc)

            history = stats.history

            static_js = ""
            js_files = ["jquery-1.11.3.min.js", "echarts.common.min.js", "vintage.js", "chart.js"]
            for js_file in js_files:
                path = os.path.join(os.path.dirname(__file__), "static", js_file)
                with open(path, encoding="utf8") as f:
                    content = f.read()
                static_js += "// " + js_file + "\n"
                static_js += content
                static_js += "\n\n\n"

            res = render_template(
                "report.html",
                int=int,
                round=round,
                requests_statistics=requests_statistics,
                failures_statistics=failures_statistics,
                exceptions_statistics=exceptions_statistics,
                start_time=start_time,
                end_time=end_time,
                host=host,
                history=history,
                static_js=static_js,
            )
            if request.args.get("download"):
                res = app.make_response(res)
                res.headers["Content-Disposition"] = "attachment;filename=report_%s.html" % time()
            return res

        def _download_csv_suggest_file_name(suggest_filename_prefix):
            """Generate csv file download attachment filename suggestion.

            Arguments:
            suggest_filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
            """

            return f"{suggest_filename_prefix}_{time()}.csv"

        def _download_csv_response(csv_data, filename_prefix):
            """Generate csv file download response with 'csv_data'.

            Arguments:
            csv_data: CSV header and data rows.
            filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
            """

            response = make_response(csv_data)
            response.headers["Content-type"] = "text/csv"
            response.headers[
                "Content-disposition"
            ] = f"attachment;filename={_download_csv_suggest_file_name(filename_prefix)}"
            return response

        @app.route("/stats/requests/csv")
        @self.auth_required_if_enabled
        def request_stats_csv():
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.requests_csv(writer)
            return _download_csv_response(data.getvalue(), "requests")

        @app.route("/stats/requests_full_history/csv")
        @self.auth_required_if_enabled
        def request_stats_full_history_csv():
            options = self.environment.parsed_options
            if options and options.stats_history_enabled:
                return send_file(
                    os.path.abspath(self.stats_csv_writer.stats_history_file_name()),
                    mimetype="text/csv",
                    as_attachment=True,
                    attachment_filename=_download_csv_suggest_file_name("requests_full_history"),
                    add_etags=True,
                    cache_timeout=None,
                    conditional=True,
                    last_modified=None,
                )

            return make_response("Error: Server was not started with option to generate full history.", 404)

        @app.route("/stats/failures/csv")
        @self.auth_required_if_enabled
        def failures_stats_csv():
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.failures_csv(writer)
            return _download_csv_response(data.getvalue(), "failures")

        @app.route("/stats/requests")
        @self.auth_required_if_enabled
        @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
        def request_stats():
            stats = []

            for s in chain(sort_stats(self.environment.runner.stats.entries), [environment.runner.stats.total]):
                stats.append(
                    {
                        "method": s.method,
                        "name": s.name,
                        "safe_name": escape(s.name, quote=False),
                        "num_requests": s.num_requests,
                        "num_failures": s.num_failures,
                        "avg_response_time": s.avg_response_time,
                        "min_response_time": 0 if s.min_response_time is None else proper_round(s.min_response_time),
                        "max_response_time": proper_round(s.max_response_time),
                        "current_rps": s.current_rps,
                        "current_fail_per_sec": s.current_fail_per_sec,
                        "median_response_time": s.median_response_time,
                        "ninetieth_response_time": s.get_response_time_percentile(0.9),
                        "avg_content_length": s.avg_content_length,
                    }
                )

            errors = []
            for e in environment.runner.errors.values():
                err_dict = e.to_dict()
                err_dict["name"] = escape(err_dict["name"])
                err_dict["error"] = escape(err_dict["error"])
                errors.append(err_dict)

            # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
            # to render extremely slowly. Aggregate stats should be preserved.
            report = {"stats": stats[:500], "errors": errors[:500]}
            if len(stats) > 500:
                report["stats"] += [stats[-1]]

            if stats:
                report["total_rps"] = stats[len(stats) - 1]["current_rps"]
                report["fail_ratio"] = environment.runner.stats.total.fail_ratio
                report[
                    "current_response_time_percentile_95"
                ] = environment.runner.stats.total.get_current_response_time_percentile(0.95)
                report[
                    "current_response_time_percentile_50"
                ] = environment.runner.stats.total.get_current_response_time_percentile(0.5)

            is_distributed = isinstance(environment.runner, MasterRunner)
            if is_distributed:
                workers = []
                for worker in environment.runner.clients.values():
                    workers.append(
                        {
                            "id": worker.id,
                            "state": worker.state,
                            "user_count": worker.user_count,
                            "cpu_usage": worker.cpu_usage,
                        }
                    )

                report["workers"] = workers

            report["state"] = environment.runner.state
            report["user_count"] = environment.runner.user_count

            return jsonify(report)

        @app.route("/exceptions")
        @self.auth_required_if_enabled
        def exceptions():
            return jsonify(
                {
                    "exceptions": [
                        {
                            "count": row["count"],
                            "msg": row["msg"],
                            "traceback": row["traceback"],
                            "nodes": ", ".join(row["nodes"]),
                        }
                        for row in environment.runner.exceptions.values()
                    ]
                }
            )

        @app.route("/exceptions/csv")
        @self.auth_required_if_enabled
        def exceptions_csv():
            data = StringIO()
            writer = csv.writer(data)
            writer.writerow(["Count", "Message", "Traceback", "Nodes"])
            for exc in environment.runner.exceptions.values():
                nodes = ", ".join(exc["nodes"])
                writer.writerow([exc["count"], exc["msg"], exc["traceback"], nodes])

            return _download_csv_response(data.getvalue(), "exceptions")

    def start(self):
        self.greenlet = gevent.spawn(self.start_server)
        self.greenlet.link_exception(greenlet_exception_handler)

    def start_server(self):
        if self.tls_cert and self.tls_key:
            self.server = pywsgi.WSGIServer(
                (self.host, self.port), self.app, log=None, keyfile=self.tls_key, certfile=self.tls_cert
            )
        else:
            self.server = pywsgi.WSGIServer((self.host, self.port), self.app, log=None)
        self.server.serve_forever()

    def stop(self):
        """
        Stop the running web server
        """
        self.server.stop()

    def auth_required_if_enabled(self, view_func):
        """
        Decorator that can be used on custom route methods that will turn on Basic Auth
        authentication if the ``--web-auth`` flag is used. Example::

            @web_ui.app.route("/my_custom_route")
            @web_ui.auth_required_if_enabled
            def my_custom_route():
                return "custom response"
        """

        @wraps(view_func)
        def wrapper(*args, **kwargs):
            if self.app.config["BASIC_AUTH_ENABLED"]:
                if self.auth.authenticate():
                    return view_func(*args, **kwargs)
                else:
                    return self.auth.challenge()
            else:
                return view_func(*args, **kwargs)

        return wrapper

    def update_template_args(self):
        override_host_warning = False
        if self.environment.host:
            host = self.environment.host
        elif self.environment.runner.user_classes:
            all_hosts = set([l.host for l in self.environment.runner.user_classes])
            if len(all_hosts) == 1:
                host = list(all_hosts)[0]
            else:
                # since we have multiple User classes with different host attributes, we'll
                # inform that specifying host will override the host for all User classes
                override_host_warning = True
                host = None
        else:
            host = None

        options = self.environment.parsed_options

        is_distributed = isinstance(self.environment.runner, MasterRunner)
        if is_distributed:
            worker_count = self.environment.runner.worker_count
        else:
            worker_count = 0

        self.template_args = {
            "state": self.environment.runner.state,
            "is_distributed": is_distributed,
            "user_count": self.environment.runner.user_count,
            "version": version,
            "host": host,
            "override_host_warning": override_host_warning,
            "num_users": options and options.num_users,
            "spawn_rate": options and options.spawn_rate,
            "step_users": options and options.step_users,
            "step_time": options and options.step_time,
            "worker_count": worker_count,
            "is_step_load": self.environment.step_load,
            "is_shape": self.environment.shape_class,
            "stats_history_enabled": options and options.stats_history_enabled,
        }
Beispiel #3
0
class WebUI:
    """
    Sets up and runs a Flask web app that can start and stop load tests using the 
    :attr:`environment.runner <locust.env.Environment.runner>` as well as show the load test statistics 
    in :attr:`environment.stats <locust.env.Environment.stats>`
    """

    app = None
    """
    Reference to the :class:`flask.Flask` app. Can be used to add additional web routes and customize
    the Flask app in other various ways. Example::
    
        from flask import request
        
        @web_ui.app.route("/my_custom_route")
        def my_custom_route():
            return "your IP is: %s" % request.remote_addr
    """

    greenlet = None
    """
    Greenlet of the running web server
    """

    server = None
    """Reference to the :class:`pyqsgi.WSGIServer` instance"""
    def __init__(self,
                 environment,
                 host,
                 port,
                 auth_credentials=None,
                 tls_cert=None,
                 tls_key=None):
        """
        Create WebUI instance and start running the web server in a separate greenlet (self.greenlet)
        
        Arguments:
        environment: Reference to the curren Locust Environment
        host: Host/interface that the web server should accept connections to
        port: Port that the web server should listen to
        auth_credentials:  If provided, it will enable basic auth with all the routes protected by default.
                           Should be supplied in the format: "user:pass".
        tls_cert: A path to a TLS certificate
        tls_key: A path to a TLS private key
        """
        environment.web_ui = self
        self.environment = environment
        self.host = host
        self.port = port
        self.tls_cert = tls_cert
        self.tls_key = tls_key
        app = Flask(__name__)
        self.app = app
        app.debug = True
        app.root_path = os.path.dirname(os.path.abspath(__file__))
        self.app.config["BASIC_AUTH_ENABLED"] = False
        self.auth = None
        self.greenlet = None

        if auth_credentials is not None:
            credentials = auth_credentials.split(':')
            if len(credentials) == 2:
                self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
                self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
                self.app.config["BASIC_AUTH_ENABLED"] = True
                self.auth = BasicAuth()
                self.auth.init_app(self.app)
            else:
                raise AuthCredentialsError(
                    "Invalid auth_credentials. It should be a string in the following format: 'user.pass'"
                )

        @app.route('/')
        @self.auth_required_if_enabled
        def index():
            if not environment.runner:
                return make_response(
                    "Error: Locust Environment does not have any runner", 500)

            is_distributed = isinstance(environment.runner, MasterRunner)
            if is_distributed:
                worker_count = environment.runner.worker_count
            else:
                worker_count = 0

            override_host_warning = False
            if environment.host:
                host = environment.host
            elif environment.runner.user_classes:
                all_hosts = set(
                    [l.host for l in environment.runner.user_classes])
                if len(all_hosts) == 1:
                    host = list(all_hosts)[0]
                else:
                    # since we have mulitple User classes with different host attributes, we'll
                    # inform that specifying host will override the host for all User classes
                    override_host_warning = True
                    host = None
            else:
                host = None

            options = environment.parsed_options
            return render_template(
                "index.html",
                state=environment.runner.state,
                is_distributed=is_distributed,
                user_count=environment.runner.user_count,
                version=version,
                host=host,
                override_host_warning=override_host_warning,
                num_users=options and options.num_users,
                hatch_rate=options and options.hatch_rate,
                step_users=options and options.step_users,
                step_time=options and options.step_time,
                worker_count=worker_count,
                is_step_load=environment.step_load,
            )

        @app.route('/swarm', methods=["POST"])
        @self.auth_required_if_enabled
        def swarm():
            assert request.method == "POST"
            user_count = int(request.form["user_count"])
            hatch_rate = float(request.form["hatch_rate"])
            if (request.form.get("host")):
                environment.host = str(request.form["host"])

            if environment.step_load:
                step_user_count = int(request.form["step_user_count"])
                step_duration = parse_timespan(
                    str(request.form["step_duration"]))
                environment.runner.start_stepload(user_count, hatch_rate,
                                                  step_user_count,
                                                  step_duration)
                return jsonify({
                    'success': True,
                    'message': 'Swarming started in Step Load Mode',
                    'host': environment.host
                })

            environment.runner.start(user_count, hatch_rate)
            return jsonify({
                'success': True,
                'message': 'Swarming started',
                'host': environment.host
            })

        @app.route('/stop')
        @self.auth_required_if_enabled
        def stop():
            environment.runner.stop()
            return jsonify({'success': True, 'message': 'Test stopped'})

        @app.route("/stats/reset")
        @self.auth_required_if_enabled
        def reset_stats():
            environment.runner.stats.reset_all()
            environment.runner.exceptions = {}
            return "ok"

        @app.route("/stats/requests/csv")
        @self.auth_required_if_enabled
        def request_stats_csv():
            data = StringIO()
            writer = csv.writer(data)
            requests_csv(self.environment.runner.stats, writer)
            response = make_response(data.getvalue())
            file_name = "requests_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

        @app.route("/stats/failures/csv")
        @self.auth_required_if_enabled
        def failures_stats_csv():
            data = StringIO()
            writer = csv.writer(data)
            failures_csv(self.environment.runner.stats, writer)
            response = make_response(data.getvalue())
            file_name = "failures_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

        @app.route('/stats/requests')
        @self.auth_required_if_enabled
        @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
        def request_stats():
            stats = []

            for s in chain(sort_stats(self.environment.runner.stats.entries),
                           [environment.runner.stats.total]):
                stats.append({
                    "method":
                    s.method,
                    "name":
                    s.name,
                    "safe_name":
                    escape(s.name, quote=False),
                    "num_requests":
                    s.num_requests,
                    "num_failures":
                    s.num_failures,
                    "avg_response_time":
                    s.avg_response_time,
                    "min_response_time":
                    0 if s.min_response_time is None else proper_round(
                        s.min_response_time),
                    "max_response_time":
                    proper_round(s.max_response_time),
                    "current_rps":
                    s.current_rps,
                    "current_fail_per_sec":
                    s.current_fail_per_sec,
                    "median_response_time":
                    s.median_response_time,
                    "ninetieth_response_time":
                    s.get_response_time_percentile(0.9),
                    "avg_content_length":
                    s.avg_content_length,
                })

            errors = []
            for e in environment.runner.errors.values():
                err_dict = e.to_dict()
                err_dict["name"] = escape(err_dict["name"])
                err_dict["error"] = escape(err_dict["error"])
                errors.append(err_dict)

            # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
            # to render extremely slowly. Aggregate stats should be preserved.
            report = {"stats": stats[:500], "errors": errors[:500]}
            if len(stats) > 500:
                report["stats"] += [stats[-1]]

            if stats:
                report["total_rps"] = stats[len(stats) - 1]["current_rps"]
                report[
                    "fail_ratio"] = environment.runner.stats.total.fail_ratio
                report[
                    "current_response_time_percentile_95"] = environment.runner.stats.total.get_current_response_time_percentile(
                        0.95)
                report[
                    "current_response_time_percentile_50"] = environment.runner.stats.total.get_current_response_time_percentile(
                        0.5)

            is_distributed = isinstance(environment.runner, MasterRunner)
            if is_distributed:
                workers = []
                for worker in environment.runner.clients.values():
                    workers.append({
                        "id": worker.id,
                        "state": worker.state,
                        "user_count": worker.user_count,
                        "cpu_usage": worker.cpu_usage
                    })

                report["workers"] = workers

            report["state"] = environment.runner.state
            report["user_count"] = environment.runner.user_count

            return jsonify(report)

        @app.route("/exceptions")
        @self.auth_required_if_enabled
        def exceptions():
            return jsonify({
                'exceptions': [{
                    "count": row["count"],
                    "msg": row["msg"],
                    "traceback": row["traceback"],
                    "nodes": ", ".join(row["nodes"])
                } for row in environment.runner.exceptions.values()]
            })

        @app.route("/exceptions/csv")
        @self.auth_required_if_enabled
        def exceptions_csv():
            data = StringIO()
            writer = csv.writer(data)
            writer.writerow(["Count", "Message", "Traceback", "Nodes"])
            for exc in environment.runner.exceptions.values():
                nodes = ", ".join(exc["nodes"])
                writer.writerow(
                    [exc["count"], exc["msg"], exc["traceback"], nodes])

            response = make_response(data.getvalue())
            file_name = "exceptions_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

        # start the web server
        self.greenlet = gevent.spawn(self.start)
        self.greenlet.link_exception(greenlet_exception_handler)

    def start(self):
        if self.tls_cert and self.tls_key:
            self.server = pywsgi.WSGIServer((self.host, self.port),
                                            self.app,
                                            log=None,
                                            keyfile=self.tls_key,
                                            certfile=self.tls_cert)
        else:
            self.server = pywsgi.WSGIServer((self.host, self.port),
                                            self.app,
                                            log=None)
        self.server.serve_forever()

    def stop(self):
        """
        Stop the running web server
        """
        self.server.stop()

    def auth_required_if_enabled(self, view_func):
        """
        Decorator that can be used on custom route methods that will turn on Basic Auth 
        authentication if the ``--web-auth`` flag is used. Example::
        
            @web_ui.app.route("/my_custom_route")
            @web_ui.auth_required_if_enabled
            def my_custom_route():
                return "custom response"
        """
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            if self.app.config["BASIC_AUTH_ENABLED"]:
                if self.auth.authenticate():
                    return view_func(*args, **kwargs)
                else:
                    return self.auth.challenge()
            else:
                return view_func(*args, **kwargs)

        return wrapper
Beispiel #4
0
class WebUI:
    server = None
    """Reference to pyqsgi.WSGIServer once it's started"""
    def __init__(self, environment, auth_credentials=None):
        """
        If auth_credentials is provided, it will enable basic auth with all the routes protected by default.
        Should be supplied in the format: "user:pass".
        """
        environment.web_ui = self
        self.environment = environment
        app = Flask(__name__)
        self.app = app
        app.debug = True
        app.root_path = os.path.dirname(os.path.abspath(__file__))
        self.app.config["BASIC_AUTH_ENABLED"] = False
        self.auth = None

        if auth_credentials is not None:
            credentials = auth_credentials.split(':')
            if len(credentials) == 2:
                self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
                self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
                self.app.config["BASIC_AUTH_ENABLED"] = True
                self.auth = BasicAuth()
                self.auth.init_app(self.app)
            else:
                raise AuthCredentialsError(
                    "Invalid auth_credentials. It should be a string in the following format: 'user.pass'"
                )

        @app.route('/')
        @self.auth_required_if_enabled
        def index():
            if not environment.runner:
                return make_response(
                    "Error: Locust Environment does not have any runner", 500)

            is_distributed = isinstance(environment.runner, MasterLocustRunner)
            if is_distributed:
                worker_count = environment.runner.worker_count
            else:
                worker_count = 0

            override_host_warning = False
            if environment.host:
                host = environment.host
            elif environment.runner.locust_classes:
                all_hosts = set(
                    [l.host for l in environment.runner.locust_classes])
                if len(all_hosts) == 1:
                    host = list(all_hosts)[0]
                else:
                    # since we have mulitple Locust classes with different host attributes, we'll
                    # inform that specifying host will override the host for all Locust classes
                    override_host_warning = True
                    host = None
            else:
                host = None

            return render_template(
                "index.html",
                state=environment.runner.state,
                is_distributed=is_distributed,
                user_count=environment.runner.user_count,
                version=version,
                host=host,
                override_host_warning=override_host_warning,
                worker_count=worker_count,
                is_step_load=environment.step_load,
            )

        @app.route('/swarm', methods=["POST"])
        @self.auth_required_if_enabled
        def swarm():
            assert request.method == "POST"
            locust_count = int(request.form["locust_count"])
            hatch_rate = float(request.form["hatch_rate"])
            if (request.form.get("host")):
                environment.host = str(request.form["host"])

            if environment.step_load:
                step_locust_count = int(request.form["step_locust_count"])
                step_duration = parse_timespan(
                    str(request.form["step_duration"]))
                environment.runner.start_stepload(locust_count, hatch_rate,
                                                  step_locust_count,
                                                  step_duration)
                return jsonify({
                    'success': True,
                    'message': 'Swarming started in Step Load Mode',
                    'host': environment.host
                })

            environment.runner.start(locust_count, hatch_rate)
            return jsonify({
                'success': True,
                'message': 'Swarming started',
                'host': environment.host
            })

        @app.route('/stop')
        @self.auth_required_if_enabled
        def stop():
            environment.runner.stop()
            return jsonify({'success': True, 'message': 'Test stopped'})

        @app.route("/stats/reset")
        @self.auth_required_if_enabled
        def reset_stats():
            environment.runner.stats.reset_all()
            environment.runner.exceptions = {}
            return "ok"

        @app.route("/stats/requests/csv")
        @self.auth_required_if_enabled
        def request_stats_csv():
            response = make_response(
                requests_csv(self.environment.runner.stats))
            file_name = "requests_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

        @app.route("/stats/failures/csv")
        @self.auth_required_if_enabled
        def failures_stats_csv():
            response = make_response(
                failures_csv(self.environment.runner.stats))
            file_name = "failures_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

        @app.route('/stats/requests')
        @self.auth_required_if_enabled
        @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
        def request_stats():
            stats = []

            for s in chain(sort_stats(self.environment.runner.stats.entries),
                           [environment.runner.stats.total]):
                stats.append({
                    "method":
                    s.method,
                    "name":
                    s.name,
                    "safe_name":
                    escape(s.name, quote=False),
                    "num_requests":
                    s.num_requests,
                    "num_failures":
                    s.num_failures,
                    "avg_response_time":
                    s.avg_response_time,
                    "min_response_time":
                    0 if s.min_response_time is None else proper_round(
                        s.min_response_time),
                    "max_response_time":
                    proper_round(s.max_response_time),
                    "current_rps":
                    s.current_rps,
                    "current_fail_per_sec":
                    s.current_fail_per_sec,
                    "median_response_time":
                    s.median_response_time,
                    "ninetieth_response_time":
                    s.get_response_time_percentile(0.9),
                    "avg_content_length":
                    s.avg_content_length,
                })

            errors = []
            for e in environment.runner.errors.values():
                err_dict = e.to_dict()
                err_dict["name"] = escape(err_dict["name"])
                err_dict["error"] = escape(err_dict["error"])
                errors.append(err_dict)

            # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
            # to render extremely slowly. Aggregate stats should be preserved.
            report = {"stats": stats[:500], "errors": errors[:500]}
            if len(stats) > 500:
                report["stats"] += [stats[-1]]

            if stats:
                report["total_rps"] = stats[len(stats) - 1]["current_rps"]
                report[
                    "fail_ratio"] = environment.runner.stats.total.fail_ratio
                report[
                    "current_response_time_percentile_95"] = environment.runner.stats.total.get_current_response_time_percentile(
                        0.95)
                report[
                    "current_response_time_percentile_50"] = environment.runner.stats.total.get_current_response_time_percentile(
                        0.5)

            is_distributed = isinstance(environment.runner, MasterLocustRunner)
            if is_distributed:
                workers = []
                for worker in environment.runner.clients.values():
                    workers.append({
                        "id": worker.id,
                        "state": worker.state,
                        "user_count": worker.user_count,
                        "cpu_usage": worker.cpu_usage
                    })

                report["workers"] = workers

            report["state"] = environment.runner.state
            report["user_count"] = environment.runner.user_count

            return jsonify(report)

        @app.route("/exceptions")
        @self.auth_required_if_enabled
        def exceptions():
            return jsonify({
                'exceptions': [{
                    "count": row["count"],
                    "msg": row["msg"],
                    "traceback": row["traceback"],
                    "nodes": ", ".join(row["nodes"])
                } for row in environment.runner.exceptions.values()]
            })

        @app.route("/exceptions/csv")
        @self.auth_required_if_enabled
        def exceptions_csv():
            data = StringIO()
            writer = csv.writer(data)
            writer.writerow(["Count", "Message", "Traceback", "Nodes"])
            for exc in environment.runner.exceptions.values():
                nodes = ", ".join(exc["nodes"])
                writer.writerow(
                    [exc["count"], exc["msg"], exc["traceback"], nodes])

            data.seek(0)
            response = make_response(data.read())
            file_name = "exceptions_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

    def start(self, host, port):
        self.server = pywsgi.WSGIServer((host, port), self.app, log=None)
        self.server.serve_forever()

    def stop(self):
        self.server.stop()

    def auth_required_if_enabled(self, view_func):
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            if self.app.config["BASIC_AUTH_ENABLED"]:
                if self.auth.authenticate():
                    return view_func(*args, **kwargs)
                else:
                    return self.auth.challenge()
            else:
                return view_func(*args, **kwargs)

        return wrapper
Beispiel #5
0
class WebUI:
    """
    Sets up and runs a Flask web app that can start and stop load tests using the
    :attr:`environment.runner <locust.env.Environment.runner>` as well as show the load test statistics
    in :attr:`environment.stats <locust.env.Environment.stats>`
    """

    app: Optional[Flask] = None
    """
    Reference to the :class:`flask.Flask` app. Can be used to add additional web routes and customize
    the Flask app in other various ways. Example::

        from flask import request

        @web_ui.app.route("/my_custom_route")
        def my_custom_route():
            return "your IP is: %s" % request.remote_addr
    """

    greenlet: Optional[gevent.Greenlet] = None
    """
    Greenlet of the running web server
    """

    server: Optional[pywsgi.WSGIServer] = None
    """Reference to the :class:`pyqsgi.WSGIServer` instance"""

    template_args: Dict[str, Any]
    """Arguments used to render index.html for the web UI. Must be used with custom templates
    extending index.html."""
    def __init__(
        self,
        environment: "Environment",
        host: str,
        port: int,
        auth_credentials: Optional[str] = None,
        tls_cert: Optional[str] = None,
        tls_key: Optional[str] = None,
        stats_csv_writer: Optional[StatsCSV] = None,
        delayed_start=False,
    ):
        """
        Create WebUI instance and start running the web server in a separate greenlet (self.greenlet)

        Arguments:
        environment: Reference to the current Locust Environment
        host: Host/interface that the web server should accept connections to
        port: Port that the web server should listen to
        auth_credentials:  If provided, it will enable basic auth with all the routes protected by default.
                           Should be supplied in the format: "user:pass".
        tls_cert: A path to a TLS certificate
        tls_key: A path to a TLS private key
        delayed_start: Whether or not to delay starting web UI until `start()` is called. Delaying web UI start
                       allows for adding Flask routes or Blueprints before accepting requests, avoiding errors.
        """
        environment.web_ui = self
        self.stats_csv_writer = stats_csv_writer or StatsCSV(
            environment, stats_module.PERCENTILES_TO_REPORT)
        self.environment = environment
        self.host = host
        self.port = port
        self.tls_cert = tls_cert
        self.tls_key = tls_key
        app = Flask(__name__)
        CORS(app)
        self.app = app
        app.jinja_env.add_extension("jinja2.ext.do")
        app.debug = True
        app.root_path = os.path.dirname(os.path.abspath(__file__))
        self.app.config["BASIC_AUTH_ENABLED"] = False
        self.auth: Optional[BasicAuth] = None
        self.greenlet: Optional[gevent.Greenlet] = None
        self._swarm_greenlet: Optional[gevent.Greenlet] = None
        self.template_args = {}

        if auth_credentials is not None:
            credentials = auth_credentials.split(":")
            if len(credentials) == 2:
                self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
                self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
                self.app.config["BASIC_AUTH_ENABLED"] = True
                self.auth = BasicAuth()
                self.auth.init_app(self.app)
            else:
                raise AuthCredentialsError(
                    "Invalid auth_credentials. It should be a string in the following format: 'user:pass'"
                )
        if environment.runner:
            self.update_template_args()
        if not delayed_start:
            self.start()

        @app.route("/")
        @self.auth_required_if_enabled
        def index() -> Union[str, Response]:
            if not environment.runner:
                return make_response(
                    "Error: Locust Environment does not have any runner", 500)
            self.update_template_args()
            return render_template("index.html", **self.template_args)

        @app.route("/swarm", methods=["POST"])
        @self.auth_required_if_enabled
        def swarm() -> Response:
            assert request.method == "POST"

            parsed_options_dict = vars(environment.parsed_options
                                       ) if environment.parsed_options else {}
            for key, value in request.form.items():
                if key == "user_count":  # if we just renamed this field to "users" we wouldn't need this
                    user_count = int(value)
                elif key == "spawn_rate":
                    spawn_rate = float(value)
                elif key == "host":
                    # Replace < > to guard against XSS
                    environment.host = str(request.form["host"]).replace(
                        "<", "").replace(">", "")
                elif key in parsed_options_dict:
                    # update the value in environment.parsed_options, but dont change the type.
                    # This won't work for parameters that are None
                    parsed_options_dict[key] = type(
                        parsed_options_dict[key])(value)

            if environment.shape_class and environment.runner is not None:
                environment.runner.start_shape()
                return jsonify({
                    "success": True,
                    "message": "Swarming started using shape class",
                    "host": environment.host
                })

            if self._swarm_greenlet is not None:
                self._swarm_greenlet.kill(block=True)
                self._swarm_greenlet = None

            if environment.runner is not None:
                self._swarm_greenlet = gevent.spawn(environment.runner.start,
                                                    user_count, spawn_rate)
                self._swarm_greenlet.link_exception(greenlet_exception_handler)
                return jsonify({
                    "success": True,
                    "message": "Swarming started",
                    "host": environment.host
                })
            else:
                return jsonify({
                    "success": False,
                    "message": "No runner",
                    "host": environment.host
                })

        @app.route("/stop")
        @self.auth_required_if_enabled
        def stop() -> Response:
            if self._swarm_greenlet is not None:
                self._swarm_greenlet.kill(block=True)
                self._swarm_greenlet = None
            if environment.runner is not None:
                environment.runner.stop()
            return jsonify({"success": True, "message": "Test stopped"})

        @app.route("/stats/reset")
        @self.auth_required_if_enabled
        def reset_stats() -> str:
            environment.events.reset_stats.fire()
            if environment.runner is not None:
                environment.runner.stats.reset_all()
                environment.runner.exceptions = {}
            return "ok"

        @app.route("/stats/report")
        @self.auth_required_if_enabled
        def stats_report() -> Response:
            res = get_html_report(
                self.environment,
                show_download_link=not request.args.get("download"))
            if request.args.get("download"):
                res = app.make_response(res)
                res.headers[
                    "Content-Disposition"] = f"attachment;filename=report_{time()}.html"
            return res

        def _download_csv_suggest_file_name(
                suggest_filename_prefix: str) -> str:
            """Generate csv file download attachment filename suggestion.

            Arguments:
            suggest_filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
            """

            return f"{suggest_filename_prefix}_{time()}.csv"

        def _download_csv_response(csv_data: str,
                                   filename_prefix: str) -> Response:
            """Generate csv file download response with 'csv_data'.

            Arguments:
            csv_data: CSV header and data rows.
            filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
            """

            response = make_response(csv_data)
            response.headers["Content-type"] = "text/csv"
            response.headers[
                "Content-disposition"] = f"attachment;filename={_download_csv_suggest_file_name(filename_prefix)}"
            return response

        @app.route("/stats/requests/csv")
        @self.auth_required_if_enabled
        def request_stats_csv() -> Response:
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.requests_csv(writer)
            return _download_csv_response(data.getvalue(), "requests")

        @app.route("/stats/requests_full_history/csv")
        @self.auth_required_if_enabled
        def request_stats_full_history_csv() -> Response:
            options = self.environment.parsed_options
            if options and options.stats_history_enabled and isinstance(
                    self.stats_csv_writer, StatsCSVFileWriter):
                return send_file(
                    os.path.abspath(
                        self.stats_csv_writer.stats_history_file_name()),
                    mimetype="text/csv",
                    as_attachment=True,
                    download_name=_download_csv_suggest_file_name(
                        "requests_full_history"),
                    etag=True,
                    cache_timeout=None,
                    conditional=True,
                    last_modified=None,
                )

            return make_response(
                "Error: Server was not started with option to generate full history.",
                404)

        @app.route("/stats/failures/csv")
        @self.auth_required_if_enabled
        def failures_stats_csv() -> Response:
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.failures_csv(writer)
            return _download_csv_response(data.getvalue(), "failures")

        @app.route("/stats/requests")
        @self.auth_required_if_enabled
        @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
        def request_stats() -> Response:
            stats: List[Dict[str, Any]] = []
            errors: List[StatsErrorDict] = []

            if environment.runner is None:
                report = {
                    "stats": stats,
                    "errors": errors,
                    "total_rps": 0.0,
                    "fail_ratio": 0.0,
                    "current_response_time_percentile_95": None,
                    "current_response_time_percentile_50": None,
                    "state": STATE_MISSING,
                    "user_count": 0,
                }

                if isinstance(environment.runner, MasterRunner):
                    report.update({"workers": []})

                return jsonify(report)

            for s in chain(sort_stats(environment.runner.stats.entries),
                           [environment.runner.stats.total]):
                stats.append({
                    "method":
                    s.method,
                    "name":
                    s.name,
                    "safe_name":
                    escape(s.name, quote=False),
                    "num_requests":
                    s.num_requests,
                    "num_failures":
                    s.num_failures,
                    "avg_response_time":
                    s.avg_response_time,
                    "min_response_time":
                    0 if s.min_response_time is None else proper_round(
                        s.min_response_time),
                    "max_response_time":
                    proper_round(s.max_response_time),
                    "current_rps":
                    s.current_rps,
                    "current_fail_per_sec":
                    s.current_fail_per_sec,
                    "median_response_time":
                    s.median_response_time,
                    "ninetieth_response_time":
                    s.get_response_time_percentile(0.9),
                    "ninety_ninth_response_time":
                    s.get_response_time_percentile(0.99),
                    "avg_content_length":
                    s.avg_content_length,
                })

            for e in environment.runner.errors.values():
                err_dict = e.serialize()
                err_dict["name"] = escape(err_dict["name"])
                err_dict["error"] = escape(err_dict["error"])
                errors.append(err_dict)

            # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
            # to render extremely slowly. Aggregate stats should be preserved.
            truncated_stats = stats[:500]
            if len(stats) > 500:
                truncated_stats += [stats[-1]]

            report = {"stats": truncated_stats, "errors": errors[:500]}

            if stats:
                report["total_rps"] = stats[len(stats) - 1]["current_rps"]
                report[
                    "fail_ratio"] = environment.runner.stats.total.fail_ratio
                report[
                    "current_response_time_percentile_95"] = environment.runner.stats.total.get_current_response_time_percentile(
                        0.95)
                report[
                    "current_response_time_percentile_50"] = environment.runner.stats.total.get_current_response_time_percentile(
                        0.5)

            if isinstance(environment.runner, MasterRunner):
                workers = []
                for worker in environment.runner.clients.values():
                    workers.append({
                        "id": worker.id,
                        "state": worker.state,
                        "user_count": worker.user_count,
                        "cpu_usage": worker.cpu_usage,
                        "memory_usage": worker.memory_usage,
                    })

                report["workers"] = workers

            report["state"] = environment.runner.state
            report["user_count"] = environment.runner.user_count

            return jsonify(report)

        @app.route("/exceptions")
        @self.auth_required_if_enabled
        def exceptions() -> Response:
            return jsonify({
                "exceptions": [{
                    "count": row["count"],
                    "msg": row["msg"],
                    "traceback": row["traceback"],
                    "nodes": ", ".join(row["nodes"]),
                } for row in (environment.runner.exceptions.values(
                ) if environment.runner is not None else [])]
            })

        @app.route("/exceptions/csv")
        @self.auth_required_if_enabled
        def exceptions_csv() -> Response:
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.exceptions_csv(writer)
            return _download_csv_response(data.getvalue(), "exceptions")

        @app.route("/tasks")
        @self.auth_required_if_enabled
        def tasks() -> Dict[str, Dict[str, Dict[str, float]]]:
            runner = self.environment.runner
            user_spawned: Dict[str, int]
            if runner is None:
                user_spawned = {}
            else:
                user_spawned = (runner.reported_user_classes_count
                                if isinstance(runner, MasterRunner) else
                                runner.user_classes_count)

            task_data = {
                "per_class":
                get_ratio(self.environment.user_classes, user_spawned, False),
                "total":
                get_ratio(self.environment.user_classes, user_spawned, True),
            }
            return task_data

    def start(self):
        self.greenlet = gevent.spawn(self.start_server)
        self.greenlet.link_exception(greenlet_exception_handler)

    def start_server(self):
        if self.tls_cert and self.tls_key:
            self.server = pywsgi.WSGIServer((self.host, self.port),
                                            self.app,
                                            log=None,
                                            keyfile=self.tls_key,
                                            certfile=self.tls_cert)
        else:
            self.server = pywsgi.WSGIServer((self.host, self.port),
                                            self.app,
                                            log=None)
        self.server.serve_forever()

    def stop(self):
        """
        Stop the running web server
        """
        self.server.stop()

    def auth_required_if_enabled(self, view_func):
        """
        Decorator that can be used on custom route methods that will turn on Basic Auth
        authentication if the ``--web-auth`` flag is used. Example::

            @web_ui.app.route("/my_custom_route")
            @web_ui.auth_required_if_enabled
            def my_custom_route():
                return "custom response"
        """
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            if self.app.config["BASIC_AUTH_ENABLED"]:
                if self.auth.authenticate():
                    return view_func(*args, **kwargs)
                else:
                    return self.auth.challenge()
            else:
                return view_func(*args, **kwargs)

        return wrapper

    def update_template_args(self):
        override_host_warning = False
        if self.environment.host:
            host = self.environment.host
        elif self.environment.runner.user_classes:
            all_hosts = {l.host for l in self.environment.runner.user_classes}
            if len(all_hosts) == 1:
                host = list(all_hosts)[0]
            else:
                # since we have multiple User classes with different host attributes, we'll
                # inform that specifying host will override the host for all User classes
                override_host_warning = True
                host = None
        else:
            host = None

        options = self.environment.parsed_options

        is_distributed = isinstance(self.environment.runner, MasterRunner)
        if is_distributed:
            worker_count = self.environment.runner.worker_count
        else:
            worker_count = 0

        stats = self.environment.runner.stats
        extra_options = argument_parser.ui_extra_args_dict()

        self.template_args = {
            "locustfile": self.environment.locustfile,
            "state": self.environment.runner.state,
            "is_distributed": is_distributed,
            "user_count": self.environment.runner.user_count,
            "version": version,
            "host": host,
            "history": stats.history if stats.num_requests > 0 else {},
            "override_host_warning": override_host_warning,
            "num_users": options and options.num_users,
            "spawn_rate": options and options.spawn_rate,
            "worker_count": worker_count,
            "is_shape": self.environment.shape_class,
            "stats_history_enabled": options and options.stats_history_enabled,
            "tasks": dumps({}),
            "extra_options": extra_options,
        }
Beispiel #6
0
class WebUI:
    """
    Sets up and runs a Flask web app that can start and stop load tests using the
    :attr:`environment.runner <locust.env.Environment.runner>` as well as show the load test statistics
    in :attr:`environment.stats <locust.env.Environment.stats>`
    """

    app = None
    greenlet = None
    server = None
    etcdt = None
    reporter_running_status=False
    """Reference to the :class:`pyqsgi.WSGIServer` instance"""

    def  __init__(self,environment,host,port,masterHost,auth_credentials=None,tls_cert=None,tls_key=None,stats_csv_writer=None):
        """
        Create WebUI instance and start running the web server in a separate greenlet (self.greenlet)

        Arguments:
        environment: Reference to the curren Locust Environment
        host: Host/interface that the web server should accept connections to
        port: Port that the web server should listen to
        auth_credentials:  If provided, it will enable basic auth with all the routes protected by default.
                           Should be supplied in the format: "user:pass".
        tls_cert: A path to a TLS certificate
        tls_key: A path to a TLS private key
        """
        environment.web_ui = self
        self.stats_csv_writer = stats_csv_writer or StatsCSV(environment,stats_module.PERCENTILES_TO_REPORT)
        self.environment = environment
        self.host = host
        self.port = port
        self.tls_cert = tls_cert
        self.tls_key = tls_key
        app = Flask(__name__)
        self.app = app
        app.debug = True
        app.root_path = os.path.dirname(os.path.abspath(__file__))
        self.app.config["BASIC_AUTH_ENABLED"] = False
        self.auth = None
        self.greenlet = None
        self.masterHost = masterHost
        self.etcdt=EtcdTooler("/ns/boomer_service/")
        self.workedServser={}
        self.recvMesg={}

        if auth_credentials is not None:
            credentials = auth_credentials.split(':')
            if len(credentials) == 2:
                self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
                self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
                self.app.config["BASIC_AUTH_ENABLED"] = True
                self.auth = BasicAuth()
                self.auth.init_app(self.app)
            else:
                raise AuthCredentialsError(
                    "Invalid auth_credentials. It should be a string in the following format: 'user.pass'")

        def stats_history(self):
            """Save current stats info to history for charts of report."""
            while self.reporter_running_status:
                stats = environment.runner.stats
                if not stats.total.use_response_times_cache:
                    break
                r = {
                    "time":                        datetime.datetime.now().strftime("%H:%M:%S"),
                    "current_rps":                 stats.total.current_rps or 0,
                    "current_fail_per_sec":        stats.total.current_fail_per_sec or 0,
                    "response_time_percentile_95": stats.total.get_current_response_time_percentile(0.95) or 0,
                    "response_time_percentile_50": stats.total.get_current_response_time_percentile(0.5) or 0,
                    "user_count":                  environment.runner.user_count or 0,
                }
                stats.history.append(r)
                gevent.sleep(5)
            logger.info("......结束了指标历史记录任务......")

        def initTask(self,rpcServAddr,initBommerRequest):
            """
            调用gRPC,通知初始化Boomer,改成同步处理
            :param rpcServAddr:
            :return:
            """
            s=datetime.datetime.now()
            try:
                channel = grpc.insecure_channel(rpcServAddr)  # 连接 rpc 服务器
                # 调用 rpc 服务
                stub = boomerCall_pb2_grpc.BoomerCallServiceStub(channel)
            except Exception as e:
                logger.error(e)
                return
            beforeClientIds=[ worker.id  for worker in  environment.runner.clients.values()]
            try:
                print(initBommerRequest)
                response = stub.InitBommer(initBommerRequest,timeout=15)
                self.recvMesg[rpcServAddr] = response.message

            except Exception as e:
                self.recvMesg[rpcServAddr] = "连接服务器异常:%s"%(e)
                return

            if response and response.status:
                tryCount=0
                newId=""
                while tryCount<=50: # 5s的超时
                    afterClientIds = [worker.id for worker in environment.runner.clients.values()]
                    dfIdSet = set(afterClientIds).difference(set(beforeClientIds))
                    if not dfIdSet:
                        gevent.sleep(0.1)
                        tryCount+=1
                    else:
                        newId=dfIdSet.pop()
                        break
                self.workedServser[rpcServAddr]=newId
            try:
                channel.close()
            except:
                pass
            return

        def shutTask(self,rpcServAddr):
            """
            调用gRPC,通知关闭Boomer
            :param rpcServAddr:
            :return:
            """
            try:
                channel = grpc.insecure_channel(rpcServAddr)  # 连接 rpc 服务器
                # 调用 rpc 服务
                stub = boomerCall_pb2_grpc.BoomerCallServiceStub(channel)
                response = stub.EndBommer(boomerCall_pb2.EndBommerRequest(),timeout=15)
            except Exception as e:
                self.recvMesg[rpcServAddr]="连接服务器异常:%s"%(e)
                return
            self.recvMesg[rpcServAddr] = response.message
            if response and response.status:
                if self.workedServser.__contains__(rpcServAddr):
                    del self.workedServser[rpcServAddr]
                if self.recvMesg.__contains__(rpcServAddr):
                    # 成功情况下,不需要获取消息
                    del self.recvMesg[rpcServAddr]
            try:
                channel.close()
            except:
                pass

        @app.route('/')
        @self.auth_required_if_enabled
        def index():
            if not environment.runner:
                return make_response("Error: Locust Environment does not have any runner",500)

            is_distributed = isinstance(environment.runner,MasterRunner)
            if is_distributed:
                worker_count = environment.runner.worker_count
                slave_count = len(self.etcdt.servAddressList)
            else:
                worker_count = 0
                slave_count = 0

            override_host_warning = False
            if environment.host:
                host = environment.host
            elif environment.runner.user_classes:
                all_hosts = set([l.host for l in environment.runner.user_classes])
                if len(all_hosts) == 1:
                    host = list(all_hosts)[0]
                else:
                    # since we have mulitple User classes with different host attributes, we'll
                    # inform that specifying host will override the host for all User classes
                    override_host_warning = True
                    host = None
            else:
                host = None

            return render_template("index.html",
                                   state=environment.runner.state,
                                   is_distributed=is_distributed,
                                   user_count=environment.runner.user_count,
                                   version=version,
                                   host=host,
                                   override_host_warning=override_host_warning,
                                   worker_count=worker_count,
                                   slave_count= slave_count,
                                   is_step_load=environment.step_load,
                                   )




        @app.route('/swarm',methods=["POST"])
        @self.auth_required_if_enabled
        def swarm():
            if environment.runner.state not in (runners.STATE_STOPPED,runners.STATE_INIT):
                return jsonify({'success': False,'message': '当前有任务正在执行,先停止测试再尝试'})
            if not  environment.runner.worker_count:
                 return jsonify({'success': False,'message': '没有可用的work, 不能运行测试'})
            assert request.method == "POST"
            # 清理下统计信息
            environment.runner.stats.clear_all()
            environment.runner.exceptions = {}
            # 开启协程写入指标历史记录
            self.reporter_running_status=True
            gevent.spawn(stats_history,self)
            # 开始压测任务
            user_count = int(request.form["user_count"])
            hatch_rate = float(request.form["hatch_rate"])
            run_seconds=None
            if request.form["run_time"]:
                try:
                    run_seconds = parse_timespan(request.form["run_time"])
                except ValueError:
                    pass
            if request.form.get("host"):
                environment.host = str(request.form["host"])
            def stopRunAfterSecs(x):
                if x>3600*6:
                    x=3600*6
                count=0
                while count<=x: # 这种方式避免长时间休眠造成问题
                    gevent.sleep(1)
                    count+=1
                self.reporter_running_status = False  # 结束指标历史记录
                environment.runner.stop()
            if run_seconds and run_seconds>=30:
                gevent.spawn(stopRunAfterSecs,run_seconds)
            step_user_count=None
            step_duration=None
            try:
                step_user_count = int(request.form["step_user_count"])
                step_duration = parse_timespan(str(request.form["step_duration"]))
            except:
                pass
            if environment.step_load and step_user_count and step_duration:
                environment.runner.start_stepload(user_count,hatch_rate,step_user_count,step_duration)
                return jsonify(
                    {'success': True,'message': 'Swarming started in Step Load Mode','host': environment.host})
            environment.runner.start(user_count,hatch_rate)
            return jsonify({'success': True,'message': 'Swarming started','host': environment.host})

        @app.route('/stop')
        @self.auth_required_if_enabled
        def stop():
            self.reporter_running_status=False #结束指标历史记录
            if environment.runner.state not in (runners.STATE_SPAWNING,runners.STATE_STOPPING,runners.STATE_STOPPED,runners.STATE_INIT):
                environment.runner.stop()
                return jsonify({'success': True,'message': '测试已经停止'})
            elif environment.runner.state == runners.STATE_SPAWNING:
                environment.runner.stop()
                return jsonify({'success': False,'message': '测试用户增加中,请多试几次'})
            else:
                return jsonify({'success': False,'message': '测试没有运行'})

        @app.route("/stats/reset")
        @self.auth_required_if_enabled
        def reset_stats():
            if environment.runner.state == runners.STATE_STOPPED:
                environment.runner.stats.clear_all()
                environment.runner.exceptions = {}
                environment.host=None
                return "ok"
            environment.runner.stats.reset_all()
            environment.runner.exceptions = {}
            return "ok"

        def makeHttpItem(request,prefixStr,i):
            """
            构造任务
            :return:
            """
            xPretask = {}
            pretask_x_method = request.form.get("%s-%d-method" % (prefixStr,i),type=str,default="Get")
            xPretask["Method"] = pretask_x_method
            pretask_x_urlPath = request.form.get("%s-%d-urlPath" % (prefixStr,i),type=str,default="")
            xPretask["UrlPath"] = pretask_x_urlPath
            # Headers 处理
            pretask_x_headers_key = request.form.getlist("%s-%d-headers-key" % (prefixStr,i))
            pretask_x_headers_value = request.form.getlist("%s-%d-headers-value" % (prefixStr,i))
            if pretask_x_headers_key and pretask_x_headers_value and \
                    len(pretask_x_headers_key) == len(pretask_x_headers_value):
                xPretask["Headers"]={}
                for j in range(len(pretask_x_headers_key)):
                    xPretask["Headers"][pretask_x_headers_key[j]] = pretask_x_headers_value[j]
            else:
                xPretask["Headers"] = None
            # Params 处理
            pretask_x_params_key = request.form.getlist("%s-%d-params-key" % (prefixStr,i))
            pretask_x_params_value = request.form.getlist("%s-%d-params-value" % (prefixStr,i))
            if pretask_x_params_key and pretask_x_params_value and \
                    len(pretask_x_params_key) == len(pretask_x_params_value):
                xPretask["Params"]={}
                for j in range(len(pretask_x_params_key)):
                    xPretask["Params"][pretask_x_params_key[j]] = pretask_x_params_value[j]
            else:
                xPretask["Params"] = None
            # DictData 处理
            pretask_x_dictdata_key = request.form.getlist("%s-%d-dictdata-key" % (prefixStr,i))
            pretask_x_dictdata_value = request.form.getlist("%s-%d-dictdata-value" % (prefixStr,i))
            if pretask_x_dictdata_key and pretask_x_dictdata_value and \
                    len(pretask_x_dictdata_key) == len(pretask_x_dictdata_value):
                xPretask["DictData"]={}
                for j in range(len(pretask_x_dictdata_key)):
                    xPretask["DictData"][pretask_x_dictdata_key[j]] = pretask_x_dictdata_value[j]
            else:
                xPretask["DictData"] = None
            # RawData 处理
            pretask_x_rawdata = request.form.get("%s-%d-rawdata" % (prefixStr,i),type=str,default="")
            xPretask["RawData"] = pretask_x_rawdata
            # JsonData 处理
            pretask_x_jsondata = request.form.get("%s-%d-jsondata" % (prefixStr,i),type=str,default="")
            xPretask["JsonData"] = pretask_x_jsondata
            # AssertChain 处理
            pretask_x_assertType = request.form.getlist("%s-%d-assertType" % (prefixStr,i))
            pretask_x_assertValue = request.form.getlist("%s-%d-assertValue" % (prefixStr,i))
            if pretask_x_assertType and pretask_x_assertValue and \
                    len(pretask_x_assertType) == len(pretask_x_assertValue):
                xPretask["AssertChain"] = []
                for j in range(len(pretask_x_assertType)):
                    xPretask["AssertChain"].append({
                        "AssertType": parse2Int4saveTrans(pretask_x_assertType[j]),
                        "RuleValue":  parse2Int4saveTrans(pretask_x_assertValue[j])
                    })
            else:
                xPretask["AssertChain"] = None
            # SaveParamAction 处理
            pretask_x_saveType = request.form.getlist("%s-%d-saveType" % (prefixStr,i))
            pretask_x_paramName = request.form.getlist("%s-%d-paramName" % (prefixStr,i))
            pretask_x_ruleValue = request.form.getlist("%s-%d-ruleValue" % (prefixStr,i))
            if pretask_x_saveType and pretask_x_paramName and pretask_x_ruleValue and \
                    len(pretask_x_saveType) == len(pretask_x_paramName) and \
                    len(pretask_x_saveType) == len(pretask_x_ruleValue):
                xPretask["SaveParamChain"] = []
                for j in range(len(pretask_x_saveType)):
                    xPretask["SaveParamChain"].append({
                        "SaveType":  parse2Int4saveTrans(pretask_x_saveType[j]),
                        "ParamName": pretask_x_paramName[j],
                        "RuleValue": pretask_x_ruleValue[j]
                    })
            else:
                xPretask["SaveParamChain"] = None
            return xPretask

        @app.route('/resetTrans',methods=["POST"])
        @self.auth_required_if_enabled
        def resetTransation():
            with open("./jsons/main.json",mode="w") as f:
                f.write("{}")
            return jsonify({'success': True,'message': ''})

        @app.route('/saveTrans',methods=["POST"])
        @self.auth_required_if_enabled
        def saveTransation():
            if environment.runner.state not in (runners.STATE_STOPPED,runners.STATE_INIT):
                return jsonify({'success': False,'message': '当前有任务正在执行,先停止测试再尝试'})
            transObj={}
            isSession=request.form.get("isSession",type=int,default=0)
            transObj["isSession"]=isSession==1
            httpProxy=request.form.get("HttpProxy",type=str,default="")
            transObj["HttpProxy"] = httpProxy
            preTaskMark=request.form.get("PreTaskMark",type=int,default=0)
            # PreTask 处理
            if preTaskMark==0:
                transObj["PreTask"] = None
            else:
                preTask=[]
                for i in range(1,preTaskMark+1):
                    preTask.append(makeHttpItem(request,"pretask",i))
                transObj["PreTask"]=preTask
            # TestTask 处理
            testTask=[]
            TestTaskIdMark = request.form.get("TestTaskIdMark",type=str,default="1")
            TestTaskMark=request.form.get("TestTaskMark",type=str,default="0")
            if(len(TestTaskIdMark.split(","))!=len(TestTaskMark.split(",")) or len(TestTaskIdMark.split(","))<1):
                return jsonify({'success': False,'message': '测试事务的id号出现错乱,可能需要重新来!'})
            for i,ttm in enumerate(TestTaskMark.split(",")):
                taskId=parse2Int4saveTrans(TestTaskIdMark.split(",")[i])
                if taskId==0:
                    return jsonify({'success': False,'message': '测试事务的id号出现错误,可能需要重新来!'})
                xTestTask={}
                xtm=parse2Int4saveTrans(ttm)
                xTaskWeight=request.form.get("testtask-%d-taskWeight"%(taskId),type=int,default=0)
                xTestTask["TaskWeight"]=xTaskWeight
                xTaskName=request.form.get("testtask-%d-TaskName"%(taskId),type=str,default="测试事务%d"%(i+1))
                xTestTask["TaskName"] = xTaskName
                if xtm==0:
                    xTestTask["PreWork"]=None
                else:
                    preWork=[]
                    for j in range(1,xtm + 1):
                        preWork.append(makeHttpItem(request,"testtask-%d-prework"%(taskId),j))
                    xTestTask["PreWork"]=preWork
                xTestTask["TestWork"]=makeHttpItem(request,"testwork",taskId)
                testTask.append(xTestTask)
            transObj["MainTask"]=testTask
            try:
                with open("./jsons/main.json",mode="w+") as f:
                    f.write(json.dumps(transObj,indent=4))
                return jsonify({'success': True,'message': '保存成功'})
            except Exception as e:
                return jsonify({'success': False,'message': e})

        @app.route('/importTrans',methods=['Post'])
        @self.auth_required_if_enabled
        def importTrans():
            try:
                f=request.files["file"]
                f.save("./jsons/tmp.json")
                return jsonify({'success': True,'message': ''})
            except Exception as e:
                return jsonify({'success': False,'message': str(e)+"\n"+traceback.format_exc()})

        @app.route('/transaction',methods=["GET"])
        @self.auth_required_if_enabled
        def transation():
            return render_template("transaction.html")

        @app.route('/importedTrans',methods=['Get'])
        @self.auth_required_if_enabled
        def importedTrans():
            if request.args.get("from_save",type=str,default="0")=="1":
                transPath="./jsons/main.json"
            else:
                transPath="./jsons/tmp.json"
            try:
                with open(transPath,mode='rb') as f:
                    transation=json.load(f)
                    if not transation.get("PreTask"):
                        PreTaskMark=0
                    else:
                        PreTaskMark=len(transation.get("PreTask"))
                    if not transation.get("MainTask"):
                        logger.error("json中没有MainTask")
                        return render_template("transaction.html")
                    TestTaskMark=[]
                    TestTaskIdMark=[]
                    tid=0
                    for mt in transation.get("MainTask"):
                        TestTaskIdMark.append(tid+1)
                        tid+=1
                        if not mt.get("PreWork"):
                            TestTaskMark.append(0)
                        else:
                            TestTaskMark.append(len(mt.get("PreWork")))
                    transMark = {
                        "PreTaskMark":  PreTaskMark,
                        "TestTaskMark": TestTaskMark,
                        "TestTaskIdMark": TestTaskIdMark,
                        "TestTaskId":tid
                    }
                    return render_template("importedTransaction.html",transMark=transMark,transation=transation)
            except Exception as e:
                logger.error(e)
                return render_template("transaction.html")


        @app.route('/backupTrans',methods=['POST'])
        @self.auth_required_if_enabled
        def backupTrans():
            transObj = {}
            isSession = request.form.get("isSession",type=int,default=0)
            transObj["isSession"] = isSession == 1
            httpProxy = request.form.get("HttpProxy",type=str,default="")
            transObj["HttpProxy"] = httpProxy
            preTaskMark = request.form.get("PreTaskMark",type=int,default=0)
            # PreTask 处理
            if preTaskMark == 0:
                transObj["PreTask"] = None
            else:
                preTask = []
                for i in range(1,preTaskMark + 1):
                    preTask.append(makeHttpItem(request,"pretask",i))
                transObj["PreTask"] = preTask
            # TestTask 处理
            testTask = []
            TestTaskIdMark = request.form.get("TestTaskIdMark",type=str,default="1")
            TestTaskMark = request.form.get("TestTaskMark",type=str,default="0")
            if (len(TestTaskIdMark.split(",")) != len(TestTaskMark.split(",")) or len(TestTaskIdMark.split(",")) < 1):
                return render_template("transaction.html")
            for i,ttm in enumerate(TestTaskMark.split(",")):
                taskId = parse2Int4saveTrans(TestTaskIdMark.split(",")[i])
                if taskId == 0:
                    return render_template("transaction.html")
                xTestTask = {}
                xtm = parse2Int4saveTrans(ttm)
                xTaskWeight = request.form.get("testtask-%d-taskWeight" % (taskId),type=int,default=0)
                xTestTask["TaskWeight"] = xTaskWeight
                xTaskName = request.form.get("testtask-%d-TaskName" % (taskId),type=str,
                                             default="测试事务%d" % (i + 1))
                xTestTask["TaskName"] = xTaskName
                if xtm == 0:
                    xTestTask["PreWork"] = None
                else:
                    preWork = []
                    for j in range(1,xtm + 1):
                        preWork.append(makeHttpItem(request,"testtask-%d-prework" % (taskId),j))
                    xTestTask["PreWork"] = preWork
                xTestTask["TestWork"] = makeHttpItem(request,"testwork",taskId)
                testTask.append(xTestTask)
            transObj["MainTask"] = testTask
            rawData = StringIO()
            rawData.write(json.dumps(transObj,indent=4))
            rawData.seek(0)
            response = make_response(rawData.getvalue())
            rawData.close()
            file_name = "backup_{0}.json".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/json"
            response.headers["Content-disposition"] = disposition
            return response


        @app.route('/download_boomer',methods=['GET'])
        @self.auth_required_if_enabled
        def downloadBoomer():
            if os.path.isfile(os.path.join("./slaveEXE","boomerHazardServer.exe")):
                return send_from_directory("./slaveEXE","boomerHazardServer.exe",as_attachment=True)

        @app.route('/download_boomer_linux',methods=['GET'])
        @self.auth_required_if_enabled
        def downloadBoomerLinux():
            if os.path.isfile(os.path.join("./slaveEXE","boomerHazardServer")):
                return send_from_directory("./slaveEXE","boomerHazardServer",as_attachment=True)


        @app.route('/initBoomer',methods=["POST"])
        @self.auth_required_if_enabled
        def initBoomer():
            if not self.etcdt.servAddressList:
                return jsonify({'success': False,'message': '没有可用的压力机'})
            selectServAddrList = request.form.getlist("servAddr[]")
            if not selectServAddrList:
                return jsonify({'success': False,'message': '请选择压力机'})
            initBommerRequest,errMsg = makeInitBoomerRequest("jsons/main.json",self.masterHost)
            if not initBommerRequest:
                return jsonify({'success': False,'message': errMsg})
            for rpcServAddr in set(self.etcdt.servAddressList).intersection(set(selectServAddrList)):
                initTask(self,rpcServAddr,initBommerRequest)
            return jsonify({'success': True,'message': '已通知压测机初始化,请检查Workers中各压力机的最新消息'})

        @app.route('/shutdownBoomer',methods=["POST"])
        @self.auth_required_if_enabled
        def shutdownBoomer():
            if  environment.runner.state  not in (runners.STATE_STOPPED,runners.STATE_INIT):
                return jsonify({'success': False,'message': '当前有任务正在执行,先停止测试再尝试'})
            if not self.etcdt.servAddressList:
                return jsonify({'success': False,'message': '没有可用的压力机'})
            for rpcServAddr in self.etcdt.servAddressList: # 全部关闭
                shutTask(self,rpcServAddr)
            return jsonify({'success': True,'message': '已通知压测机停止,请检查Workers中各压力机的最新情况'})


        def _download_csv_suggest_file_name(suggest_filename_prefix):
            """Generate csv file download attachment filename suggestion.

            Arguments:
            suggest_filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
            """

            return f"{suggest_filename_prefix}_{time()}.csv"

        def _download_csv_response(csv_data, filename_prefix):
            """Generate csv file download response with 'csv_data'.

            Arguments:
            csv_data: CSV header and data rows.
            filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
            """

            response = make_response(csv_data)
            response.headers["Content-type"] = "text/csv"
            response.headers[
                "Content-disposition"
            ] = f"attachment;filename={_download_csv_suggest_file_name(filename_prefix)}"
            return response

        @app.route("/stats/requests/csv")
        @self.auth_required_if_enabled
        def request_stats_csv():
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.requests_csv(writer)
            return _download_csv_response(data.getvalue(),"requests")

        @app.route("/stats/requests_full_history/csv")
        @self.auth_required_if_enabled
        def request_stats_full_history_csv():
            options = self.environment.parsed_options
            if options and options.stats_history_enabled:
                return send_file(
                    os.path.abspath(self.stats_csv_writer.stats_history_file_name()),
                    mimetype="text/csv",
                    as_attachment=True,
                    attachment_filename=_download_csv_suggest_file_name("requests_full_history"),
                    add_etags=True,
                    cache_timeout=None,
                    conditional=True,
                    last_modified=None,
                )

            return make_response("Error: Server was not started with option to generate full history.",404)

        @app.route("/stats/failures/csv")
        @self.auth_required_if_enabled
        def failures_stats_csv():
            data = StringIO()
            writer = csv.writer(data)
            self.stats_csv_writer.failures_csv(writer)
            return _download_csv_response(data.getvalue(),"failures")

        @app.route('/stats/requests')
        @self.auth_required_if_enabled
        @memoize(timeout=DEFAULT_CACHE_TIME,dynamic_timeout=True)
        def request_stats():
            stats = []

            for s in chain(sort_stats(self.environment.runner.stats.entries),[environment.runner.stats.total]):
                stats.append(
                    {
                        "method":                  s.method,
                        "name":                    s.name,
                        "safe_name":               escape(s.name,quote=False),
                        "num_requests":            s.num_requests,
                        "num_failures":            s.num_failures,
                        "avg_response_time":       s.avg_response_time,
                        "min_response_time":       0 if s.min_response_time is None else proper_round(s.min_response_time),
                        "max_response_time":       proper_round(s.max_response_time),
                        "current_rps":             s.current_rps,
                        "current_fail_per_sec":    s.current_fail_per_sec,
                        "median_response_time":    s.median_response_time,
                        "ninetieth_response_time": s.get_response_time_percentile(0.9),
                        "avg_content_length":      s.avg_content_length,
                    }
                )

            errors = []
            for e in environment.runner.errors.values():
                err_dict = e.to_dict()
                err_dict["name"] = escape(err_dict["name"])
                err_dict["error"] = escape(err_dict["error"])
                errors.append(err_dict)

            # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
            # to render extremely slowly. Aggregate stats should be preserved.
            report = {"stats": stats[:500],"errors": errors[:500]}
            if len(stats) > 500:
                report["stats"] += [stats[-1]]

            if stats:
                report["total_rps"] = stats[len(stats) - 1]["current_rps"]
                report["fail_ratio"] = environment.runner.stats.total.fail_ratio
                report[
                    "current_response_time_percentile_95"] = environment.runner.stats.total.get_current_response_time_percentile(
                    0.95)
                report[
                    "current_response_time_percentile_50"] = environment.runner.stats.total.get_current_response_time_percentile(
                    0.5)

            is_distributed = isinstance(environment.runner,MasterRunner)
            if is_distributed:
                workers = []
                missingClientIds=[]
                for key,worker in environment.runner.clients.items():
                    if worker.state==runners.STATE_MISSING:
                        missingClientIds.append(key)
                        continue
                    workers.append({
                        "id":        worker.id,
                        "state":     worker.state,
                        "user_count": worker.user_count,
                        "cpu_usage": worker.cpu_usage
                    })
                # 移除missing的worker
                for missingClientId in missingClientIds:
                    del environment.runner.clients[missingClientId]
                report["workers"] = workers
                report["slaves"] = [{
                    "slave":x,
                    "clientId": (lambda t:"-" if not t or not t in [w.get("id") for w in workers] else t)(self.workedServser.get(x)),
                    "rectMsg": (lambda t: "" if not t else t)(self.recvMesg.get(x))
                } for x in self.etcdt.servAddressList ]
            # print("environment.runner.state",environment.runner.state)
            report["state"] = environment.runner.state
            report["user_count"] = environment.runner.user_count

            return jsonify(report)

        @app.route("/exceptions")
        @self.auth_required_if_enabled
        def exceptions():
            return jsonify({
                'exceptions': [
                    {
                        "count":     row["count"],
                        "msg":       row["msg"],
                        "traceback": row["traceback"],
                        "nodes":     ", ".join(row["nodes"])
                    } for row in environment.runner.exceptions.values()
                ]
            })

        @app.route("/exceptions/csv")
        @self.auth_required_if_enabled
        def exceptions_csv():
            data = StringIO()
            writer = csv.writer(data)
            writer.writerow(["Count","Message","Traceback","Nodes"])
            for exc in environment.runner.exceptions.values():
                nodes = ", ".join(exc["nodes"])
                writer.writerow([exc["count"],exc["msg"],exc["traceback"],nodes])

            response = make_response(data.getvalue())
            file_name = "exceptions_{0}.csv".format(time())
            disposition = "attachment;filename={0}".format(file_name)
            response.headers["Content-type"] = "text/csv"
            response.headers["Content-disposition"] = disposition
            return response

        # start the web server
        self.greenlet = gevent.spawn(self.start)
        self.greenlet.link_exception(greenlet_exception_handler)

        @app.route("/stats/report")
        @self.auth_required_if_enabled
        def stats_report():
            stats = self.environment.runner.stats
            if not stats or not stats.start_time or not stats.last_request_timestamp or not stats.entries:
                return  render_template(
                "report.html")

            start_ts = stats.start_time
            start_time = datetime.datetime.fromtimestamp(start_ts)
            start_time = start_time.strftime("%Y-%m-%d %H:%M:%S")

            end_ts = stats.last_request_timestamp
            end_time = datetime.datetime.fromtimestamp(end_ts)
            end_time = end_time.strftime("%Y-%m-%d %H:%M:%S")

            host = None
            if environment.host:
                host = environment.host
            elif environment.runner.user_classes:
                all_hosts = set([l.host for l in environment.runner.user_classes])
                if len(all_hosts) == 1:
                    host = list(all_hosts)[0]

            requests_statistics = list(chain(sort_stats(stats.entries),[stats.total]))
            failures_statistics = sort_stats(stats.errors)
            exceptions_statistics = []
            for exc in environment.runner.exceptions.values():
                exc["nodes"] = ", ".join(exc["nodes"])
                exceptions_statistics.append(exc)

            history = stats.history

            static_js = ""
            js_files = ["jquery-1.11.3.min.js","echarts.common.min.js","vintage.js","chart.js"]
            for js_file in js_files:
                path = os.path.join(os.path.dirname(__file__),"static",js_file)
                with open(path,encoding="utf8") as f:
                    content = f.read()
                static_js += "// " + js_file + "\n"
                static_js += content
                static_js += "\n\n\n"

            res = render_template(
                "report.html",
                int=int,
                round=round,
                requests_statistics=requests_statistics,
                failures_statistics=failures_statistics,
                exceptions_statistics=exceptions_statistics,
                start_time=start_time,
                end_time=end_time,
                host=host,
                history=history,
                static_js=static_js,
            )
            if request.args.get("download"):
                res = app.make_response(res)
                res.headers["Content-Disposition"] = "attachment;filename=report_%s.html" % time()
            return res

    def start(self):
        if self.tls_cert and self.tls_key:
            self.server = pywsgi.WSGIServer((self.host,self.port),self.app,log=None,keyfile=self.tls_key,
                                            certfile=self.tls_cert)
        else:
            self.server = pywsgi.WSGIServer((self.host,self.port),self.app,log=None)
        self.server.serve_forever()

    def stop(self):
        """
        Stop the running web server
        """
        self.server.stop()

    def auth_required_if_enabled(self,view_func):
        """
        Decorator that can be used on custom route methods that will turn on Basic Auth
        authentication if the ``--web-auth`` flag is used. Example::

            @web_ui.app.route("/my_custom_route")
            @web_ui.auth_required_if_enabled
            def my_custom_route():
                return "custom response"
        """

        @wraps(view_func)
        def wrapper(*args,**kwargs):
            if self.app.config["BASIC_AUTH_ENABLED"]:
                if self.auth.authenticate():
                    return view_func(*args,**kwargs)
                else:
                    return self.auth.challenge()
            else:
                return view_func(*args,**kwargs)

        return wrapper
Beispiel #7
0
 def is_accessible(self):
     basic_auth = BasicAuth(app)
     if not basic_auth.authenticate():
         raise AuthException('Not authenticated.')
     else:
         return True