flagsmith-common 2.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. common/__init__.py +0 -0
  2. common/core/__init__.py +6 -0
  3. common/core/app.py +6 -0
  4. common/core/cli/__init__.py +0 -0
  5. common/core/cli/healthcheck.py +120 -0
  6. common/core/logging.py +24 -0
  7. common/core/main.py +105 -0
  8. common/core/management/__init__.py +0 -0
  9. common/core/management/commands/__init__.py +0 -0
  10. common/core/management/commands/docgen.py +63 -0
  11. common/core/management/commands/start.py +61 -0
  12. common/core/management/commands/waitfordb.py +87 -0
  13. common/core/metrics.py +25 -0
  14. common/core/middleware.py +22 -0
  15. common/core/templates/docgen-metrics.md +22 -0
  16. common/core/urls.py +17 -0
  17. common/core/utils.py +239 -0
  18. common/core/views.py +27 -0
  19. common/environments/permissions.py +15 -0
  20. common/features/__init__.py +0 -0
  21. common/features/multivariate/__init__.py +0 -0
  22. common/features/multivariate/serializers.py +19 -0
  23. common/features/serializers.py +68 -0
  24. common/features/versioning/__init__.py +0 -0
  25. common/features/versioning/serializers.py +13 -0
  26. common/gunicorn/__init__.py +0 -0
  27. common/gunicorn/conf.py +18 -0
  28. common/gunicorn/constants.py +23 -0
  29. common/gunicorn/logging.py +120 -0
  30. common/gunicorn/metrics.py +26 -0
  31. common/gunicorn/middleware.py +30 -0
  32. common/gunicorn/utils.py +104 -0
  33. common/migrations/__init__.py +0 -0
  34. common/migrations/helpers/__init__.py +9 -0
  35. common/migrations/helpers/postgres_helpers.py +41 -0
  36. common/organisations/permissions.py +10 -0
  37. common/projects/permissions.py +40 -0
  38. common/prometheus/__init__.py +3 -0
  39. common/prometheus/utils.py +38 -0
  40. common/py.typed +0 -0
  41. common/test_tools/__init__.py +11 -0
  42. common/test_tools/plugin.py +139 -0
  43. common/test_tools/types.py +56 -0
  44. common/test_tools/utils.py +11 -0
  45. common/types.py +45 -0
  46. flagsmith_common-2.2.4.dist-info/METADATA +196 -0
  47. flagsmith_common-2.2.4.dist-info/RECORD +92 -0
  48. flagsmith_common-2.2.4.dist-info/WHEEL +4 -0
  49. flagsmith_common-2.2.4.dist-info/entry_points.txt +6 -0
  50. flagsmith_common-2.2.4.dist-info/licenses/LICENSE +28 -0
  51. task_processor/__init__.py +0 -0
  52. task_processor/admin.py +38 -0
  53. task_processor/apps.py +47 -0
  54. task_processor/decorators.py +209 -0
  55. task_processor/exceptions.py +28 -0
  56. task_processor/health.py +44 -0
  57. task_processor/managers.py +18 -0
  58. task_processor/metrics.py +22 -0
  59. task_processor/migrations/0001_initial.py +44 -0
  60. task_processor/migrations/0002_healthcheckmodel.py +21 -0
  61. task_processor/migrations/0003_add_completed_to_task.py +22 -0
  62. task_processor/migrations/0004_recreate_task_indexes.py +43 -0
  63. task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
  64. task_processor/migrations/0006_auto_20230221_0802.py +45 -0
  65. task_processor/migrations/0007_add_is_locked.py +23 -0
  66. task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
  67. task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
  68. task_processor/migrations/0010_task_priority.py +27 -0
  69. task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
  70. task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
  71. task_processor/migrations/0013_add_last_picked_at.py +34 -0
  72. task_processor/migrations/__init__.py +0 -0
  73. task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
  74. task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
  75. task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
  76. task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
  77. task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
  78. task_processor/migrations/sql/__init__.py +0 -0
  79. task_processor/models.py +237 -0
  80. task_processor/monitoring.py +12 -0
  81. task_processor/processor.py +202 -0
  82. task_processor/py.typed +0 -0
  83. task_processor/routers.py +55 -0
  84. task_processor/serializers.py +7 -0
  85. task_processor/task_registry.py +90 -0
  86. task_processor/task_run_method.py +7 -0
  87. task_processor/tasks.py +71 -0
  88. task_processor/threads.py +128 -0
  89. task_processor/types.py +18 -0
  90. task_processor/urls.py +5 -0
  91. task_processor/utils.py +71 -0
  92. task_processor/views.py +20 -0
common/__init__.py ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ import enum
2
+
3
+
4
+ class ReplicaReadStrategy(enum.StrEnum):
5
+ DISTRIBUTED = enum.auto()
6
+ SEQUENTIAL = enum.auto()
common/core/app.py ADDED
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CoreConfig(AppConfig):
5
+ name = "common.core"
6
+ label = "common_core"
File without changes
@@ -0,0 +1,120 @@
1
+ import argparse
2
+ import socket
3
+ import urllib.parse
4
+
5
+ import requests
6
+
7
+ DEFAULT_PORT = 8000
8
+ DEFAULT_TIMEOUT_SECONDS = 1
9
+
10
+
11
+ def get_args(
12
+ argv: list[str],
13
+ *,
14
+ prog: str,
15
+ ) -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description=(
18
+ "Perform health checks. "
19
+ f"If ran without subcommand, defaults to a TCP check of port {DEFAULT_PORT}."
20
+ ),
21
+ prog=prog,
22
+ )
23
+ subcommands = parser.add_subparsers(dest="subcommand")
24
+ tcp_parser = subcommands.add_parser(
25
+ "tcp",
26
+ help="Check if the API is able to accept local TCP connections",
27
+ )
28
+ tcp_parser.add_argument(
29
+ "--port",
30
+ "-p",
31
+ type=int,
32
+ default=DEFAULT_PORT,
33
+ help=f"Port to check the API on (default: {DEFAULT_PORT})",
34
+ )
35
+ tcp_parser.add_argument(
36
+ "--timeout",
37
+ "-t",
38
+ type=int,
39
+ default=DEFAULT_TIMEOUT_SECONDS,
40
+ help=f"Socket timeout for the connection attempt in seconds (default: {DEFAULT_TIMEOUT_SECONDS})",
41
+ )
42
+ http_parser = subcommands.add_parser(
43
+ "http", help="Check if the API is able to serve HTTP requests"
44
+ )
45
+ http_parser.add_argument(
46
+ "--port",
47
+ "-p",
48
+ type=int,
49
+ default=DEFAULT_PORT,
50
+ help=f"Port to check the API on (default: {DEFAULT_PORT})",
51
+ )
52
+ http_parser.add_argument(
53
+ "--timeout",
54
+ "-t",
55
+ type=int,
56
+ default=DEFAULT_TIMEOUT_SECONDS,
57
+ help=f"Request timeout in seconds (default: {DEFAULT_TIMEOUT_SECONDS})",
58
+ )
59
+ http_parser.add_argument(
60
+ "path",
61
+ nargs="?",
62
+ type=str,
63
+ default="/health/liveness",
64
+ help="Request path (default: /health/liveness)",
65
+ )
66
+ return parser.parse_args(argv)
67
+
68
+
69
+ def check_tcp_connection(
70
+ port: int,
71
+ timeout_seconds: int,
72
+ ) -> None:
73
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
74
+ sock.settimeout(timeout_seconds)
75
+ try:
76
+ sock.connect(("127.0.0.1", port))
77
+ except socket.error as e:
78
+ print(f"Failed: {e} {port=}")
79
+ exit(1)
80
+ else:
81
+ exit(0)
82
+ finally:
83
+ sock.close()
84
+
85
+
86
+ def check_http_response(
87
+ port: int,
88
+ timeout_seconds: int,
89
+ path: str,
90
+ ) -> None:
91
+ url = urllib.parse.urljoin(f"http://127.0.0.1:{port}", path)
92
+ requests.get(
93
+ url,
94
+ timeout=timeout_seconds,
95
+ ).raise_for_status()
96
+
97
+
98
+ def main(
99
+ argv: list[str],
100
+ *,
101
+ prog: str,
102
+ ) -> None:
103
+ args = get_args(argv, prog=prog)
104
+ match args.subcommand:
105
+ case None:
106
+ check_tcp_connection(
107
+ port=DEFAULT_PORT,
108
+ timeout_seconds=DEFAULT_TIMEOUT_SECONDS,
109
+ )
110
+ case "tcp":
111
+ check_tcp_connection(
112
+ port=args.port,
113
+ timeout_seconds=args.timeout,
114
+ )
115
+ case "http":
116
+ check_http_response(
117
+ port=args.port,
118
+ timeout_seconds=args.timeout,
119
+ path=args.path,
120
+ )
common/core/logging.py ADDED
@@ -0,0 +1,24 @@
1
+ import json
2
+ import logging
3
+ from typing import Any
4
+
5
+
6
+ class JsonFormatter(logging.Formatter):
7
+ """Custom formatter for json logs."""
8
+
9
+ def get_json_record(self, record: logging.LogRecord) -> dict[str, Any]:
10
+ formatted_message = record.getMessage()
11
+ json_record = {
12
+ "levelname": record.levelname,
13
+ "message": formatted_message,
14
+ "timestamp": self.formatTime(record, self.datefmt),
15
+ "logger_name": record.name,
16
+ "pid": record.process,
17
+ "thread_name": record.threadName,
18
+ }
19
+ if record.exc_info:
20
+ json_record["exc_info"] = self.formatException(record.exc_info)
21
+ return json_record
22
+
23
+ def format(self, record: logging.LogRecord) -> str:
24
+ return json.dumps(self.get_json_record(record))
common/core/main.py ADDED
@@ -0,0 +1,105 @@
1
+ import contextlib
2
+ import logging
3
+ import os
4
+ import sys
5
+ import typing
6
+
7
+ from django.core.management import (
8
+ execute_from_command_line as django_execute_from_command_line,
9
+ )
10
+ from environs import Env
11
+
12
+ from common.core.cli import healthcheck
13
+ from common.core.utils import TemporaryDirectory
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @contextlib.contextmanager
19
+ def ensure_cli_env() -> typing.Generator[None, None, None]:
20
+ """
21
+ Set up the environment for the main entry point of the application
22
+ and clean up after it's done.
23
+
24
+ Add environment-related code that needs to happen before and after Django is involved
25
+ to here.
26
+
27
+ Use as a context manager, e.g.:
28
+
29
+ ```python
30
+ with ensure_cli_env():
31
+ main()
32
+ ```
33
+ """
34
+ env = Env()
35
+ ctx = contextlib.ExitStack()
36
+
37
+ # TODO @khvn26 Move logging setup to here
38
+
39
+ # Currently we don't install Flagsmith modules as a package, so we need to add
40
+ # $CWD to the Python path to be able to import them
41
+ sys.path.append(os.getcwd())
42
+
43
+ # TODO @khvn26 We should find a better way to pre-set the Django settings module
44
+ # without resorting to it being set outside of the application
45
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
46
+
47
+ # Set up Prometheus' multiprocess mode
48
+ if not env.str("PROMETHEUS_MULTIPROC_DIR", ""):
49
+ delete = not env.bool("PROMETHEUS_MULTIPROC_DIR_KEEP", False)
50
+ prometheus_multiproc_dir_name = ctx.enter_context(
51
+ TemporaryDirectory(delete=delete)
52
+ )
53
+ logger.info(
54
+ "Created %s for Prometheus multi-process mode",
55
+ prometheus_multiproc_dir_name,
56
+ )
57
+ os.environ["PROMETHEUS_MULTIPROC_DIR"] = prometheus_multiproc_dir_name
58
+
59
+ if "docgen" in sys.argv:
60
+ os.environ["DOCGEN_MODE"] = "true"
61
+
62
+ if "task-processor" in sys.argv:
63
+ # A hacky way to signal we're not running the API
64
+ os.environ["RUN_BY_PROCESSOR"] = "true"
65
+
66
+ with ctx:
67
+ yield
68
+
69
+
70
+ def execute_from_command_line(argv: list[str]) -> None:
71
+ try:
72
+ subcommand = argv[1]
73
+ subcommand_main = {
74
+ "healthcheck": healthcheck.main,
75
+ # Backwards compatibility for task-processor health checks
76
+ # See https://github.com/Flagsmith/flagsmith-task-processor/issues/24
77
+ "checktaskprocessorthreadhealth": healthcheck.main,
78
+ }[subcommand]
79
+ except (IndexError, KeyError):
80
+ django_execute_from_command_line(argv)
81
+ else:
82
+ subcommand_main(
83
+ argv[2:],
84
+ prog=f"{os.path.basename(argv[0])} {subcommand}",
85
+ )
86
+
87
+
88
+ def main(argv: list[str] = sys.argv) -> None:
89
+ """
90
+ The main entry point to the Flagsmith application.
91
+
92
+ An equivalent to Django's `manage.py` script, this module is used to run management commands.
93
+
94
+ It's installed as the `flagsmith` command.
95
+
96
+ Everything that needs to be run before Django is started should be done here.
97
+
98
+ The end goal is to eventually replace Core API's `run-docker.sh` with this.
99
+
100
+ Usage:
101
+ `flagsmith <command> [options]`
102
+ """
103
+ with ensure_cli_env():
104
+ # Run own commands and Django
105
+ execute_from_command_line(argv)
File without changes
File without changes
@@ -0,0 +1,63 @@
1
+ from operator import itemgetter
2
+ from typing import Any, Callable
3
+
4
+ import prometheus_client
5
+ from django.core.management import BaseCommand, CommandParser
6
+ from django.template.loader import get_template
7
+ from django.utils.module_loading import autodiscover_modules
8
+ from prometheus_client.metrics import MetricWrapperBase
9
+
10
+
11
+ class Command(BaseCommand):
12
+ help = "Generate documentation for the Flagsmith codebase."
13
+
14
+ def add_arguments(self, parser: CommandParser) -> None:
15
+ subparsers = parser.add_subparsers(
16
+ title="sub-commands",
17
+ required=True,
18
+ )
19
+
20
+ metric_parser = subparsers.add_parser(
21
+ "metrics",
22
+ help="Generate metrics documentation.",
23
+ )
24
+ metric_parser.set_defaults(handle_method=self.handle_metrics)
25
+
26
+ def initialise(self) -> None:
27
+ from common.gunicorn import metrics # noqa: F401
28
+
29
+ autodiscover_modules(
30
+ "metrics",
31
+ )
32
+
33
+ def handle(
34
+ self,
35
+ *args: Any,
36
+ handle_method: Callable[..., None],
37
+ **options: Any,
38
+ ) -> None:
39
+ self.initialise()
40
+ handle_method(*args, **options)
41
+
42
+ def handle_metrics(self, *args: Any, **options: Any) -> None:
43
+ template = get_template("docgen-metrics.md")
44
+
45
+ flagsmith_metrics = sorted(
46
+ (
47
+ {
48
+ "name": collector._name,
49
+ "documentation": collector._documentation,
50
+ "labels": collector._labelnames,
51
+ "type": collector._type,
52
+ }
53
+ for collector in prometheus_client.REGISTRY._collector_to_names
54
+ if isinstance(collector, MetricWrapperBase)
55
+ ),
56
+ key=itemgetter("name"),
57
+ )
58
+
59
+ self.stdout.write(
60
+ template.render(
61
+ context={"flagsmith_metrics": flagsmith_metrics},
62
+ )
63
+ )
@@ -0,0 +1,61 @@
1
+ from typing import Any, Callable
2
+
3
+ from django.core.management import BaseCommand, CommandParser
4
+ from django.utils.module_loading import autodiscover_modules
5
+
6
+ from common.gunicorn.utils import add_arguments as add_gunicorn_arguments
7
+ from common.gunicorn.utils import run_server
8
+ from task_processor.utils import add_arguments as add_task_processor_arguments
9
+ from task_processor.utils import start_task_processor
10
+
11
+
12
+ class Command(BaseCommand):
13
+ help = "Start the Flagsmith application."
14
+
15
+ def create_parser(self, *args: Any, **kwargs: Any) -> CommandParser:
16
+ return super().create_parser(*args, conflict_handler="resolve", **kwargs)
17
+
18
+ def add_arguments(self, parser: CommandParser) -> None:
19
+ add_gunicorn_arguments(parser)
20
+
21
+ subparsers = parser.add_subparsers(
22
+ title="sub-commands",
23
+ required=True,
24
+ )
25
+
26
+ api_parser = subparsers.add_parser(
27
+ "api",
28
+ help="Start the Core API.",
29
+ )
30
+ api_parser.set_defaults(handle_method=self.handle_api)
31
+
32
+ task_processor_parser = subparsers.add_parser(
33
+ "task-processor",
34
+ help="Start the Task Processor.",
35
+ )
36
+ task_processor_parser.set_defaults(handle_method=self.handle_task_processor)
37
+ add_task_processor_arguments(task_processor_parser)
38
+
39
+ def initialise(self) -> None:
40
+ autodiscover_modules(
41
+ "metrics",
42
+ "tasks",
43
+ )
44
+
45
+ def handle(
46
+ self,
47
+ *args: Any,
48
+ handle_method: Callable[..., None],
49
+ **options: Any,
50
+ ) -> None:
51
+ self.initialise()
52
+ handle_method(*args, **options)
53
+
54
+ def handle_api(self, *args: Any, **options: Any) -> None:
55
+ run_server(options)
56
+
57
+ def handle_task_processor(self, *args: Any, **options: Any) -> None:
58
+ with start_task_processor(options):
59
+ # Delegate signal handling to Gunicorn.
60
+ # The task processor will finalise once Gunicorn is shut down.
61
+ run_server(options)
@@ -0,0 +1,87 @@
1
+ import logging
2
+ import time
3
+ from argparse import ArgumentParser
4
+ from typing import Any
5
+
6
+ from django.core.management import BaseCommand, CommandError
7
+ from django.db import OperationalError, connections
8
+ from django.db.migrations.executor import MigrationExecutor
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Command(BaseCommand):
14
+ def add_arguments(self, parser: ArgumentParser) -> None:
15
+ parser.add_argument(
16
+ "--waitfor",
17
+ type=int,
18
+ dest="wait_for",
19
+ help="Number of seconds to wait for db to become available.",
20
+ default=5,
21
+ )
22
+ parser.add_argument(
23
+ "--migrations",
24
+ action="store_true",
25
+ dest="should_wait_for_migrations",
26
+ help="Whether to wait until all migrations are applied.",
27
+ default=False,
28
+ )
29
+ parser.add_argument(
30
+ "--database",
31
+ type=str,
32
+ dest="database",
33
+ help=(
34
+ 'The database to wait for ("default", "analytics").'
35
+ 'Defaults to the "default" database.'
36
+ ),
37
+ default="default",
38
+ )
39
+
40
+ def handle(
41
+ self,
42
+ *args: Any,
43
+ wait_for: int,
44
+ should_wait_for_migrations: bool,
45
+ database: str,
46
+ **options: Any,
47
+ ) -> None:
48
+ start = time.monotonic()
49
+ wait_between_checks = 0.25
50
+
51
+ logger.info("Checking if database is ready for connections.")
52
+
53
+ while True:
54
+ if time.monotonic() - start > wait_for:
55
+ msg = f"Failed to connect to DB within {wait_for} seconds."
56
+ logger.error(msg)
57
+ raise CommandError(msg)
58
+
59
+ conn = connections.create_connection(database)
60
+ try:
61
+ with conn.temporary_connection() as cursor:
62
+ cursor.execute("SELECT 1")
63
+ logger.info("Successfully connected to the database.")
64
+ break
65
+ except OperationalError as e:
66
+ logger.warning("Database not yet ready for connections.")
67
+ logger.warning("Error was: %s: %s", e.__class__.__name__, e)
68
+
69
+ time.sleep(wait_between_checks)
70
+
71
+ if should_wait_for_migrations:
72
+ logger.info("Checking for applied migrations.")
73
+
74
+ while True:
75
+ if time.monotonic() - start > wait_for:
76
+ msg = f"Didn't detect applied migrations for {wait_for} seconds."
77
+ logger.error(msg)
78
+ raise CommandError(msg)
79
+
80
+ conn = connections[database]
81
+ executor = MigrationExecutor(conn)
82
+ if not executor.migration_plan(executor.loader.graph.leaf_nodes()):
83
+ logger.info("No pending migrations detected, good to go.")
84
+ return
85
+
86
+ logger.warning("Migrations not yet applied.")
87
+ time.sleep(wait_between_checks)
common/core/metrics.py ADDED
@@ -0,0 +1,25 @@
1
+ import prometheus_client
2
+ from django.conf import settings
3
+
4
+ from common.core.utils import get_version_info
5
+
6
+ flagsmith_build_info = prometheus_client.Gauge(
7
+ "flagsmith_build_info",
8
+ "Flagsmith version and build information.",
9
+ ["ci_commit_sha", "version"],
10
+ multiprocess_mode="livemax",
11
+ )
12
+
13
+
14
+ def advertise() -> None:
15
+ # Advertise Flagsmith build info.
16
+ version_info = get_version_info()
17
+
18
+ flagsmith_build_info.labels(
19
+ ci_commit_sha=version_info["ci_commit_sha"],
20
+ version=version_info.get("package_versions", {}).get(".") or "unknown",
21
+ ).set(1)
22
+
23
+
24
+ if not settings.DOCGEN_MODE:
25
+ advertise()
@@ -0,0 +1,22 @@
1
+ from typing import Callable
2
+
3
+ from django.http import HttpRequest, HttpResponse
4
+
5
+ from common.core.utils import get_version
6
+
7
+
8
+ class APIResponseVersionHeaderMiddleware:
9
+ """
10
+ Middleware to add the API version to the response headers
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ get_response: Callable[[HttpRequest], HttpResponse],
16
+ ) -> None:
17
+ self.get_response = get_response
18
+
19
+ def __call__(self, request: HttpRequest) -> HttpResponse:
20
+ response = self.get_response(request)
21
+ response.headers["Flagsmith-Version"] = get_version()
22
+ return response
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: Metrics
3
+ sidebar_label: Metrics
4
+ sidebar_position: 20
5
+ ---
6
+
7
+ ## Prometheus
8
+
9
+ To enable the Prometheus `/metrics` endpoint, set the `PROMETHEUS_ENABLED` environment variable to `true`.
10
+
11
+ The metrics provided by Flagsmith are described below.
12
+
13
+ {% for metric in flagsmith_metrics %}
14
+ ### `{{ metric.name }}`
15
+
16
+ {{ metric.type|title }}.
17
+
18
+ {{ metric.documentation }}
19
+
20
+ Labels:
21
+ {% for label in metric.labels %} - `{{ label }}`
22
+ {% endfor %}{% endfor %}
common/core/urls.py ADDED
@@ -0,0 +1,17 @@
1
+ from django.conf import settings
2
+ from django.urls import include, path, re_path
3
+
4
+ from common.core import views
5
+
6
+ urlpatterns = [
7
+ path("version/", views.version_info),
8
+ path("health/liveness/", views.liveness),
9
+ path("health/readiness/", include("health_check.urls", namespace="health")),
10
+ re_path(r"^health", include("health_check.urls", namespace="health-deprecated")),
11
+ # Aptible health checks must be on /healthcheck and cannot redirect
12
+ # see https://www.aptible.com/docs/core-concepts/apps/connecting-to-apps/app-endpoints/https-endpoints/health-checks
13
+ re_path(r"^healthcheck", include("health_check.urls", namespace="health-aptible")),
14
+ ]
15
+
16
+ if settings.PROMETHEUS_ENABLED:
17
+ urlpatterns += [path("metrics/", views.metrics)]