mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
import logging
|
|
import multiprocessing
|
|
import os
|
|
import threading
|
|
|
|
from config.env import env
|
|
from uvicorn_worker import UvicornWorker
|
|
|
|
# Ensure the environment variable for Django settings is set
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.production")
|
|
|
|
# Import Django and set it up before accessing settings
|
|
import django # noqa: E402
|
|
|
|
django.setup()
|
|
|
|
from api.compliance import warm_compliance_caches # noqa: E402
|
|
from config.custom_logging import BackendLogger # noqa: E402
|
|
from config.django.production import DEBUG # noqa: E402
|
|
from config.django.production import LOGGING as DJANGO_LOGGERS # noqa: E402
|
|
|
|
BIND_ADDRESS = env("DJANGO_BIND_ADDRESS", default="127.0.0.1")
|
|
PORT = env("DJANGO_PORT", default=8080)
|
|
|
|
|
|
class ProwlerUvicornWorker(UvicornWorker):
|
|
CONFIG_KWARGS = {
|
|
# Keep-alive idle timeout. Must exceed the load balancer idle timeout.
|
|
"timeout_keep_alive": env.int("GUNICORN_KEEPALIVE", default=75),
|
|
"loop": "uvloop",
|
|
"lifespan": "off", # Django ASGIHandler doesn't handle lifespan scopes
|
|
}
|
|
|
|
|
|
# Required so SSE endpoints can keep the event loop alive while waiting for events
|
|
worker_class = env(
|
|
"DJANGO_WORKER_CLASS",
|
|
default="config.guniconf.ProwlerUvicornWorker",
|
|
)
|
|
|
|
# Server settings
|
|
bind = f"{BIND_ADDRESS}:{PORT}"
|
|
|
|
workers = env.int("DJANGO_WORKERS", default=multiprocessing.cpu_count() * 2 + 1)
|
|
reload = DEBUG
|
|
|
|
# Preload the application before forking workers in production: the app is
|
|
# imported once in the master and workers fork from it. In development, disable
|
|
# preload so the server restarts on code changes.
|
|
preload_app = not DEBUG
|
|
|
|
# Worker timeout in seconds. Increased from the default 30s to handle requests
|
|
# that may take longer, such as complex API operations.
|
|
timeout = env.int("GUNICORN_TIMEOUT", default=120)
|
|
|
|
# Logging
|
|
logconfig_dict = DJANGO_LOGGERS
|
|
gunicorn_logger = logging.getLogger(BackendLogger.GUNICORN)
|
|
|
|
|
|
# Hooks
|
|
def on_starting(_):
|
|
gunicorn_logger.info(f"Starting gunicorn server with {workers} workers")
|
|
if reload:
|
|
gunicorn_logger.warning("Reload settings enabled (dev mode)")
|
|
|
|
|
|
def on_reload(_):
|
|
gunicorn_logger.warning("Gunicorn server has reloaded")
|
|
|
|
|
|
def when_ready(_):
|
|
gunicorn_logger.info("Gunicorn server is ready")
|
|
|
|
|
|
def _warm_compliance_caches_in_background():
|
|
"""Warm compliance caches off the request path and log the outcome."""
|
|
failed = warm_compliance_caches()
|
|
if failed:
|
|
gunicorn_logger.warning("Compliance caches warmed (skipped: %s)", failed)
|
|
else:
|
|
gunicorn_logger.info("Compliance caches warmed")
|
|
|
|
|
|
def post_fork(_server, worker):
|
|
"""Re-initialize attack-paths drivers and warm compliance caches per worker.
|
|
|
|
Neo4j / Neptune drivers spawn background IO threads that do not survive
|
|
``fork()``. When the gunicorn master runs with ``preload_app=True``, the
|
|
child inherits driver objects whose pool references dead threads and
|
|
hangs on the first ``pool.acquire`` call until the watchdog kills the
|
|
worker. Re-initializing per worker guarantees each child owns its own
|
|
live threads. See GUNICORN_WORKER_TIMEOUTS_ANALYSIS.md for detail.
|
|
|
|
Compliance caches are then warmed in a background thread so the worker
|
|
becomes ready immediately. A request for a not-yet-warmed provider lazily
|
|
loads just that provider, which stays well under the worker timeout.
|
|
"""
|
|
from api.attack_paths import database as graph_database
|
|
|
|
try:
|
|
graph_database.close_driver()
|
|
except Exception: # pragma: no cover - best-effort cleanup
|
|
gunicorn_logger.debug(
|
|
"Failed to close inherited Neo4j driver in post_fork for worker pid=%s",
|
|
worker.pid,
|
|
exc_info=True,
|
|
)
|
|
graph_database.init_driver()
|
|
gunicorn_logger.info(f"Attack-paths drivers initialized for worker {worker.pid}")
|
|
|
|
threading.Thread(
|
|
target=_warm_compliance_caches_in_background,
|
|
name="warm-compliance-caches",
|
|
daemon=True,
|
|
).start()
|