diff --git a/Dockerfile b/Dockerfile index 18b83e5..e29c45e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,7 @@ COPY src . COPY package.json ../ EXPOSE 8191 +EXPOSE 8192 # dumb-init avoids zombie chromium processes ENTRYPOINT ["/usr/bin/dumb-init", "--"] diff --git a/README.md b/README.md index 496b128..fe467ec 100644 --- a/README.md +++ b/README.md @@ -226,23 +226,51 @@ This is the same as `request.get` but it takes one more param: ## Environment variables -| Name | Default | Notes | -|-----------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| LOG_LEVEL | info | Verbosity of the logging. Use `LOG_LEVEL=debug` for more information. | -| LOG_HTML | false | Only for debugging. If `true` all HTML that passes through the proxy will be logged to the console in `debug` level. | -| CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. | -| TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. | -| HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. | -| BROWSER_TIMEOUT | 40000 | If you are experiencing errors/timeouts because your system is slow, you can try to increase this value. Remember to increase the `maxTimeout` parameter too. | -| TEST_URL | https://www.google.com | FlareSolverr makes a request on start to make sure the web browser is working. You can change that URL if it is blocked in your country. | -| PORT | 8191 | Listening port. You don't need to change this if you are running on Docker. | -| HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker. | +| Name | Default | Notes | +|--------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| LOG_LEVEL | info | Verbosity of the logging. Use `LOG_LEVEL=debug` for more information. | +| LOG_HTML | false | Only for debugging. If `true` all HTML that passes through the proxy will be logged to the console in `debug` level. | +| CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. | +| TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. | +| HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. | +| BROWSER_TIMEOUT | 40000 | If you are experiencing errors/timeouts because your system is slow, you can try to increase this value. Remember to increase the `maxTimeout` parameter too. | +| TEST_URL | https://www.google.com | FlareSolverr makes a request on start to make sure the web browser is working. You can change that URL if it is blocked in your country. | +| PORT | 8191 | Listening port. You don't need to change this if you are running on Docker. | +| HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker. | +| PROMETHEUS_ENABLED | false | Enable Prometheus exporter. See the Prometheus section below. | +| PROMETHEUS_PORT | 8192 | Listening port for Prometheus exporter. See the Prometheus section below. | Environment variables are set differently depending on the operating system. Some examples: * Docker: Take a look at the Docker section in this document. Environment variables can be set in the `docker-compose.yml` file or in the Docker CLI command. * Linux: Run `export LOG_LEVEL=debug` and then start FlareSolverr in the same shell. * Windows: Open `cmd.exe`, run `set LOG_LEVEL=debug` and then start FlareSolverr in the same shell. +## Prometheus exporter + +The Prometheus exporter for FlareSolverr is disabled by default. It can be enabled with the environment variable `PROMETHEUS_ENABLED`. If you are using Docker make sure you expose the `PROMETHEUS_PORT`. + +Example metrics: +```shell +# HELP flaresolverr_request_total Total requests with result +# TYPE flaresolverr_request_total counter +flaresolverr_request_total{domain="nowsecure.nl",result="solved"} 1.0 +# HELP flaresolverr_request_created Total requests with result +# TYPE flaresolverr_request_created gauge +flaresolverr_request_created{domain="nowsecure.nl",result="solved"} 1.690141657157109e+09 +# HELP flaresolverr_request_duration Request duration in seconds +# TYPE flaresolverr_request_duration histogram +flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="0.0"} 0.0 +flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="10.0"} 1.0 +flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="25.0"} 1.0 +flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="50.0"} 1.0 +flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="+Inf"} 1.0 +flaresolverr_request_duration_count{domain="nowsecure.nl"} 1.0 +flaresolverr_request_duration_sum{domain="nowsecure.nl"} 5.858 +# HELP flaresolverr_request_duration_created Request duration in seconds +# TYPE flaresolverr_request_duration_created gauge +flaresolverr_request_duration_created{domain="nowsecure.nl"} 1.6901416571570296e+09 +``` + ## Captcha Solvers > **Warning** diff --git a/requirements.txt b/requirements.txt index 1660ea3..1b7ce51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,10 @@ bottle==0.12.25 waitress==2.1.2 selenium==4.10.0 func-timeout==4.3.5 +prometheus-client==0.17.1 # required by undetected_chromedriver requests==2.31.0 -certifi==2023.5.7 +certifi==2023.7.22 websockets==11.0.3 # only required for linux xvfbwrapper==0.2.9 diff --git a/src/bottle_plugins/prometheus_plugin.py b/src/bottle_plugins/prometheus_plugin.py new file mode 100644 index 0000000..046bd5f --- /dev/null +++ b/src/bottle_plugins/prometheus_plugin.py @@ -0,0 +1,53 @@ +import logging +import os +import urllib.parse + +from dtos import V1ResponseBase +from metrics import start_metrics_http_server, REQUEST_COUNTER, REQUEST_DURATION + +PROMETHEUS_ENABLED = os.environ.get('PROMETHEUS_ENABLED', 'false').lower() == 'true' +PROMETHEUS_PORT = int(os.environ.get('PROMETHEUS_PORT', 8192)) + + +def setup(): + if PROMETHEUS_ENABLED: + start_metrics_http_server(PROMETHEUS_PORT) + + +def prometheus_plugin(callback): + """ + Bottle plugin to expose Prometheus metrics + http://bottlepy.org/docs/dev/plugindev.html + """ + def wrapper(*args, **kwargs): + actual_response = callback(*args, **kwargs) + + if PROMETHEUS_ENABLED: + try: + export_metrics(actual_response) + except Exception as e: + logging.warning("Error exporting metrics: " + str(e)) + + return actual_response + + def export_metrics(actual_response): + res = V1ResponseBase(actual_response) + + domain = "unknown" + if res.solution and res.solution.url: + parsed_url = urllib.parse.urlparse(res.solution.url) + domain = parsed_url.hostname + + run_time = (res.endTimestamp - res.startTimestamp) / 1000 + REQUEST_DURATION.labels(domain=domain).observe(run_time) + + result = "unknown" + if res.message == "Challenge solved!": + result = "solved" + elif res.message == "Challenge not detected!": + result = "not_detected" + elif res.message.startswith("Error"): + result = "error" + REQUEST_COUNTER.labels(domain=domain, result=result).inc() + + return wrapper diff --git a/src/flaresolverr.py b/src/flaresolverr.py index fee520e..c4d0720 100644 --- a/src/flaresolverr.py +++ b/src/flaresolverr.py @@ -8,6 +8,7 @@ from bottle import run, response, Bottle, request, ServerAdapter from bottle_plugins.error_plugin import error_plugin from bottle_plugins.logger_plugin import logger_plugin +from bottle_plugins import prometheus_plugin from dtos import V1RequestBase import flaresolverr_service import utils @@ -24,10 +25,6 @@ class JSONErrorBottle(Bottle): app = JSONErrorBottle() -# plugin order is important -app.install(logger_plugin) -app.install(error_plugin) - @app.route('/') def index(): @@ -101,6 +98,13 @@ if __name__ == "__main__": # test browser installation flaresolverr_service.test_browser_installation() + # start bootle plugins + # plugin order is important + app.install(logger_plugin) + app.install(error_plugin) + prometheus_plugin.setup() + app.install(prometheus_plugin.prometheus_plugin) + # start webserver # default server 'wsgiref' does not support concurrent requests # https://github.com/FlareSolverr/FlareSolverr/issues/680 diff --git a/src/metrics.py b/src/metrics.py new file mode 100644 index 0000000..4112dd1 --- /dev/null +++ b/src/metrics.py @@ -0,0 +1,32 @@ +import logging + +from prometheus_client import Counter, Histogram, start_http_server +import time + +REQUEST_COUNTER = Counter( + name='flaresolverr_request', + documentation='Total requests with result', + labelnames=['domain', 'result'] +) +REQUEST_DURATION = Histogram( + name='flaresolverr_request_duration', + documentation='Request duration in seconds', + labelnames=['domain'], + buckets=[0, 10, 25, 50] +) + + +def serve(port): + start_http_server(port=port) + while True: + time.sleep(600) + + +def start_metrics_http_server(prometheus_port: int): + logging.info(f"Serving Prometheus exporter on http://0.0.0.0:{prometheus_port}/metrics") + from threading import Thread + Thread( + target=serve, + kwargs=dict(port=prometheus_port), + daemon=True, + ).start()