diff --git a/py4web/server_adapters.py b/py4web/server_adapters.py index 6a1a20253..50391172d 100644 --- a/py4web/server_adapters.py +++ b/py4web/server_adapters.py @@ -11,6 +11,8 @@ wsservers_list = [] __all__ = [ + "gunicorn", + "gunicornGevent", "gevent", "geventWebSocketServer", "geventWs", # short_name @@ -22,12 +24,14 @@ # ---------------------- utils ----------------------------------------------- # export PY4WEB_LOGS=/tmp # export PY4WEB_LOGS= -def get_log_file(): +def get_log_file(out_banner=True): log_dir = os.environ.get("PY4WEB_LOGS", None) - log_file = os.path.join(log_dir, "server-py4web.log") if log_dir else None - if log_file: - print(f"log_file: {log_file}") - return log_file + if log_dir and os.path.isdir(log_dir): + log_file = os.path.join(log_dir, "server-py4web.log") + if out_banner: + print(f"log_file: {log_file}") + return log_file + return None def check_level(level): @@ -57,7 +61,7 @@ def check_level(level): ) -def logging_conf(level=logging.WARN, logger_name=__name__, test_log=False): +def logging_conf(level=logging.WARN, logger_name=__name__, fmode="w", test_log=False): log_file = get_log_file() log_to = dict() @@ -65,11 +69,15 @@ def logging_conf(level=logging.WARN, logger_name=__name__, test_log=False): if log_file: if sys.version_info >= (3, 9): log_to["filename"] = log_file - log_to["filemode"] = "w" + log_to["filemode"] = fmode log_to["encoding"] = "utf-8" else: - h = logging.FileHandler(log_file, mode="w", encoding="utf-8") - log_to.update({"handlers": [h]}) + try: + h = logging.FileHandler(log_file, mode=fmode, encoding="utf-8") + log_to.update({"handlers": [h]}) + except (LookupError, KeyError, ValueError) as ex: + print(f"{ex}, bad encoding {__file__}") + pass short_msg = "%(message)s > %(threadName)s > %(asctime)s.%(msecs)03d" # long_msg = short_msg + " > %(funcName)s > %(filename)s:%(lineno)d > %(levelname)s" @@ -79,17 +87,13 @@ def logging_conf(level=logging.WARN, logger_name=__name__, test_log=False): try: logging.basicConfig( - format=short_msg, - datefmt=time_msg, - level=check_level(level), - **log_to, + format=short_msg, datefmt=time_msg, level=check_level(level), **log_to, ) except (OSError, LookupError, KeyError, ValueError) as ex: print(f"{ex}, {__file__}") print(f"cannot open {log_file}") logging.basicConfig( - format="%(message)s", - level=check_level(level), + format="%(message)s", level=check_level(level), ) if logger_name is None: @@ -119,6 +123,145 @@ def get_workers(opts, default=10): # ---------------------- servers ----------------------------------------------- +def gunicorn(): + + from gevent import local # pip install gevent gunicorn + import threading + + # To use gevent monkey.patch_all() + # run ./py4web.py run apps -s gunicornGevent ...... + if isinstance(threading.local(), local.local): + print("gunicorn: monkey.patch_all() applied") + + class GunicornServer(ServerAdapter): + """ https://docs.gunicorn.org/en/stable/settings.html """ + + # https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/ + # ./py4web.py run apps -s gunicorn --watch=off --port=8000 --ssl_cert=cert.pem --ssl_key=key.pem -w 6 -L 20 + # ./py4web.py run apps -s gunicornGevent --watch=off --port=8000 --ssl_cert=cert.pem --ssl_key=key.pem -w 6 -L 20 + + def run(self, app_handler): + from gunicorn.app.base import BaseApplication + + config = { + "bind": f"{self.host}:{self.port}", + "workers": get_workers(self.options), + "certfile": self.options.get("certfile", None), + "keyfile": self.options.get("keyfile", None), + } + + if not self.quiet: + + level = check_level(self.options["logging_level"]) + log_file = get_log_file(out_banner=False) + + logger = logging_conf(level) + log_to = "-" if log_file is None else log_file + + config.update( + { + "loglevel": logging.getLevelName(level), + "access_log_format": '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"', + "accesslog": log_to, + "errorlog": log_to, + } + ) + + class GunicornApplication(BaseApplication): + def get_gunicorn_vars(self, env_file="gunicorn.saenv"): + result = dict() + if os.path.isfile(env_file): + try: + with open(env_file, "r") as f: + lines = f.read().splitlines() + for line in lines: + line = line.strip() + if not line or line.startswith(("#",'[')): + continue + for k in ("export ", "GUNICORN_"): + line = line.replace(k, "",1) + k, v = None, None + try: + k, v = line.split("=", 1) + except ValueError: + continue + v = v.strip() + result[k.strip().lower()] = None if v == 'None' else v + if result: + print(f"gunicorn: read {env_file}") + return result + except OSError as ex: + print(f"{ex} gunicorn: cannot read {env_file}") + for k, v in os.environ.items(): + if k.startswith("GUNICORN_") and v: + key = k.split("_", 1)[1].lower() + result[key] = v + return result + + def load_config(self): + + """ + gunicorn.saenv + + # example + # export GUNICORN_max_requests=1200 + export GUNICORN_worker_tmp_dir=/dev/shm + + # run as -s gunicornGevent + #worker_class=gevent + #worker_class=eventlet + + # run as -s gunicorn + #worker_class=sync + [hello any text] + worker_class=gthread + workers=4 + threads=8 + + """ + + + # export GUNICORN_BACKLOG=4096 + # export GUNICORN_worker_connections=100 + + # export GUNICORN_worker_class=sync + # export GUNICORN_worker_class=gthread + # export GUNICORN_worker_tmp_dir=/dev/shm + # export GUNICORN_threads=8 + # export GUNICORN_timeout=10 + # export GUNICORN_max_requests=1200 + + # + # tested with ssep4w https://github.com/ali96343/lvsio + # + # To use gevent monkey.patch_all() + # run ./py4web.py run apps -s gunicornGevent ...... + # export GUNICORN_worker_class=gevent + # export GUNICORN_worker_class=gunicorn.workers.ggevent.GeventWorker + # + # pip install gunicorn[eventlet] + # export GUNICORN_worker_class=eventlet + # + # time seq 1 5000 | xargs -I % -P 0 curl http://localhost:8000/todo &>/dev/null + + gunicorn_vars = self.get_gunicorn_vars() + + if gunicorn_vars: + config.update(gunicorn_vars) + print("gunicorn config:", config) + + for key, value in config.items(): + self.cfg.set(key, value) + + def load(self): + return app_handler + + GunicornApplication().run() + + return GunicornServer + + +gunicornGevent = gunicorn def gevent(): @@ -139,13 +282,10 @@ def gevent(): class GeventServer(ServerAdapter): def run(self, app_handler): - logger = "default" + logger = None # "default" if not self.quiet: - logger = logging_conf( - self.options["logging_level"], - "gevent", - ) + logger = logging_conf(self.options["logging_level"], "gevent",) # logger.addHandler(logging.StreamHandler()) certfile = self.options.get("certfile", None) @@ -177,6 +317,7 @@ def run(self, app_handler): def geventWebSocketServer(): from gevent import pywsgi + # from geventwebsocket.handler import WebSocketHandler # pip install gevent-websocket from gevent_ws import WebSocketHandler # pip install gevent gevent-ws @@ -193,21 +334,15 @@ def geventWebSocketServer(): class GeventWebSocketServer(ServerAdapter): def run(self, app_handler): - logger = "default" + logger = None # "default" if not self.quiet: - logger = logging_conf( - self.options["logging_level"], - "gevent-ws", - ) + logger = logging_conf(self.options["logging_level"], "gevent-ws",) certfile = self.options.get("certfile", None) ssl_args = ( - dict( - certfile=certfile, - keyfile=self.options.get("keyfile", None), - ) + dict(certfile=certfile, keyfile=self.options.get("keyfile", None),) if certfile else dict() ) @@ -235,8 +370,7 @@ def wsgirefThreadingServer(): import socket from concurrent.futures import ThreadPoolExecutor # pip install futures from socketserver import ThreadingMixIn - from wsgiref.simple_server import (WSGIRequestHandler, WSGIServer, - make_server) + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer, make_server class WSGIRefThreadingServer(ServerAdapter): def run(self, app_handler): @@ -244,10 +378,7 @@ def run(self, app_handler): self.log = None if not self.quiet: - self.log = logging_conf( - self.options["logging_level"], - "wsgiref", - ) + self.log = logging_conf(self.options["logging_level"], "wsgiref",) self_run = self # used in internal classes to access options and logger @@ -355,9 +486,7 @@ def run(self, app_handler): if not self.quiet: - logging_conf( - self.options["logging_level"], - ) + logging_conf(self.options["logging_level"],) interface = ( ( @@ -406,7 +535,8 @@ def salog(pat='SA:'): return _srv_log - dbg and salog().info(str(mess)) + caller = f" > {APP_NAME} > {sys._getframe().f_back.f_code.co_name}" + dbg and salog().info(mess + caller) log_warn=log_info log_debug=log_info