diff --git a/Dockerfile b/Dockerfile index 55d2e73..b397b5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ COPY --from=build /wheels /wheels RUN --mount=type=cache,target=/var/cache/buildkit/pip \ pip install --find-links /wheels --no-index /wheels/$WHEEL_FILE mysqlclient -COPY bugsink_conf.py . +COPY bugsink/conf_templates/docker.py.template bugsink_conf.py RUN ["bugsink-manage", "migrate", "snappea", "--database=snappea"] diff --git a/bugsink/app_settings.py b/bugsink/app_settings.py index 0ddbbee..165fd0f 100644 --- a/bugsink/app_settings.py +++ b/bugsink/app_settings.py @@ -39,7 +39,7 @@ DEFAULTS = { # System inner workings: "DIGEST_IMMEDIATELY": True, - # MAX* below mirror the (current) values for the Sentry Relax + # MAX* below mirror the (current) values for the Sentry Relay "MAX_EVENT_SIZE": _MEBIBYTE, "MAX_EVENT_COMPRESSED_SIZE": 200 * _KIBIBYTE, # Note: this only applies to the deprecated "store" endpoint. "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE, diff --git a/bugsink/conf_templates/docker.py.template b/bugsink/conf_templates/docker.py.template index 6e6f425..1761db0 100644 --- a/bugsink/conf_templates/docker.py.template +++ b/bugsink/conf_templates/docker.py.template @@ -1,59 +1,109 @@ import os +from urllib.parse import urlparse from bugsink.settings.default import * # noqa from bugsink.settings.default import DATABASES -DEBUG = True +_KIBIBYTE = 1024 +_MEBIBYTE = 1024 * _KIBIBYTE + +DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "yes") + +# The security checks on SECRET_KEY are done as part of 'bugsink-manage check --deploy' SECRET_KEY = os.getenv("SECRET_KEY") -# Alternatively, pass the SECRET_KEY as an environment variable. (although that has security implications too!) -# i.e. those may leak in shared server setups. -# -# SECRET_KEY = os.environ["SECRET_KEY"] # use dictionary lookup rather than .getenv to ensure the variable is set. - -ALLOWED_HOSTS = ["bugsink"] +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",") # The time-zone here is the default for display purposes (when no project/user configuration is used). # https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TIME_ZONE -TIME_ZONE = 'Europe/Amsterdam' +TIME_ZONE = os.getenv("TIME_ZONE", "UTC") -# See TODO in the docs +# Our Docker image is hard-coded to run with snappea in the background; this means we hard-code (as opposed to reading +# the from the env) certain variables: TASK_ALWAYS_EAGER, WORKAHOLIC and DIGEST_IMMEDIATELY. + SNAPPEA = { - "TASK_ALWAYS_EAGER": False, - "NUM_WORKERS": 2, + "TASK_ALWAYS_EAGER": False, # hard-coded, corresponds to Docker setup + "WORKAHOLIC": True, # hard-coded, corresponds to Docker setup + + "NUM_WORKERS": int(os.getenv("SNAPPEA_NUM_WORKERS", 2)), } -DATABASES['default'] = { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'bugsink', - "USER": "root", - "PASSWORD": "bugsink", - "HOST": "mysql_container", -} +if os.getenv("DATABASE_URL"): + DATABASE_URL = os.getenv("DATABASE_URL") + parsed = urlparse(DATABASE_URL) + + if parsed.scheme != "mysql": + raise ValueError("For DATABASE_URL, only mysql is supported.") + + DATABASES['default'] = { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': parsed.path.lstrip('/'), + "USER": parsed.username, + "PASSWORD": parsed.password, + "HOST": parsed.hostname, + "PORT": parsed.port or "3306", + } + +# else: +# print("WARNING: DATABASE_URL not set; using default sqlite database, which will not persist to a fresh container.") -# EMAIL_HOST = ... -# EMAIL_HOST_USER = ... -# EMAIL_HOST_PASSWORD = ... -# EMAIL_PORT = ... -# EMAIL_USE_TLS = ... +if os.getenv("EMAIL_HOST"): + EMAIL_HOST = os.getenv("EMAIL_HOST") + EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") + EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") + EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587)) + EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "True").lower() in ("true", "1", "yes") +else: + # print("WARNING: EMAIL_HOST not set; email will not be sent") + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +SERVER_EMAIL = DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "Bugsink ") + +# constants for "create by" (user/team/project) settings +CB_ANYBODY = "CB_ANYBODY" +CB_MEMBERS = "CB_MEMBERS" +CB_ADMINS = "CB_ADMINS" +CB_NOBODY = "CB_NOBODY" -SERVER_EMAIL = DEFAULT_FROM_EMAIL = "Bugsink " BUGSINK = { - # See TODO in the docs - "DIGEST_IMMEDIATELY": False, + "DIGEST_IMMEDIATELY": False, # hard-coded, corresponds to Docker setup - # "MAX_EVENT_SIZE": _MEBIBYTE, - # "MAX_EVENT_COMPRESSED_SIZE": 200 * _KIBIBYTE, - # "MAX_ENVELOPE_SIZE": 100 * _MEBIBYTE, - # "MAX_ENVELOPE_COMPRESSED_SIZE": 20 * _MEBIBYTE, + # The URL where the Bugsink instance is hosted. This is used in the email notifications and to construct DSNs. + "BASE_URL": os.getenv("BASE_URL", "http://localhost:9000"), # no trailing slash - # "BASE_URL": "http://bugsink:9000", # no trailing slash - # "SITE_TITLE": "Bugsink", # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" + # you can customize this as e.g. "My Bugsink" or "Bugsink for My Company" + "SITE_TITLE": os.getenv("SITE_TITLE", "Bugsink"), + + # Settings for Users, Teams and Projects + "SINGLE_USER": os.getenv("SINGLE_USER", "False").lower() in ("true", "1", "yes"), + + # who can register new users. default: anybody, i.e. users can register themselves + "USER_REGISTRATION": os.getenv("USER_REGISTRATION", CB_ANYBODY), + "USER_REGISTRATION_VERIFY_EMAIL": + os.getenv("USER_REGISTRATION_VERIFY_EMAIL", "True").lower() in ("true", "1", "yes"), + "USER_REGISTRATION_VERIFY_EMAIL_EXPIRY": + int(os.getenv("USER_REGISTRATION_VERIFY_EMAIL_EXPIRY", 7 * 24 * 60 * 60)), # 7 days + + # if True, there is only one team, and all projects are in that team + "SINGLE_TEAM": os.getenv("SINGLE_TEAM", "False").lower() in ("true", "1", "yes"), + "TEAM_CREATION": os.getenv("TEAM_CREATION", CB_MEMBERS), # who can create new teams. + + # MAX* below mirror the (current) values for the Sentry Relay. + "MAX_EVENT_SIZE": int(os.getenv("MAX_EVENT_SIZE", _MEBIBYTE)), + "MAX_EVENT_COMPRESSED_SIZE": int(os.getenv("MAX_EVENT_COMPRESSED_SIZE", 200 * _KIBIBYTE)), + "MAX_ENVELOPE_SIZE": int(os.getenv("MAX_ENVELOPE_SIZE", 100 * _MEBIBYTE)), + "MAX_ENVELOPE_COMPRESSED_SIZE": int(os.getenv("MAX_ENVELOPE_COMPRESSED_SIZE", 20 * _MEBIBYTE)), + + # Bugsink-specific limits: + # The default values are 1_000 and 5_000 respectively; which corresponds to ~6% and ~2.7% of the total capacity of + # 50 requests/s (ingestion) on low-grade hardware that I measured, and with 50% of the default value for retention. + "MAX_EVENTS_PER_PROJECT_PER_5_MINUTES": int(os.getenv("MAX_EVENTS_PER_PROJECT_PER_5_MINUTES", 1_000)), + "MAX_EVENTS_PER_PROJECT_PER_HOUR": int(os.getenv("MAX_EVENTS_PER_PROJECT_PER_HOUR", 5_000)), } diff --git a/bugsink/scripts/create_conf.py b/bugsink/scripts/create_conf.py index b2ab6c9..87d73fc 100644 --- a/bugsink/scripts/create_conf.py +++ b/bugsink/scripts/create_conf.py @@ -10,7 +10,7 @@ def main(): parser = argparse.ArgumentParser(description="Create a configuration file.") parser.add_argument("--output-file", "-o", help="Output file", default="bugsink_conf.py") parser.add_argument( - "--template", help="Template to use; recommended or local", choices=["recommended", "local"]) + "--template", help="Template to use; recommended or local", choices=["recommended", "local", "docker"]) parser.add_argument("--port", help="Port to use in SITE_TITLE ; default is 9000", type=int, default=9000) parser.add_argument("--host", help="Host to use in SITE_TITLE ; default is 127.0.0.1", default="127.0.0.1")