settings.py 28 KB

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