settings.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. """
  2. Django settings for GlitchTip project.
  3. For more information on this file, see
  4. https://docs.djangoproject.com/en/dev/topics/settings/
  5. For the full list of settings and their values, see
  6. https://docs.djangoproject.com/en/dev/ref/settings/
  7. """
  8. import logging
  9. import os
  10. import sys
  11. import warnings
  12. from datetime import timedelta
  13. import environ
  14. import sentry_sdk
  15. from celery.schedules import crontab
  16. from corsheaders.defaults import default_headers
  17. from django.conf import global_settings
  18. from django.core.exceptions import ImproperlyConfigured
  19. from django.http import UnreadablePostError
  20. from sentry_sdk.integrations.django import DjangoIntegration
  21. env = environ.FileAwareEnv(
  22. ALLOWED_HOSTS=(list, ["*"]),
  23. DEFAULT_FILE_STORAGE=(str, global_settings.STORAGES["default"]["BACKEND"]),
  24. AWS_ACCESS_KEY_ID=(str, None),
  25. AWS_SECRET_ACCESS_KEY=(str, None),
  26. AWS_STORAGE_BUCKET_NAME=(str, None),
  27. AWS_S3_ENDPOINT_URL=(str, None),
  28. AWS_LOCATION=(str, ""),
  29. AZURE_ACCOUNT_NAME=(str, None),
  30. AZURE_ACCOUNT_KEY=(str, None),
  31. AZURE_CONTAINER=(str, None),
  32. AZURE_URL_EXPIRATION_SECS=(int, None),
  33. IS_LOAD_TEST=(bool, False),
  34. GS_BUCKET_NAME=(str, None),
  35. GS_PROJECT_ID=(str, None),
  36. DEBUG=(bool, False),
  37. DEBUG_TOOLBAR=(bool, False),
  38. STATIC_URL=(str, "/"),
  39. ENABLE_OBSERVABILITY_API=(bool, False),
  40. )
  41. path = environ.Path()
  42. # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
  43. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  44. # Quick-start development settings - unsuitable for production
  45. # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/
  46. # SECURITY WARNING: keep the secret key used in production secret!
  47. SECRET_KEY = env.str("SECRET_KEY", "change_me")
  48. # SECURITY WARNING: don't run with debug turned on in production!
  49. DEBUG = env("DEBUG")
  50. # Enable only for running end to end testing. Debug must be True to use.
  51. ENABLE_TEST_API = env.bool("ENABLE_TEST_API", False)
  52. if DEBUG is False:
  53. ENABLE_TEST_API = False
  54. if DEBUG and ENABLE_TEST_API:
  55. ACCOUNT_RATE_LIMITS = False # Disable for e2e tests
  56. ALLOWED_HOSTS = env("ALLOWED_HOSTS")
  57. # Necessary for kubernetes health checks
  58. POD_IP = env.str("POD_IP", default=None)
  59. if POD_IP:
  60. ALLOWED_HOSTS.append(POD_IP)
  61. ENVIRONMENT = env.str("ENVIRONMENT", None)
  62. GLITCHTIP_VERSION = env.str("GLITCHTIP_VERSION", "0.0.0-unknown")
  63. # Used in email and DSN generation. Set to full domain such as https://glitchtip.example.com
  64. default_url = env.str(
  65. "APP_URL", env.str("GLITCHTIP_DOMAIN", "http://localhost:8000")
  66. ) # DigitalOcean App Platform uses APP_URL
  67. GLITCHTIP_URL = env.url("GLITCHTIP_URL", default_url)
  68. if GLITCHTIP_URL.scheme not in ["http", "https"]:
  69. raise ImproperlyConfigured("GLITCHTIP_DOMAIN must start with http or https")
  70. # Is running unit test
  71. TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
  72. DATA_UPLOAD_MAX_MEMORY_SIZE = 4294967295 # TMP REMOVE THIS
  73. DATA_UPLOAD_MAX_NUMBER_FIELDS = env.int(
  74. "DATA_UPLOAD_MAX_NUMBER_FIELDS",
  75. default=global_settings.DATA_UPLOAD_MAX_NUMBER_FIELDS,
  76. )
  77. # Limits size (in bytes) of uncompressed event payloads. Mitigates DOS risk.
  78. GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE = env.int(
  79. "GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE", global_settings.DATA_UPLOAD_MAX_MEMORY_SIZE
  80. )
  81. # Events and associated data older than this will be deleted from the database
  82. GLITCHTIP_MAX_EVENT_LIFE_DAYS = env.int("GLITCHTIP_MAX_EVENT_LIFE_DAYS", default=90)
  83. GLITCHTIP_MAX_UPTIME_CHECK_LIFE_DAYS = env.int(
  84. "GLITCHTIP_MAX_UPTIME_CHECK_LIFE_DAYS", default=GLITCHTIP_MAX_EVENT_LIFE_DAYS
  85. )
  86. GLITCHTIP_MAX_TRANSACTION_EVENT_LIFE_DAYS = env.int(
  87. "GLITCHTIP_MAX_TRANSACTION_EVENT_LIFE_DAYS", default=GLITCHTIP_MAX_EVENT_LIFE_DAYS
  88. )
  89. GLITCHTIP_MAX_FILE_LIFE_DAYS = env.int(
  90. "GLITCHTIP_MAX_EVENT_LIFE_DAYS", default=GLITCHTIP_MAX_EVENT_LIFE_DAYS
  91. )
  92. # Check if a throttle is needed 1 out of every 5000 event requests
  93. GLITCHTIP_THROTTLE_CHECK_INTERVAL = env.int("GLITCHTIP_THROTTLE_CHECK_INTERVAL", 5000)
  94. # Freezes acceptance of new events, for use during db maintenance
  95. MAINTENANCE_EVENT_FREEZE = env.bool("MAINTENANCE_EVENT_FREEZE", False)
  96. # For development purposes only, prints out inbound event store json
  97. EVENT_STORE_DEBUG = env.bool("EVENT_STORE_DEBUG", False)
  98. STATIC_URL = "static/"
  99. # Base HREF, such as example.com/glitchtip/ where BASE_PATH would be "/glitchtip"
  100. if "BASE_PATH" in os.environ or "FORCE_SCRIPT_NAME" in os.environ:
  101. FORCE_SCRIPT_NAME = env.str("BASE_PATH", env.str("FORCE_SCRIPT_NAME", ""))
  102. # GlitchTip can track GlitchTip's own errors.
  103. # If enabling this, use a different server to avoid infinite loops.
  104. def before_send(event, hint):
  105. """Don't log useless, inactionable errors in Sentry."""
  106. if "log_record" in hint:
  107. if hint["log_record"].name == "django.security.DisallowedHost":
  108. return None
  109. if "exc_info" in hint:
  110. _, exc_value, _ = hint["exc_info"]
  111. if isinstance(exc_value, UnreadablePostError):
  112. return None
  113. return event
  114. SENTRY_DSN = env.str("SENTRY_DSN", None)
  115. # Optionally allow a different DSN for the frontend
  116. SENTRY_FRONTEND_DSN = env.str("SENTRY_FRONTEND_DSN", SENTRY_DSN)
  117. # Set sample_rate to 1.0 to capture 100%.
  118. SENTRY_SAMPLE_RATE = env.float("SENTRY_SAMPLE_RATE", 1.0)
  119. # Set traces_sample_rate to 1.0 to capture 100%. Recommended to keep this value low.
  120. SENTRY_TRACES_SAMPLE_RATE = env.float("SENTRY_TRACES_SAMPLE_RATE", 0.01)
  121. # Ignore whitenoise served static routes
  122. def traces_sampler(sampling_context):
  123. if (
  124. sampling_context.get("wsgi_environ", {})
  125. .get("PATH_INFO", "")
  126. .startswith(STATIC_URL)
  127. ):
  128. return 0.0
  129. return SENTRY_TRACES_SAMPLE_RATE
  130. if SENTRY_DSN:
  131. release = "glitchtip@" + GLITCHTIP_VERSION if GLITCHTIP_VERSION else None
  132. sentry_sdk.init(
  133. dsn=SENTRY_DSN,
  134. integrations=[DjangoIntegration()],
  135. before_send=before_send,
  136. release=release,
  137. environment=ENVIRONMENT,
  138. auto_session_tracking=False,
  139. send_client_reports=False,
  140. sample_rate=SENTRY_SAMPLE_RATE,
  141. traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
  142. traces_sampler=traces_sampler,
  143. )
  144. def show_toolbar(request):
  145. return env("DEBUG_TOOLBAR")
  146. DEBUG_TOOLBAR = env("DEBUG_TOOLBAR")
  147. DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar}
  148. DEBUG_TOOLBAR_PANELS = [
  149. "debug_toolbar.panels.versions.VersionsPanel",
  150. "debug_toolbar.panels.timer.TimerPanel",
  151. "debug_toolbar.panels.settings.SettingsPanel",
  152. "debug_toolbar.panels.headers.HeadersPanel",
  153. "debug_toolbar.panels.request.RequestPanel",
  154. "debug_toolbar.panels.sql.SQLPanel",
  155. # "debug_toolbar.panels.history.HistoryPanel",
  156. # "debug_toolbar.panels.profiling.ProfilingPanel",
  157. ]
  158. # Application definition
  159. # Conditionally load to workaround unnecessary memory usage in celery/beat
  160. WEB_INSTALLED_APPS = [
  161. "django.contrib.admin",
  162. "django.contrib.messages",
  163. "django.contrib.staticfiles",
  164. "ninja",
  165. ]
  166. INSTALLED_APPS = [
  167. "django.contrib.auth",
  168. "django.contrib.contenttypes",
  169. "django.contrib.humanize",
  170. "psqlextra",
  171. "django_prometheus",
  172. "allauth",
  173. "allauth.account",
  174. "allauth.headless",
  175. "allauth.mfa",
  176. "allauth.socialaccount",
  177. "allauth.socialaccount.providers.digitalocean",
  178. "allauth.socialaccount.providers.gitea",
  179. "allauth.socialaccount.providers.github",
  180. "allauth.socialaccount.providers.gitlab",
  181. "allauth.socialaccount.providers.google",
  182. "allauth.socialaccount.providers.microsoft",
  183. "allauth.socialaccount.providers.nextcloud",
  184. "allauth.socialaccount.providers.openid_connect",
  185. "allauth.socialaccount.providers.okta",
  186. "anymail",
  187. "django_rest_mfa",
  188. "corsheaders",
  189. "django_extensions",
  190. ]
  191. if DEBUG_TOOLBAR:
  192. INSTALLED_APPS.append("debug_toolbar")
  193. INSTALLED_APPS += [
  194. "storages",
  195. "glitchtip",
  196. "apps.alerts",
  197. "apps.environments",
  198. "apps.organizations_ext",
  199. "events",
  200. "issues",
  201. "apps.users",
  202. "apps.importer",
  203. "apps.uptime",
  204. "apps.performance",
  205. "apps.projects",
  206. "apps.teams",
  207. "apps.releases",
  208. "apps.stripe",
  209. "apps.sourcecode",
  210. "apps.difs",
  211. "apps.api_tokens",
  212. "apps.files",
  213. "apps.issue_events",
  214. "apps.event_ingest",
  215. "import_export", # Contains import management command, keep under apps.importer
  216. ]
  217. IS_CELERY = env.bool("IS_CELERY", False)
  218. if not IS_CELERY:
  219. INSTALLED_APPS = WEB_INSTALLED_APPS + INSTALLED_APPS
  220. # Ensure no one uses runsslserver in production
  221. if SECRET_KEY == "change_me" and DEBUG is True:
  222. INSTALLED_APPS += ["sslserver"]
  223. ENABLE_OBSERVABILITY_API = env("ENABLE_OBSERVABILITY_API")
  224. # Workaround https://github.com/korfuri/django-prometheus/issues/34
  225. PROMETHEUS_EXPORT_MIGRATIONS = False
  226. # https://github.com/korfuri/django-prometheus/blob/master/documentation/exports.md#exporting-metrics-in-a-wsgi-application-with-multiple-processes-per-process
  227. if start_port := env.int("METRICS_START_PORT", None):
  228. PROMETHEUS_METRICS_EXPORT_PORT_RANGE = range(
  229. start_port, start_port + env.int("UWSGI_WORKERS", 1)
  230. )
  231. MIDDLEWARE = [
  232. "django.middleware.security.SecurityMiddleware",
  233. "django.contrib.sessions.middleware.SessionMiddleware",
  234. "corsheaders.middleware.CorsMiddleware",
  235. "csp.middleware.CSPMiddleware",
  236. "django.middleware.clickjacking.XFrameOptionsMiddleware",
  237. "whitenoise.middleware.WhiteNoiseMiddleware",
  238. ]
  239. if DEBUG_TOOLBAR:
  240. MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
  241. MIDDLEWARE += [
  242. "django.middleware.common.CommonMiddleware",
  243. "django.middleware.csrf.CsrfViewMiddleware",
  244. "django.contrib.auth.middleware.AuthenticationMiddleware",
  245. "django.contrib.messages.middleware.MessageMiddleware",
  246. "django.middleware.clickjacking.XFrameOptionsMiddleware",
  247. "sentry.middleware.proxy.DecompressBodyMiddleware",
  248. "django.middleware.locale.LocaleMiddleware",
  249. "allauth.account.middleware.AccountMiddleware",
  250. ]
  251. if ENABLE_OBSERVABILITY_API:
  252. MIDDLEWARE.insert(0, "django_prometheus.middleware.PrometheusBeforeMiddleware")
  253. MIDDLEWARE.append("django_prometheus.middleware.PrometheusAfterMiddleware")
  254. ROOT_URLCONF = "glitchtip.urls"
  255. TEMPLATES = [
  256. {
  257. "BACKEND": "django.template.backends.django.DjangoTemplates",
  258. "DIRS": [path("dist"), path("templates")],
  259. "APP_DIRS": True,
  260. "OPTIONS": {
  261. "context_processors": [
  262. "django.template.context_processors.debug",
  263. "django.template.context_processors.request",
  264. "django.contrib.auth.context_processors.auth",
  265. "django.contrib.messages.context_processors.messages",
  266. ],
  267. },
  268. },
  269. ]
  270. WSGI_APPLICATION = "glitchtip.wsgi.application"
  271. CORS_ORIGIN_ALLOW_ALL = env.bool("CORS_ORIGIN_ALLOW_ALL", True)
  272. CORS_ORIGIN_WHITELIST = env.tuple("CORS_ORIGIN_WHITELIST", str, default=())
  273. CORS_ALLOW_HEADERS = list(default_headers) + [
  274. "x-sentry-auth",
  275. "baggage",
  276. "sentry-trace",
  277. ]
  278. BILLING_ENABLED = False
  279. STRIPE_PUBLIC_KEY = env.str("STRIPE_PUBLIC_KEY", None)
  280. STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY", None)
  281. STRIPE_WEBHOOK_SECRET = env.str("STRIPE_WEBHOOK_SECRET", None)
  282. if STRIPE_PUBLIC_KEY and STRIPE_SECRET_KEY:
  283. BILLING_ENABLED = True
  284. if env.str("STRIPE_TEST_PUBLIC_KEY", None) or env.str("STRIPE_LIVE_PUBLIC_KEY", None):
  285. BILLING_ENABLED = True
  286. # Set to chatwoot website token to enable live help widget. Assumes app.chatwoot.com.
  287. CHATWOOT_WEBSITE_TOKEN = env.str("CHATWOOT_WEBSITE_TOKEN", None)
  288. CHATWOOT_IDENTITY_TOKEN = env.str("CHATWOOT_IDENTITY_TOKEN", None)
  289. CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", str, [])
  290. SECURE_BROWSER_XSS_FILTER = True
  291. CSP_DEFAULT_SRC = env.list("CSP_DEFAULT_SRC", str, ["'self'"])
  292. # https://github.com/swimlane/ngx-charts/issues/1937
  293. CSP_STYLE_SRC = env.list("CSP_STYLE_SRC", str, ["'self'", "'unsafe-inline'"])
  294. # Use this once unsafe-inline is removed (blame is ngx-charts)
  295. # if "CSP_STYLE_SRC" in os.environ:
  296. # CSP_STYLE_SRC = env.list("CSP_STYLE_SRC", str)
  297. if "CSP_STYLE_SRC_ELEM" in os.environ:
  298. CSP_STYLE_SRC_ELEM = env.list("CSP_STYLE_SRC_ELEM", str)
  299. CSP_FONT_SRC = env.list("CSP_FONT_SRC", str, ["'self'", "data:"])
  300. if "CSP_WORKER_SRC" in os.environ:
  301. CSP_WORKER_SRC = env.list("CSP_WORKER_SRC", str)
  302. # Enable Chatwoot only when configured
  303. default_connect_src = ["'self'", "https://*.glitchtip.com"]
  304. if CHATWOOT_WEBSITE_TOKEN:
  305. default_connect_src.append("https://app.chatwoot.com")
  306. CSP_CONNECT_SRC = env.list("CSP_CONNECT_SRC", str, default_connect_src)
  307. # Enable stripe by default only when configured
  308. stripe_domain = "https://js.stripe.com"
  309. default_script_src = [
  310. "'self'",
  311. "https://*.glitchtip.com",
  312. "'sha256-iRcDQ27XiXX4k+jbJ8nGeQFBnBOjmII7FdMlixb6QE4='", # Theme picker inline JS
  313. ]
  314. default_frame_src = ["'self'"]
  315. if BILLING_ENABLED:
  316. default_script_src.append(stripe_domain)
  317. default_frame_src.append(stripe_domain)
  318. CSP_SCRIPT_SRC = env.list("CSP_SCRIPT_SRC", str, default_script_src)
  319. CSP_IMG_SRC = env.list("CSP_IMG_SRC", str, ["'self'"])
  320. CSP_FRAME_SRC = env.list("CSP_FRAME_SRC", str, default_frame_src)
  321. # Consider tracking CSP reports with GlitchTip itself
  322. CSP_INCLUDE_NONCE_IN = ["default-src", "script-src"]
  323. CSP_REPORT_URI = env.tuple("CSP_REPORT_URI", str, None)
  324. CSP_REPORT_ONLY = env.bool("CSP_REPORT_ONLY", False)
  325. SECURE_HSTS_SECONDS = env.int("SECURE_HSTS_SECONDS", 0)
  326. SECURE_HSTS_PRELOAD = env.bool("SECURE_HSTS_PRELOAD", False)
  327. SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("SECURE_HSTS_INCLUDE_SUBDOMAINS", False)
  328. SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", False)
  329. SESSION_COOKIE_SAMESITE = env.str("SESSION_COOKIE_SAMESITE", "Lax")
  330. DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", "webmaster@localhost")
  331. ANYMAIL_SETTINGS = [
  332. "MAILGUN_API_KEY",
  333. "MAILGUN_SENDER_DOMAIN",
  334. "MAILGUN_API_URL",
  335. "MAILGUN_WEBHOOK_SIGNING_KEY",
  336. "SENDGRID_API_KEY",
  337. "SENDGRID_API_URL",
  338. "POSTMARK_SERVER_TOKEN",
  339. "POSTMARK_API_URL",
  340. "MANDRILL_API_KEY",
  341. "MANDRILL_WEBHOOK_KEY",
  342. "MANDRILL_WEBHOOK_URL",
  343. "MANDRILL_API_URL",
  344. "SENDINBLUE_API_KEY",
  345. "SENDINBLUE_API_URL",
  346. "MAILJET_API_KEY",
  347. "MAILJET_SECRET_KEY",
  348. "MAILJET_API_URL",
  349. "POSTAL_API_KEY",
  350. "POSTAL_API_URL",
  351. "POSTAL_WEBHOOK_KEY",
  352. "SPARKPOST_API_KEY",
  353. "SPARKPOST_API_URL",
  354. "SPARKPOST_TRACK_INITIAL_OPEN_AS_OPENED",
  355. ]
  356. ANYMAIL = {
  357. anymail_var: env.str(anymail_var)
  358. for anymail_var in ANYMAIL_SETTINGS
  359. if anymail_var in os.environ
  360. }
  361. ACCOUNT_EMAIL_SUBJECT_PREFIX = env.str("ACCOUNT_EMAIL_SUBJECT_PREFIX", "")
  362. # Database
  363. # https://docs.djangoproject.com/en/dev/ref/settings/#databases
  364. DATABASES = {
  365. "default": env.db(default="postgres://postgres:postgres@postgres:5432/postgres")
  366. }
  367. # Support setting DATABASES in parts in order to get values from the postgresql helm chart
  368. DATABASE_HOST = env.str("DATABASE_HOST", None)
  369. DATABASE_PASSWORD = env.str("DATABASE_PASSWORD", None)
  370. if DATABASE_HOST and DATABASE_PASSWORD:
  371. DATABASES["default"] = {
  372. "NAME": env.str("DATABASE_NAME", "postgres"),
  373. "USER": env.str("DATABASE_USER", "postgres"),
  374. "PASSWORD": DATABASE_PASSWORD,
  375. "HOST": DATABASE_HOST,
  376. "PORT": env.str("DATABASE_PORT", "5432"),
  377. "CONN_MAX_AGE": env.int("DATABASE_CONN_MAX_AGE", 0),
  378. "CONN_HEALTH_CHECKS": env.bool("DATABASE_CONN_HEALTH_CHECKS", False),
  379. }
  380. DATABASES["default"]["ENGINE"] = "psqlextra.backend"
  381. if not DATABASES["default"].get("OPTIONS"):
  382. DATABASES["default"]["OPTIONS"] = {}
  383. # Enable pool by default, if there is no conn_max_age
  384. if DATABASES["default"].get("CONN_MAX_AGE", 0) == 0:
  385. if env.bool("DATABASE_POOL", True):
  386. pool_options = {}
  387. min_size = env.int("DATABASE_POOL_MIN_SIZE", 2)
  388. max_size = env.int("DATABASE_POOL_MAX_SIZE", 6)
  389. if min_size:
  390. pool_options["min_size"] = min_size
  391. if max_size:
  392. pool_options["max_size"] = max_size
  393. if pool_options:
  394. DATABASES["default"]["OPTIONS"]["pool"] = pool_options
  395. else:
  396. DATABASES["default"]["OPTIONS"]["pool"] = True
  397. PSQLEXTRA_PARTITIONING_MANAGER = "glitchtip.partitioning.manager"
  398. DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
  399. # We need to support both url and broken out host to support helm redis chart
  400. REDIS_HOST = env.str("REDIS_HOST", None)
  401. if REDIS_HOST:
  402. REDIS_PORT = env.str("REDIS_PORT", "6379")
  403. REDIS_DATABASE = env.str("REDIS_DATABASE", "0")
  404. REDIS_PASSWORD = env.str("REDIS_PASSWORD", None)
  405. if REDIS_PASSWORD:
  406. REDIS_URL = (
  407. f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DATABASE}"
  408. )
  409. else:
  410. REDIS_URL = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DATABASE}"
  411. else:
  412. REDIS_URL = env.str("REDIS_URL", "redis://redis:6379/0")
  413. REDIS_RETRY = env.bool("REDIS_RETRY", True)
  414. REDIS_MAX_CONNECTIONS = env.int("REDIS_MAX_CONNECTIONS", 100)
  415. CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", REDIS_URL)
  416. CELERY_BROKER_TRANSPORT_OPTIONS = {
  417. "fanout_prefix": True,
  418. "fanout_patterns": True,
  419. "retry_on_timeout": REDIS_RETRY,
  420. "max_connections": REDIS_MAX_CONNECTIONS,
  421. }
  422. CELERY_REDIS_RETRY_ON_TIMEOUT = REDIS_RETRY
  423. CELERY_REDIS_MAX_CONNECTIONS = REDIS_MAX_CONNECTIONS
  424. if CELERY_BROKER_URL.startswith("sentinel"):
  425. CELERY_BROKER_TRANSPORT_OPTIONS["master_name"] = env.str(
  426. "CELERY_BROKER_MASTER_NAME", "mymaster"
  427. )
  428. IS_LOAD_TEST = env("IS_LOAD_TEST")
  429. # GlitchTip doesn't require a celery result backend
  430. if IS_LOAD_TEST:
  431. CELERY_RESULT_BACKEND = REDIS_URL
  432. if socket_timeout := env.int("CELERY_BROKER_SOCKET_TIMEOUT", None):
  433. CELERY_BROKER_TRANSPORT_OPTIONS["socket_timeout"] = socket_timeout
  434. if broker_sentinel_password := env.str("CELERY_BROKER_SENTINEL_KWARGS_PASSWORD", None):
  435. CELERY_BROKER_TRANSPORT_OPTIONS["sentinel_kwargs"] = {
  436. "password": broker_sentinel_password
  437. }
  438. # Time in seconds to debounce some frequently run tasks
  439. TASK_DEBOUNCE_DELAY = env.int("TASK_DEBOUNCE_DELAY", 30)
  440. UPTIME_CHECK_INTERVAL = 10
  441. ALERT_NOTIFICATION_INTERVAL = env.int("ALERT_NOTIFICATION_INTERVAL", 60)
  442. CELERY_BEAT_SCHEDULE = {
  443. "send-alert-notifications": {
  444. "task": "apps.alerts.tasks.process_event_alerts",
  445. "schedule": ALERT_NOTIFICATION_INTERVAL,
  446. },
  447. "perform-maintenance": {
  448. "task": "glitchtip.tasks.perform_maintenance",
  449. "schedule": crontab(hour=5, minute=0),
  450. },
  451. "uptime-dispatch-checks": {
  452. "task": "apps.uptime.tasks.dispatch_checks",
  453. "schedule": UPTIME_CHECK_INTERVAL,
  454. },
  455. }
  456. # Maximum number of issues send in a single alert payload
  457. MAX_ISSUES_PER_ALERT = env.int("MAX_ISSUES_PER_ALERT", 3)
  458. if os.environ.get("CACHE_URL"):
  459. CACHES = {
  460. "default": env.cache(),
  461. }
  462. else: # Default to REDIS when unset
  463. CACHES = {
  464. "default": {
  465. "BACKEND": "django_redis.cache.RedisCache",
  466. "LOCATION": REDIS_URL,
  467. "PARSER_CLASS": "redis.connection.HiredisParser",
  468. "OPTIONS": {
  469. "CONNECTION_POOL_KWARGS": {
  470. "retry_on_timeout": REDIS_RETRY,
  471. "max_connections": REDIS_MAX_CONNECTIONS,
  472. }
  473. },
  474. }
  475. }
  476. if cache_sentinel_url := env.str("CACHE_SENTINEL_URL", None):
  477. try:
  478. cache_sentinel_host, cache_sentinel_port = cache_sentinel_url.split(":")
  479. SENTINELS = [(cache_sentinel_host, int(cache_sentinel_port))]
  480. except ValueError as err:
  481. raise ImproperlyConfigured(
  482. "Invalid cache redis sentinel url, format is host:port"
  483. ) from err
  484. DJANGO_REDIS_CONNECTION_FACTORY = "django_redis.pool.SentinelConnectionFactory"
  485. CACHES["default"]["OPTIONS"]["SENTINELS"] = SENTINELS
  486. if cache_sentinel_password := env.str("CACHE_SENTINEL_PASSWORD", None):
  487. CACHES["default"]["OPTIONS"]["SENTINEL_KWARGS"] = {
  488. "password": cache_sentinel_password
  489. }
  490. SESSION_ENGINE = "django.contrib.sessions.backends.cache"
  491. SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE", global_settings.SESSION_COOKIE_AGE)
  492. # Password validation
  493. # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
  494. AUTH_PASSWORD_VALIDATORS = [
  495. {
  496. "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
  497. },
  498. {
  499. "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
  500. },
  501. {
  502. "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
  503. },
  504. {
  505. "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
  506. },
  507. ]
  508. # Internationalization
  509. # https://docs.djangoproject.com/en/dev/topics/i18n/
  510. LANGUAGE_CODE = "en-us"
  511. TIME_ZONE = "UTC"
  512. USE_I18N = True
  513. USE_TZ = True
  514. STORAGES = {
  515. "default": {
  516. "BACKEND": env("DEFAULT_FILE_STORAGE"),
  517. },
  518. "staticfiles": {
  519. "BACKEND": env.str(
  520. "STATICFILES_STORAGE",
  521. "whitenoise.storage.CompressedManifestStaticFilesStorage",
  522. )
  523. },
  524. }
  525. AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
  526. AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
  527. AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
  528. AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL")
  529. AWS_LOCATION = env("AWS_LOCATION")
  530. AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
  531. AZURE_ACCOUNT_KEY = env("AZURE_ACCOUNT_KEY")
  532. AZURE_CONTAINER = env("AZURE_CONTAINER")
  533. AZURE_URL_EXPIRATION_SECS = env("AZURE_URL_EXPIRATION_SECS")
  534. GS_BUCKET_NAME = env("GS_BUCKET_NAME")
  535. GS_PROJECT_ID = env("GS_PROJECT_ID")
  536. if AWS_S3_ENDPOINT_URL:
  537. MEDIA_URL = env.str(
  538. "MEDIA_URL", "https://%s/%s/" % (AWS_S3_ENDPOINT_URL, AWS_LOCATION)
  539. )
  540. STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
  541. else:
  542. MEDIA_URL = "media/"
  543. MEDIA_ROOT = env.str("MEDIA_ROOT", "")
  544. STATICFILES_DIRS = [
  545. "assets",
  546. "dist",
  547. ]
  548. STATIC_ROOT = path("static/")
  549. EMAIL_BACKEND = env.str(
  550. "EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
  551. )
  552. if os.getenv("EMAIL_HOST_USER"):
  553. EMAIL_HOST_USER = env.str("EMAIL_HOST_USER")
  554. if os.getenv("EMAIL_HOST_PASSWORD"):
  555. EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
  556. if os.getenv("EMAIL_HOST"):
  557. EMAIL_HOST = env.str("EMAIL_HOST")
  558. if os.getenv("EMAIL_PORT"):
  559. EMAIL_PORT = env.str("EMAIL_PORT")
  560. if os.getenv("EMAIL_USE_TLS"):
  561. EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS")
  562. if os.getenv("EMAIL_USE_SSL"):
  563. EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL")
  564. if os.getenv("EMAIL_TIMEOUT"):
  565. EMAIL_TIMEOUT = env.int("EMAIL_TIMEOUT")
  566. if os.getenv("EMAIL_FILE_PATH"):
  567. EMAIL_FILE_PATH = env.str("EMAIL_FILE_PATH")
  568. if os.getenv(
  569. "EMAIL_URL"
  570. ): # Careful, this will override most EMAIL_*** settings. Set them all individually, or use EMAIL_URL to set them all at once, but don't do both.
  571. EMAIL_CONFIG = env.email_url("EMAIL_URL")
  572. vars().update(EMAIL_CONFIG)
  573. AUTH_USER_MODEL = "users.User"
  574. ACCOUNT_ADAPTER = "glitchtip.adapters.CustomDefaultAccountAdapter"
  575. ACCOUNT_LOGIN_METHODS = {"email"}
  576. ACCOUNT_EMAIL_REQUIRED = True
  577. ACCOUNT_USERNAME_REQUIRED = False
  578. ACCOUNT_USER_MODEL_USERNAME_FIELD = None
  579. ACCOUNT_REAUTHENTICATION_TIMEOUT = SESSION_COOKIE_AGE # Disabled for now
  580. LOGIN_REDIRECT_URL = "/"
  581. LOGIN_URL = "/login"
  582. # This config will later default to True and then be removed
  583. USE_NEW_SOCIAL_CALLBACKS = env.bool("USE_NEW_SOCIAL_CALLBACKS", False)
  584. HEADLESS_ONLY = True
  585. HEADLESS_FRONTEND_URLS = {
  586. "account_signup": "/login",
  587. "account_reset_password": "/reset-password",
  588. "account_confirm_email": "/profile/confirm-email/{key}/",
  589. "account_reset_password_from_key": "/reset-password/set-new-password/{key}",
  590. "socialaccount_login_error": "/login?socialLoginError=true",
  591. }
  592. HEADLESS_CLIENTS = ("browser",)
  593. HEADLESS_SERVE_SPECIFICATION = True
  594. MFA_TOTP_ISSUER = GLITCHTIP_URL.hostname
  595. MFA_TOTP_TOLERANCE = 1
  596. MFA_SUPPORTED_TYPES = ["totp", "webauthn", "recovery_codes"]
  597. MFA_PASSKEY_LOGIN_ENABLED = True
  598. MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG
  599. SOCIALACCOUNT_ADAPTER = "glitchtip.adapters.CustomSocialAccountAdapter"
  600. INVITATION_BACKEND = "apps.organizations_ext.invitation_backend.InvitationBackend"
  601. SOCIALACCOUNT_PROVIDERS = {}
  602. if GITLAB_URL := env.url("SOCIALACCOUNT_PROVIDERS_gitlab_GITLAB_URL", None):
  603. SOCIALACCOUNT_PROVIDERS["gitlab"] = {"GITLAB_URL": GITLAB_URL.geturl()}
  604. if GITEA_URL := env.url("SOCIALACCOUNT_PROVIDERS_gitea_GITEA_URL", None):
  605. SOCIALACCOUNT_PROVIDERS["gitea"] = {"GITEA_URL": GITEA_URL.geturl()}
  606. if NEXTCLOUD_URL := env.url("SOCIALACCOUNT_PROVIDERS_nextcloud_SERVER", None):
  607. SOCIALACCOUNT_PROVIDERS["nextcloud"] = {"SERVER": NEXTCLOUD_URL.geturl()}
  608. if MICROSOFT_TENANT := env.str("SOCIALACCOUNT_PROVIDERS_microsoft_TENANT", None):
  609. SOCIALACCOUNT_PROVIDERS["microsoft"] = {"TENANT": MICROSOFT_TENANT}
  610. ENABLE_USER_REGISTRATION = env.bool("ENABLE_USER_REGISTRATION", True)
  611. ENABLE_ORGANIZATION_CREATION = env.bool(
  612. "ENABLE_OPEN_USER_REGISTRATION", env.bool("ENABLE_ORGANIZATION_CREATION", False)
  613. )
  614. AUTHENTICATION_BACKENDS = (
  615. # Needed to login by username in Django admin, regardless of `allauth`
  616. "django.contrib.auth.backends.ModelBackend",
  617. # `allauth` specific authentication methods, such as login by e-mail
  618. "allauth.account.auth_backends.AuthenticationBackend",
  619. )
  620. NINJA_PAGINATION_CLASS = "glitchtip.api.pagination.AsyncLinkHeaderPagination"
  621. NINJA_PAGINATION_PER_PAGE = 50
  622. LOGGING_HANDLER_CLASS = env.str("DJANGO_LOGGING_HANDLER_CLASS", "logging.StreamHandler")
  623. LOGGING = {
  624. "version": 1,
  625. "disable_existing_loggers": False,
  626. "handlers": {
  627. "null": {
  628. "class": "logging.NullHandler",
  629. },
  630. "console": {
  631. "class": LOGGING_HANDLER_CLASS,
  632. },
  633. },
  634. "loggers": {
  635. "django.security.DisallowedHost": {
  636. "handlers": ["null"],
  637. "propagate": False,
  638. },
  639. },
  640. "root": {"handlers": ["console"]},
  641. }
  642. if LOGGING_HANDLER_CLASS is not logging.StreamHandler:
  643. from celery.signals import after_setup_logger, after_setup_task_logger
  644. @after_setup_logger.connect
  645. @after_setup_task_logger.connect
  646. def setup_celery_logging(logger, **kwargs):
  647. from django.utils.module_loading import import_string
  648. handler = import_string(LOGGING_HANDLER_CLASS)
  649. for h in logger.handlers:
  650. logger.removeHandler(h)
  651. logger.addHandler(handler())
  652. def organization_request_callback(request):
  653. raise ImproperlyConfigured(
  654. "Organization request callback required by dj-stripe but not used."
  655. )
  656. # Set to track activity with Plausible
  657. PLAUSIBLE_URL = env.str("PLAUSIBLE_URL", default=None)
  658. PLAUSIBLE_DOMAIN = env.str("PLAUSIBLE_DOMAIN", default=None)
  659. # See https://liberapay.com/GlitchTip/donate - suggested self-host donation is $5/month/user.
  660. # Support plans available. Email info@burkesoftware.com for more info.
  661. I_PAID_FOR_GLITCHTIP = env.bool("I_PAID_FOR_GLITCHTIP", False)
  662. DJSTRIPE_SUBSCRIBER_MODEL = "organizations_ext.Organization"
  663. DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK = organization_request_callback
  664. DJSTRIPE_USE_NATIVE_JSONFIELD = True
  665. DJSTRIPE_FOREIGN_KEY_TO_FIELD = "djstripe_id"
  666. MARKETING_URL = "https://glitchtip.com"
  667. STRIPE_AUTOMATIC_TAX = env.bool("STRIPE_AUTOMATIC_TAX", False)
  668. STRIPE_LIVE_MODE = env.bool("STRIPE_LIVE_MODE", False)
  669. if BILLING_ENABLED:
  670. I_PAID_FOR_GLITCHTIP = True
  671. INSTALLED_APPS.append("djstripe")
  672. INSTALLED_APPS.append("apps.djstripe_ext")
  673. STRIPE_TEST_PUBLIC_KEY = env.str("STRIPE_TEST_PUBLIC_KEY", None)
  674. STRIPE_TEST_SECRET_KEY = env.str("STRIPE_TEST_SECRET_KEY", None)
  675. STRIPE_LIVE_PUBLIC_KEY = env.str("STRIPE_LIVE_PUBLIC_KEY", None)
  676. STRIPE_LIVE_SECRET_KEY = env.str("STRIPE_LIVE_SECRET_KEY", None)
  677. CELERY_BEAT_SCHEDULE["check-all-organizations-throttle"] = {
  678. "task": "apps.organizations_ext.tasks.check_all_organizations_throttle",
  679. "schedule": timedelta(hours=4),
  680. }
  681. elif TESTING:
  682. # Must run tests with djstripe enabled
  683. BILLING_ENABLED = True
  684. INSTALLED_APPS.append("djstripe")
  685. INSTALLED_APPS.append("apps.djstripe_ext")
  686. STRIPE_TEST_PUBLIC_KEY = "fake"
  687. STRIPE_TEST_SECRET_KEY = "sk_test_fake" # nosec
  688. logging.disable(logging.WARNING)
  689. CELERY_TASK_ALWAYS_EAGER = env.bool("CELERY_TASK_ALWAYS_EAGER", False)
  690. if TESTING:
  691. TEST_RUNNER = "glitchtip.test_runner.TimedTestRunner"
  692. # Optimization
  693. PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
  694. DATABASES["default"]["CONN_MAX_AGE"] = None
  695. DATABASES["default"]["OPTIONS"]["pool"] = False
  696. CELERY_TASK_ALWAYS_EAGER = True
  697. SESSION_ENGINE = "django.contrib.sessions.backends.cache"
  698. STORAGES = global_settings.STORAGES
  699. # https://github.com/evansd/whitenoise/issues/215
  700. warnings.filterwarnings(
  701. "ignore", message="No directory at", module="whitenoise.base"
  702. )
  703. if CELERY_TASK_ALWAYS_EAGER:
  704. CACHES = {
  705. "default": {
  706. "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
  707. }
  708. }
  709. CACHE_IS_REDIS = CACHES["default"]["BACKEND"] == "django_redis.cache.RedisCache"
  710. warnings.filterwarnings(
  711. "ignore", message="No directory at", module="django.core.handlers.base"
  712. )